React-简单组件渲染(render)过程

2018年12月1日 ... ☕️ 6 min read

没有需求就没有任务,没有任务就不想看代码。但是对于分配了代码讲解任务的人来说,终于还是有不得不看代码的“任务”。

C或者Java等“严谨语言圈”对阅读源码有一种谜样的执着,仿佛大神都是从阅读源码一步一步修炼过来的。但是对于“不严谨”的JavaScript语言圈来说,却明显缺少这样的执着。究其原因,各端兼容占了60%的时间,然后滚滚而来的需求和层出不穷的轮子,让JS码农甚至无暇顾及代码的可读性;JS天生的“不确定性”,也让接触的人摸不着头脑,而JS的进步也并不靠本身的规范和增强,而是全靠工具库、轮子和社区贡献。所以读代码的时候,下一个轮子已经碾压过去了。

但是读代码就没有用了吗?至少看一下代码设计的新思路,这门语言的“奇巧淫技”般的写法,或者做一些“魔改”,都是大有好处的。

下面正文开始。

不管是官方例子或各种教程介绍里,对于初次使用React组件,应该都会有个类似下面的例子:

class Hello extends React.Component {
  render() {
    return <div>Hello {this.props.name}</div>;
  }
}
 
ReactDOM.render(
  <Hello name="World" />,
  document.getElementById('root')
);

上面代码使用了JSX语法,由JSX编写的代码是无法在浏览器中运行的,需要用Babel来将它编译成正常的JavaScript表达式。关于这部分如果有疑惑,可以参考 官方说明。 最终编译结果大概是这样:

class Hello extends React.Component {
  render() {
    return React.createElement('div', null, `Hello ${this.props.name}`);
  }
}
 
ReactDOM.render(
  React.createElement(Hello, {name: 'World'}, null),
  document.getElementById('root')
);

以调试方式查看代码

源代码是react-15-stable版本,这是一个比较老的版本,使用的是Stack引擎,新版本(react-16+)使用的是Fiber,但是整体结构已经比较完善。有时候,从比较初期的版本往后看,能对代码整体脉络和结构由更准确地把握。

代码查看,推荐使用调试工具。起初啥都没用,就直接挨个文件找function,然后对应几篇代码讲解文章,看的云里雾里。后来突然看到一篇文章以调试顺序讲解代码,打开WebStorm的调试,笑出了猪声。

WebStorm,配合chrome插件。这个组合可以很方便地在WebStorm里单步调试,比chrome自带的调试工具功能丰富,查看代码的运行情况也更方便。

关于如何使用WebStorm在chrome里调试JS,请看 这里。

调试目标文件,可以自己写个简单的例子,调用编译之后的react.js和react-dom.js文件(位于build目录)。或者直接打开example目录,几个基础的例子就够用了。

调用栈里用到的标签 – 函数调用 = 别名

初次简单渲染 这里叫初次简单渲染,是因为直接在根节点渲染只有一级的简单组件,只渲染一次,不包含状态更新和其他生命周期函数操作。

React.createElement( type, [props], [...children] )
=ReactElement.createElement( type, config, children )
-return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props )
{
	type: function(props, context, updater) {...} // reactClassComponent: {displayName:'Hello',...},
	key: null,
	ref: null,
	props: {name: 'World'},
	_owner: ReactCurrentOwner.current
}

整体流程

react-render-overall

下面所有函数所在文件均位于react-dom/lib/

从编译后的代码可以看出,ReactDOM.render这个函数就是唯一入口了。

ReactDOM.render
=ReactMount.render( nextElement, container, callback )
=return ReactMount._renderSubtreeIntoContainer(
      null,
      nextElement, // 上一步生成的ReactElement对象
      container, // 'root'节点,HTML-Element
      callback,
    );

_renderSubtreeIntoContainer主要负责以下任务:

1.包装nextElement(仍然使用ReactElement.createElement)

{
      type: TopLevelWrapper,
      ...
      props: {
            child: reactElement // 上一步生成的ReactElement对象	
      }
}

2.判断有无父级ReactElement元素,这里从根节点创建的,所以没有(从react的dom树中取:

ReactDOMComponentTree.getInstanceFromNode(rootEl))
prevComponent = getTopLevelWrapperInContainer(container);

