初识 react 原理,createElement 方法做了什么
基础介绍
我们在使用 react 的时候,编写的都是 jsx 语法
1function Child({id}) {
2 return <div id={id}>Child</div>
3}
4
5function App() {
6 return (
7 <div>
8 <Child id="child">
9 </div>
10 )
11}
在编译阶段,上面的 jsx 代码会被 Babel 的 @babel/plugin-transform-react-jsx
插件转化为 createElement
的形式
1function Child({ id }) {
2 return React.createElement('div', { id: id }, 'Child')
3}
4
5function App() {
6 return React.createElement(
7 'div',
8 null,
9 React.createElement(Child, { id: 'child' })
10 )
11}
下面我们就来具体分析 createElement
方法的具体实现
createElement 实现原理
createElement
方法定义在 packages/react/src/React.js
,在开发环境和生产环境会使用不同的方法,我们直接看开发环境使用的 createElementProd
方法
1// packages/react/src/React.js
2const createElement: any = __DEV__
3 ? createElementWithValidation
4 : createElementProd
createElementProd
方法有三个参数,type 是元素的类型,可以是 html 元素,也可以是自定义的组件;config 是元素上面的各种属性;children 是元素的子节点
createElementProd
方法主要有三个实现步骤,下面我们来具体分析每一步的实现
- 处理 config 属性和默认属性 defaultProps
- 处理 children 子节点
- 通过
ReactElement
方法返回 react 元素
第一步首先处理 config 属性,config 中存在 4 个特殊的元素,对于这 4 个特殊的元素,会先校验是否存在,如果存在才放到 props 中,其他属性则直接放到 props 中
这 4 个特殊的属性分别是
- key:元素的唯一标识,可以根据 key 更高效的判断哪些元素需要做新增、修改、删除操作
- ref:获取 react 元素的引用,可以用来直接操作 dom 元素
- self:标识 react 元素所属的组件实例的
this
上下文,主要是在 devtool 中使用 - srouce:标识 react 元素的源码位置信息,主要是在开发调试中更方便
如果 type 存在 defaultProps 属性的话,会遍历 defaultProps,如果在 props 中也没有 defaultProps 的属性定义的话,就放到 props 中
1export function createElement(type, config, children) {
2 let propName
3
4 // 1. 处理 config 属性和默认属性 defaultProps
5 const props = {}
6
7 let key = null
8 let ref = null
9 let self = null
10 let source = null
11
12 if (config != null) {
13 if (hasValidRef(config)) {
14 ref = config.ref
15 }
16 if (hasValidKey(config)) {
17 key = '' + config.key
18 }
19
20 self = config.__self === undefined ? null : config.__self
21 source = config.__source === undefined ? null : config.__source
22
23 // 其余属性都作为 props 传递
24 for (propName in config) {
25 if (
26 hasOwnProperty.call(config, propName) &&
27 !RESERVED_PROPS.hasOwnProperty(propName)
28 ) {
29 props[propName] = config[propName]
30 }
31 }
32 }
33
34 // 处理 defaultProps 属性
35 if (type && type.defaultProps) {
36 const defaultProps = type.defaultProps
37 for (propName in defaultProps) {
38 if (props[propName] === undefined) {
39 props[propName] = defaultProps[propName]
40 }
41 }
42 }
43}
第二步处理 children 子节点,因为元素的子节点可能只有一个,也可能有多个
- 如果只有一个子节点(
createElement
方法的参数有 3 个参数),直接将 children 放到 props 中 - 如果有多个并列的子节点(
createElement
参数数量大于 3),将所有子节点放到一个数组 childArray 中,再将 childArray 放到 props 中
1export function createElement(type, config, children) {
2 // ...
3
4 // 2. 处理 children 子节点
5 const childrenLength = arguments.length - 2
6 if (childrenLength === 1) {
7 props.children = children
8 } else if (childrenLength > 1) {
9 const childArray = Array(childrenLength)
10 for (let i = 0; i < childrenLength; i++) {
11 childArray[i] = arguments[i + 2]
12 }
13 props.children = childArray
14 }
15
16 // ...
17}
第三步使用 ReactElement
方法创建 react 元素,ReactElement
方法本质上工厂函数,将上一步处理好的 type、key、ref、props 封装为一个标准对象,对象新增了两个属性
$$typeof
: 标记这是一个 react 元素_owner
: 用于记录创建当前元素的组件,即父组件
1export function createElement(type, config, children) {
2 // ...
3
4 // 3. 返回 react 元素
5 return ReactElement(
6 type,
7 key,
8 ref,
9 self,
10 source,
11 ReactCurrentOwner.current,
12 props
13 )
14}
15
16function ReactElement(type, key, ref, self, source, owner, props) {
17 const element = {
18 // 标记这是一个 react 元素
19 $$typeof: REACT_ELEMENT_TYPE,
20
21 // createElement 传进来的属性
22 type: type,
23 key: key,
24 ref: ref,
25 props: props,
26
27 // 记录创建当前元素的组件,即父组件
28 _owner: owner,
29 }
30
31 return element
32}
总结
在编译阶段,Babel 会将 jsx 转换为 createElement
嵌套的形式,createElement
方法本质是将 jsx 的嵌套结构,转化为标准的 react 元素,主要有三个实现步骤
- 处理 jsx 的属性,将 jsx 上的属性放到 props 中
- 处理 jsx 的子节点,将子节点 children 放到 props 中
- 通过
ReactElement
创建一个 react 元素