一、createElement
上一章我们讲到了所有jsx语法都会被转成createElement。
那么createElement的实现是怎样的呢?
首先我们从github克隆下来react的源码库,我们先来分析下react源码库的文件布局。
react工程根目录下有packages文件夹,其间放置的是react的各个包,我们暂时把着力点放于react目录下。内部是react源码实现。
抛出去一些非必要的检测,和warn代码,核心的react代码其实只有几百行。react源码本身并不复杂,负责渲染的react-dom才是最复杂的。
react目录的src,就是react的核心实现了。
createElement方法位于ReactElement.js文件内,实现如下:
export function createElement(type, config, children) { let propName; // Reserved names are extracted const props = {}; let key = null; let ref = null; let self = null; let source = null; if (config != null) { if (hasValidRef(config)) { ref = config.ref; } if (hasValidKey(config)) { key = '' + config.key; } self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object for (propName in config) { if ( hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; } } } // Children can be more than one argument, and those are transferred onto // the newly allocated props object. const childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { const childArray = Array(childrenLength); for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } if (__DEV__) { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } if (__DEV__) { if (key || ref) { const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; if (key) { defineKeyPropWarningGetter(props, displayName); } if (ref) { defineRefPropWarningGetter(props, displayName); } } } return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, );}复制代码
这里面有一些开发环境下检测,和外部调用方法,可能会使阅读者精力分散,我们来稍微改动精简下代码,使功能一致,同时更好阅读:
export function createElement(type, config, ...children) { const {ref = null, key = null} = config || {}; const {current} = ReactCurrentOwner; const {defaultProps} = type || {}; const props = assignProps(config, defaultProps, children); return new ReactElement({ type, key: '' + key, ref, current, props, });}复制代码
经过精简和简化后,createElement仅有30行代码。我们来逐行解析下。
/** * * @param type {string | function | object} * 如果type是字符串,那就是原生dom元素,比如div * 如果是function或者是Component的子类 则是React组件 * object 会是一些特殊的type 比如fragment * @param config {object} * props 和key 还有ref 其实都是在config里了 * @param children * 就是由其他嵌套createElement方法返回的ReactElement实例 * @returns {ReactElement} * */export function createElement(type, config, ...children) { // 给config设置一个空对象的默认值 // ref和key 默认为null const {ref = null, key = null} = config || {}; // ReactCurrentOwner负责管理当前渲染的组件和节点 const {current} = ReactCurrentOwner; // 如果是函数组件和类组件 是可以有defaultProps的 // 比如 // function A({age}) {return{age}} // A.defaultProps = { age:123 } const {defaultProps} = type || {}; // 把defaultProps和props 合并一下 const props = assignProps(config, defaultProps, children); // 返回了一个ReactElement实例 return new ReactElement({ type, key: '' + key, ref, current, props, });}复制代码
ref和key不用多说,大家都知道是干啥的。之前有个同事问过我,key明明传的是数字,为啥最后成了字符串,症结就在上面的ReactELement构造函数传参的key那里了,key:''+key
。
assignProps是我抽象了一个方法,合并defaultProps和传入props的方法,稍后提供代码,其实在cloneElement方法里,也有一段类似代码,但是react并没有抽象出来,相对来说,会有代码冗余,暂且提炼出来。
重点在new ReactElement()。
react的代码里,ReactElement是个工厂函数,返回一个对象。但是我个人觉得比较奇怪。
第一、工厂函数生成实例,这个工厂函数不该大写开头。
第二、使用构造函数或者类来声明ReactElement难道不是一个更好,更符合语义的选择?
在这里,为了便于理解,把ReactElement从工厂函数,改变成了一个类,createElement返回的就是一个ReactElement类的实例。
下面看下asssignProps的实现,该方法在cloneElement也可以复用:
const RESERVED_PROPS = ['key', 'ref', '__self', '__source'];export function assignProps(config, defaultProps, children) { const props = { children, }; config = config || {}; for (const propName in config) { if ( config.hasOwnProperty(propName) && !RESERVED_PROPS.includes(propName) ) { props[propName] = config[propName]; if ( props[propName] === undefined && defaultProps && defaultProps[propName] !== undefined ) { props[propName] = defaultProps[propName]; } } } return props;}复制代码
二、ReactElement
create返回的是个ReactElement实例,那么ReactElement又是啥呢?
抛出去dev时的代码,精简后如下:
const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, props: props, _owner: owner, }; return element;};复制代码
可以看到,其实就是返回了一个对象,我们现在可以简单而浮夸的想象下,react的render机制其实就是读取这些数据结构,然后根据结构树,层层根据原生dom方法渲染而成。(暂时这样想象)
经过用类改造后的代码为:
export class ReactElement { constructor(elementParams) { const {type, key, ref, current, props} = elementParams || {}; // 如果是原生标签比如h1 那就是字符串 // 如果是组件 则是组件的引用 this.type = type; // key this.key = key; // ref this.ref = ref; // 延后再讲 this._owner = current; // props this.props = props; // 类型标识 新版本中的React里是symbo this.$$typeof = REACT_ELEMENT_TYPE; }}复制代码
三、总结
本章的重点在于,在react中,jsx标签的本质就是ReactElement,createElement会对组件或者dom的type和props经过一层封装处理,最后返回了ReactElement的实例。