如果有,需要更新父级元素。

3.判断是否有react标记,且不包含非根的react元素,决定是否重用标记shouldReuseMarkup

var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false

4.向虚拟DOM里渲染一个新的react根组件

return ReactMount._renderNewRootComponent(
      nextWrappedElement, // TopLevelWrapper
      container, // 'root'节点,HTML-Element
      shouldReuseMarkup, // false
      nextContext, // 空对象{}
    )
-var componentInstance = instantiateReactComponent(nextElement, false); // *** 实例化React组件,此为实际被挂载的组件,nextElement为逐层传递的对象。

下面仅列出batch调用栈,暂时忽略BatchingStrategy运行机制

-ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,  // 实际执行的函数
      componentInstance, // ReactCompositeComponent
      container,
      shouldReuseMarkup,
      context,
    );
=BatchingStrategy.batchedUpdates(
      batchedMountComponentIntoNode,
      componentInstance, // ReactCompositeComponent
      container,
      shouldReuseMarkup,
      context,
    );
-batchedMountComponentIntoNode(
componentInstance, // ReactCompositeComponent
container,
shouldReuseMarkup,
context,
    );

5.最终的渲染会在transaction中执行(暂时忽略transaction的运行机制)

-mountComponentIntoNode
=var markup=ReactReconciler.mountComponent( // Initializes the component, renders markup,
         // and registers  event listeners.
wrapperInstance, // componentInstance
transaction,
ReactDOMContainerInfo, // 节点容器信息,同名函数生成(ReactDOMContainerInfo.js),包含
// _topLevelWrapper, _idCount, _ownerDocument, _node, _tag, _namespaceURI
context,
...)
|=internalInstance.mountComponent // *** internalInstance即上一步的wrapperInstance,函数实现ReactCompositeComponent.mountComponent(ReactCompositeComponent.js):用于初始化组件,添加属性,渲染标签,注册事件监听,react生命周期函数也在这里执行。
 
|= *** ReactMount._mountImageIntoNode(
markup, // 上一步生成的markup
container, // 'root'节点
wrapperInstance, // componentInstance
shouldReuseMarkup,
transaction
    )

用document.createElement等函数,把markup插入页面。

小记

1.步骤4中如何确定实例化的对象类型,比如Hello自定义标签,div等HTML标签,或者字符串?

instantiateReactComponent函数会根据type==TopLevelWrapper,确定instance的实例类型:

instance = new ReactCompositeComponentWrapper(element); // 再包装nextWrappedElement

var ReactCompositeComponentWrapper = function (element) {
this.construct(element);
}

注意文件结尾

_assign(ReactCompositeComponentWrapper.prototype, ReactCompositeComponent, {
_instantiateReactComponent: instantiateReactComponent
});

所以this.cunstruct使用的是ReactCompositeComponent的构建函数,即实例化了一个ReactCompositeComponent组件。

而根据不同的type类型,还会实例化为ReactDOMComponent(HTML标签)和ReactDOMTextComponent(文本节点)。

jsx-to-vdom

在React中,并没有名为“Virtual DOM”的文件或者类。而上面3中实例化的类型,差不多就是react中的虚拟DOM(Virtual DOM)了。因为这几个组件,包含了需要创建浏览器DOM需要的所有信息。

2.react节点包装过程

1)ReactDOM.render时生成的原始组件

react-render1

2)_renderSubtreeIntoContainer包装TopLevelWrapper

react-render2

3)instantiateReactComponent时实例化组件

react-render3

步骤3执行了ReactCompositeComponent的construct构建函数

function construct(element) { // 添加和初始化各种变量,以后在mountComponent中赋值
	...
	this._currentElement = element; // _renderSubtreeIntoContainer包装过的wrappedElement
	this._rootNodeID = 0;
	...
	this._mountOrder = 0;
	...
	// ComponentWillUnmount shall only be called once
	this._calledComponentWillUnmount = false;
	...
}

接下来… ReactCompositeComponent.mountComponent,setState更新过程,batchedUpdates,Transaction

参考网址:

深入理解React源码
Under-the-hood-ReactJS

#react

SideEffect is a blog for front-end web development.
Code by Axiu / rss