React源码解读之ComponentMount
作为初级码农不该天花乱坠大讲情怀,一开始入坑了几天对于很多地方充满了爱迪生般的诸多疑问(莫名把自己夸了一波,XD),所以打算看一波源代码,这个过程可以说是非常曲折,本人智商不高,看了四五遍部分源码后,一脸懵逼,于是在接下来的一周内处于浑浑噩噩,若即若离的抽离状态,于是放弃了解读,最近感觉学习react有两个月了,公司大牛的教诲和启发,打算原路折回再次拾起react这个胖小孩,不得不说有大牛的帮助真会让你进步飞快。
这是本人的第一篇react相关的文章,本来只是留作自己笔记之用,结果笔记越写越多,一方面是为了加深自己对于优雅的react的理解,另一方面为了给计划学习react的旁友们提供一点微不足道的小思路。 当然一提到分析解读源码,这几个庄重的字眼的时候,首先是油然而生的浓浓的自豪感,自豪感不能白来,因此也是谨慎地翻墙看了很多别人的解读,对于一些大神们解读首先是敬佩,然后觉得应该仿效他们进行更多详细的补充,当然写的有所纰漏,不足之处还希望大神们指出。
废话太多了,进入正题,下面是我自己列出的TODOList,在读源码前应该需要理解一些相关的要点
1.什么是JSX?
JSX 的官方定义是类 XML 语法的 ECMAScript 扩展。它完美地利用了 JavaScript 自带的语法 和特性,并使用大家熟悉的 HTML 语法来创建虚拟元素。使用类 XML 语法的好处是标签可以任意嵌套,我们可以像HTML一样清晰地看到DOM树
JSX 将 HTML 语法直接加入到 JavaScript代码中,在实际开发中,JSX在产品打包阶段都已经编译成纯JavaScript,不会带来任何副作用,反而会让代码更加直观并易于维护。
更多详见:CSDN
2.React.createElement
React.createElement(type, config, children) 做了三件事:
- 把 config里的数据一项一项拷入props,
- 拷贝 children 到 props.children,
- 拷贝 type.defaultProps 到 props;
3.组件生命周期
4.renderedElement和ReactDOMComponent
ReactElement
是React元素在内存中的表示形式,可以理解为一个数据类,包含type,key,refs,props等成员变量, ReactComponent
是React元素的操作类,包含mountComponent(), updateComponent()等很多操作组件的方法,主要有ReactDOMComponent, ReactCompositeComponent, ReactDOMTextComponent, ReactDOMEmptyComponent四个类型
5.inst
inst是对于组件的实例化对象,主要包括props, refs, context, updater更新方法集, state等。
顶级组件的实例化属性:
子组件的实例化属性:
6._currentElement
currentElement即为每次实例化的renderedElement,为了在componentInstance保存当前信息
var ReactCompositeComponentWrapper = function (element) { this.construct(element); };
源码分析
接下来配合一个小例子来大概分析下react内部神秘的组件挂载操作
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="./js/react.js"></script> <script src="./js/react-dom.js"></script> <script src="./js/browser.js"></script> <script type="text/babel"> class Children extends React.Component { constructor(...args) { super(...args); } render() { return <div>children</div> } } class Comp extends React.Component{ constructor(...args) { super(...args); this.state = {i: 0} } render(){ return <div onClick={() => { this.setState({i: this.state.i + 1}) }}>Hello, world! {this.props.name}, 年龄{this.props.age} {this.state.i} <i>222</i><Children /></div>; } } window.onload = function(){ var oDiv = document.getElementById('div1'); ReactDOM.render( <Comp name="zjf" age='24'/>, oDiv ); } </script> </head> <body> <div id="div1"><div>2222</div></div> </body> </html>
本次源码分析的版本号是v15.6.0(160之后变化很大有点看不懂),可以使用git reset --hard v15.6.0操作进行版本回退
首先函数的入口是reactDOM.render(), 这个函数可以放两个参数,第一个为需要渲染的组件,第二个为第一个组件挂载的对象。
通过调用ReactDom.render() -> 调用ReactMount.render() -> 调用renderSubtreeIntoContainer, 在这个函数里个人认为需要知道:
// parentComponent一般为null, nextElement,container分别为reactDOM.render中的前两个参数 renderSubtreeIntoContainer(parentComponent, nextElement, container, callback){ // ... // TopLevelWrapper为顶级容器,类型为object(其实是一个方法),内部有个rootID属性,值得注意的是该方法原型链上有render方法,该方法是第一个被调用的,它应该很自豪 var nextWrappedElement = React.createElement(TopLevelWrapper, { child: nextElement }); // 开始进入正轨,该方法内部会根据nextWrapperElement生成相应类型的组件 var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance() }
_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) { // 实例化组件,通过nextElement.type判断,string,object生成ReactDOMComponent, ReactCompositeComponent如果不存在nextElement则生成ReactEmptyComponent,如果typeof nextElement类型为string或者number直接生成ReactDOMTextComponent var componentInstance = instantiateReactComponent(nextElement, false); // The initial render is synchronous but any updates that happen during, rendering, in componentWillMount or componentDidMount, will be batched according to the current batching strategy. ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context); return componentInstance; },
// transaction.perform其实是事务,事务中简单地说有initialize->执行perform第一个callback->close操作,准备在setState介绍 function batchedMountComponentIntoNode(componentInstance, container, shouldReuseMarkup, context) { // var transaction = ReactUpdates.ReactReconcileTransaction.getPooled( /* useCreateElement */ !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement); transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context); ReactUpdates.ReactReconcileTransaction.release(transaction); }
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) { // 根据wrapperInstance来调用不同组件类型的mountComponent方法 var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, null, ReactDOMContainerInfo(wrapperInstance, container), context, 0 /* parentDebugID */); wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance; // setInnerHTML(container, markup),最终会将markup虚拟节点插入真正的DOM树 ReactMount._mountImageIntoNode(markup, container, wrapperInstance, shouldReuseMarkup, transaction); }
mountComponent:
不同的React组件的mountComponent实现都有所区别,下面分析React自定义组件类
// 来到了组件的挂载,需要注意几个变量: renderedElement, _renderedComponent, inst, ReactInstanceMap.set(inst, this), _pendingStateQueue, _pendingForceUpdate, _processPendingState,_processContext, componentWillMount, componentDidMount // 本质上是调用Component构造方法的新实例对象,这个instance上会新增,context,props,refs以及updater属性(见图二),后续使用Map的形式用此作为key,组件作为value,方便之后获取组件,比如上面所说的type为TopLevelWrapper,构造其实例 var Component = this._currentElement.type; var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue); // inst或者inst.render为空对应的是stateless组件,也就是无状态组件 // 无状态组件没有实例对象,它本质上只是一个返回JSX的函数而已。是一种轻量级的React组件 if (!shouldConstruct(Component) && (inst == null || inst.render == null)) { renderedElement = inst; warnIfInvalidElement(Component, renderedElement); inst = new StatelessComponent(Component); } // Store a reference from the instance back to the internal representation ReactInstanceMap.set(inst, this); this._pendingStateQueue = null; this._pendingReplaceState = false; this._pendingForceUpdate = false; // ... // 初始化挂载 markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context); // 将componentDidMount以事务的形式进行调用 transaction.getReactMountReady().enqueue(function () { measureLifeCyclePerf(function () { return inst.componentDidMount(); }, _this._debugID, 'componentDidMount'); });
图二:
performInitialMount:
// render前调用componentWillMount inst.componentWillMount() // 将state提前合并,故在componentWillMount中调用setState不会触发重新render,而是做一次state合并。这样做的目的是减少不必要的重新渲染 // _processPendingState进行原有state的合并, _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial); 以及设置this._pendingStateQueue = null,这也就意味着dirtyComponents进入下一次循环时,执行performUpdateIfNecessary不会再去更新组件 if (this._pendingStateQueue) { inst.state = this._processPendingState(inst.props, inst.context); } // 如果不是stateless,即无状态组件,则调用render,返回ReactElement if (renderedElement === undefined) { renderedElement = this._renderValidatedComponent(); } var nodeType = ReactNodeTypes.getType(renderedElement); this._renderedNodeType = nodeType; var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */ ); this._renderedComponent = child; // 递归渲染,渲染子组件,返回markup,匹配同类型的组件,返回markup var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID); } // 比如 var markup = internalInstance.mountComponent(transaction, hostParent, hostContainerInfo, context, parentDebugID);
_renderValidatedComponent:
// 调用render方法,得到ReactElement。JSX经过babel转译后其实就是createElement()方法,比如上面所提到的TopLevelWrapper内有render方法(图三,图四) var renderedComponent = inst.render();
图三:
图四:
由renderedElement.type类型可以知道所要生成的组件类型为reactDOMComponent,来看下这个对象下的mountComponent
方法
if (namespaceURI === DOMNamespaces.html) { if (this._tag === 'script') { // 当插入标签为script的时候react也进行了包装,这样script就只是innerHTML不会进行执行,不然会有注入的危险 var div = ownerDocument.createElement('div'); var type = this._currentElement.type; div.innerHTML = '<' + type + '></' + type + '>'; el = div.removeChild(div.firstChild); } else if (props.is) { el = ownerDocument.createElement(this._currentElement.type, props.is); } else { // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug. // See discussion in https://github.com/facebook/react/pull/6896 // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240 el = ownerDocument.createElement(this._currentElement.type); } } else { el = ownerDocument.createElementNS(namespaceURI, this._currentElement.type); } // Populate `_hostNode` on the rendered host/text component with the given DOM node. ReactDOMComponentTree.precacheNode(this, el); this._flags |= Flags.hasCachedChildNodes; if (!this._hostParent) { // 在根节点上设置data-root属性 DOMPropertyOperations.setAttributeForRoot(el); } this._updateDOMProperties(null, props, transaction); // 初始化lazyTree,返回实例 // node: node, // children: [], // html: null, // text: null, // toString: toString var lazyTree = DOMLazyTree(el); // 遍历内部props,判断props.children内部是string/number类型还是其他类型,如果是前者直接将内部children插入到node中去,否则就需要非string/number类型进行继续渲染 this._createInitialChildren(transaction, props, context, lazyTree); mountImage = lazyTree; return mountImage;
上面代码其中有必要了解下DOMLazyTree的一些属性方法因为之后会有调用以及_createInitialChildren,这个是将props.children转换为innerHTML的关键
function DOMLazyTree(node) { return { node: node, children: [], html: null, text: null, toString: toString }; } DOMLazyTree.insertTreeBefore = insertTreeBefore; DOMLazyTree.replaceChildWithTree = replaceChildWithTree; // 按序向节点的子节点列表的末尾添加新的子节点 DOMLazyTree.queueChild = queueChild; // 按序插入HTML DOMLazyTree.queueHTML = queueHTML; // 按序插入文字 DOMLazyTree.queueText = queueText; function queueChild(parentTree, childTree) { if (enableLazy) { parentTree.children.push(childTree); } else { parentTree.node.appendChild(childTree.node); } } function queueHTML(tree, html) { if (enableLazy) { tree.html = html; } else { setInnerHTML(tree.node, html); } } function queueText(tree, text) { if (enableLazy) { tree.text = text; } else { // 内部其实将node.textContent = text; setTextContent(tree.node, text); } }
看了这么多是不是感觉到浓浓的基础知识,insertBefore, appendChild, textContent,
createElement,createElementNS,nodeType
_createInitialChildren: function (transaction, props, context, lazyTree) { // Intentional use of != to avoid catching zero/false. var innerHTML = props.dangerouslySetInnerHTML; if (innerHTML != null) { if (innerHTML.__html != null) { DOMLazyTree.queueHTML(lazyTree, innerHTML.__html); } } else { // 这两个是互斥的条件,contentToUse用来判读是不是string,number,如果不是则返回null,childrenToUse生效 var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null; var childrenToUse = contentToUse != null ? null : props.children; // TODO: Validate that text is allowed as a child of this node if (contentToUse != null) { // 省略一些代码... // 上面有说过将内部其实就是插入text的操作, node.concontentText = contentToUse DOMLazyTree.queueText(lazyTree, contentToUse); } else if (childrenToUse != null) { // 对于其他类型继续进行渲染 var mountImages = this.mountChildren(childrenToUse, transaction, context); for (var i = 0; i < mountImages.length; i++) { // 向节点的子节点列表的末尾添加新的子节点 DOMLazyTree.queueChild(lazyTree, mountImages[i]); } } } },
// 这两个是互斥的条件,contentToUse用来判读是不是string,number,如果不是则返回null,childrenToUse生效 var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null; // 如果这个条件不为null var childrenToUse = contentToUse != null ? null : props.children; var mountImages = this.mountChildren(childrenToUse, transaction, context);
mountChildren: function (nestedChildren, transaction, context) { // ... var mountImages = []; var index = 0; for (var name in children) { if (children.hasOwnProperty(name)) { var child = children[name]; // 通过child的类型来实例化不同类型的组件 var mountImage = ReactReconciler.mountComponent(child, transaction, this, this._hostContainerInfo, context, selfDebugID); child._mountIndex = index++; mountImages.push(mountImage); } } return mountImages; },
总结
总的来说组件挂载大概可以概括为以下的步骤:
理解部分源码后那种喜悦的心情总是会随时在你写组件的时候伴随着你,不过react留着的坑还有很多需要我去填补,我也会坚持不懈下去,最后恭喜法国队赢得世界杯冠军~