React-简单组件渲染(render)过程
没有需求就没有任务,没有任务就不想看代码。但是对于分配了代码讲解任务的人来说,终于还是有不得不看代码的“任务”。
C或者Java等“严谨语言圈”对阅读源码有一种谜样的执着,仿佛大神都是从阅读源码一步一步修炼过来的。但是对于“不严谨”的JavaScript语言圈来说,却明显缺少这样的执着。究其原因,各端兼容占了60%的时间,然后滚滚而来的需求和层出不穷的轮子,让JS码农甚至无暇顾及代码的可读性;JS天生的“不确定性”,也让接触的人摸不着头脑,而JS的进步也并不靠本身的规范和增强,而是全靠工具库、轮子和社区贡献。所以读代码的时候,下一个轮子已经碾压过去了。
但是读代码就没有用了吗?至少看一下代码设计的新思路,这门语言的“奇巧淫技”般的写法,或者做一些“魔改”,都是大有好处的。
下面正文开始。
不管是官方例子或各种教程介绍里,对于初次使用React组件,应该都会有个类似下面的例子:
1 2 3 4 5 6 7 8 9 10 |
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表达式。关于这部分如果有疑惑,可以参考 官方说明。
最终编译结果大概是这样:
1 2 3 4 5 6 7 8 9 10 |
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目录,几个基础的例子就够用了。
调用栈里用到的标签
– 函数调用
= 别名
初次简单渲染
这里叫初次简单渲染,是因为直接在根节点渲染只有一级的简单组件,只渲染一次,不包含状态更新和其他生命周期函数操作。
1 2 3 4 5 6 7 8 9 10 |
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-dom/lib/
从编译后的代码可以看出,ReactDOM.render
这个函数就是唯一入口了。
1 2 3 4 5 6 7 8 |
ReactDOM.render =ReactMount.render( nextElement, container, callback ) =return ReactMount._renderSubtreeIntoContainer( null, nextElement, // 上一步生成的ReactElement对象 container, // 'root'节点,HTML-Element callback, ); |
_renderSubtreeIntoContainer
主要负责以下任务:
1.包装nextElement
(仍然使用ReactElement.createElement
)
1 2 3 4 5 6 7 |
{ type: TopLevelWrapper, ... props: { child: reactElement // 上一步生成的ReactElement对象 } } |
2.判断有无父级ReactElement
元素,这里从根节点创建的,所以没有(从react的dom树中取:
1 2 |
ReactDOMComponentTree.getInstanceFromNode(rootEl)) prevComponent = getTopLevelWrapperInContainer(container); |
如果有,需要更新父级元素。
3.判断是否有react
标记,且不包含非根的react
元素,决定是否重用标记shouldReuseMarkup
1 |
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false |
4.向虚拟DOM里渲染一个新的react根组件
1 2 3 4 5 6 7 |
return ReactMount._renderNewRootComponent( nextWrappedElement, // TopLevelWrapper container, // 'root'节点,HTML-Element shouldReuseMarkup, // false nextContext, // 空对象{} ) -var componentInstance = instantiateReactComponent(nextElement, false); // *** 实例化React组件,此为实际被挂载的组件,nextElement为逐层传递的对象。 |
下面仅列出batch
调用栈,暂时忽略BatchingStrategy
运行机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
-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
的运行机制)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
-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
的实例类型:
1 |
instance = new ReactCompositeComponentWrapper(element); // 再包装nextWrappedElement |
1 2 3 |
var ReactCompositeComponentWrapper = function (element) { this.construct(element); } |
注意文件结尾
1 2 3 |
_assign(ReactCompositeComponentWrapper.prototype, ReactCompositeComponent, { _instantiateReactComponent: instantiateReactComponent }); |
所以this.cunstruct
使用的是ReactCompositeComponent
的构建函数,即实例化了一个ReactCompositeComponent
组件。
而根据不同的type
类型,还会实例化为ReactDOMComponent
(HTML标签)和ReactDOMTextComponent
(文本节点)。
在React中,并没有名为“Virtual DOM”的文件或者类。而上面3中实例化的类型,差不多就是react中的虚拟DOM(Virtual DOM)了。因为这几个组件,包含了需要创建浏览器DOM需要的所有信息。
2.react节点包装过程
1)ReactDOM.render
时生成的原始组件
2)_renderSubtreeIntoContainer
包装TopLevelWrapper
3)instantiateReactComponent
时实例化组件
步骤3执行了ReactCompositeComponent
的construct
构建函数
1 2 3 4 5 6 7 8 9 10 11 |
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
参考网址:
云里雾里没看懂……没用到不想学
@zwwooooo 雾里云里,也没看懂,哈哈。
这算冷门和更高境界,就是别人看不懂,哈哈。