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和ReactComponent

ReactElement是React元素在内存中的表示形式,可以理解为一个数据类,包含type,key,refs,props等成员变量

ReactComponent是React元素的操作类,包含mountComponent(), updateComponent()等很多操作组件的方法,主要有ReactDOMComponent, ReactCompositeComponent, ReactDOMTextComponent, ReactDOMEmptyComponent四个类型

接下来配合一个小例子来大概分析下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');
});

图二:

TopLevelWrapper对象属性

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

图四:

renderedElement

由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留着的坑还有很多需要我去填补,我也会坚持不懈下去,最后恭喜法国队赢得世界杯冠军~

参考:

深入React技术栈
知乎
CSDN
掘金
segfaultment
官网
github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 以下内容是我在学习和研究React时,对React的特性、重点和注意事项的提取、精练和总结,可以做为React特性...
    科研者阅读 8,219评论 2 21
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,810评论 1 18
  • 深入JSX date:20170412笔记原文其实JSX是React.createElement(componen...
    gaoer1938阅读 8,048评论 2 35
  • 1、什么是react React.js 是一个帮助你构建页面 UI 的库。React.js 将帮助我们将界面分成了...
    谷子多阅读 2,554评论 1 13
  • 学习如何在Flow中使用React 将Flow类型添加到React组件后,Flow将静态地确保你按照组件被设计的方...
    vincent_z阅读 6,323评论 4 21