初识 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 方法主要有三个实现步骤,下面我们来具体分析每一步的实现

  1. 处理 config 属性和默认属性 defaultProps
  2. 处理 children 子节点
  3. 通过 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 元素,主要有三个实现步骤

  1. 处理 jsx 的属性,将 jsx 上的属性放到 props 中
  2. 处理 jsx 的子节点,将子节点 children 放到 props 中
  3. 通过 ReactElement 创建一个 react 元素