React的设计经常被讨论,但因为相关文件很多,源代码读起来比较累。正好最近看到Under the hood:ReactJS这个项目分享了React核心代码的流程图(MIT协议),取其中几张写成这篇文章供阅读源码时作为目录索引。
文章分4部分:
源码以v15.6.1为例,不会直接复制大段代码,主要是以超链接链到github源码对应的位置——通常是链到函数被调用的位置,少数情况会链到申明的位置。核心代码当然会涉及VDOM和事件代理,网上相关文章非常多,本文不再赘述,官方文档中已有的内容也会尽量省略。流程图是svg格式,图形填充颜色根据不同文件使用不同颜色。以iframe标签嵌入。这是兼容性最好的方式,但不知道这篇文章会被爬虫带到哪,以及各种RSS阅读器是否支持,所以附上原文地址:dmyz.org/archives/983
ReactDOM.render&ReactMount
v0.14时React拆成了react和react-dom两个包,ReactDOM.render
只是作为接口,实际调用的是ReactMount.render。假设创建了ExampleComponent
这个组件,通常是以JSX的方式传递给render例如<ExampleComponent />
。JSX只是React.createElement的语法糖。
执行的_renderSubtreeIntoContainer
创建了TopLevelWrapper,这是顶层元素,它的子元素是我们创建的ExampleComponent
(代码中的nextElement
)。再通过instantiateReactComponent实例化组件(返回的是ReactCompositeComponentWrapper实例)。上述流程的线框图如下:
到这一步只是实例化了组件,大部分逻辑是通过ReactUpdates.batchedUpdates实现的,跟Transaction有关。
Transaction
和其他几章的主题相比,Transaction偏底层,不像render可以直观感受,但它在核心代码中随处可见。Transaction.js代码注释中详细描述了Transaction做的事,简单来说就是给需要执行的方法用Wrapper封装initialize
和close
方法,initialize
,再调用方法本身,然后调用close
。所以看一个Transaction做了什么,主要是看它的Wrapper。Transaction.js等同于基类,其他Transaction从它继承。上一章提到的ReactUpdates.batchedUpdates
使用了ReactDefaultBatchingStrategyTransaction,它有两种Wrapper:
// https://github.com/facebook/react/blob/v15.6.1/src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js#L19
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
},
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
initialize
是空函数。close
设置ReactDefaultBatchingStrategy.isBatchingUpdates
为false,以及用ReactUpdates.flushBatchedUpdates验证组件。以上流程线框图如下:
再看另一个Transaction:ReactUpdates.batchedUpdates
第一个参数是作为回调函数执行的batchedMountComponentIntoNode
,函数里的transaction
是ReactReconcileTransaction,这个Transaction有三种Wrapper:
// https://github.com/facebook/react/blob/v15.6.1/src/renderers/dom/client/ReactReconcileTransaction.js#L88
var TRANSACTION_WRAPPERS = [
SELECTION_RESTORATION,
EVENT_SUPPRESSION,
ON_DOM_READY_QUEUEING,
];
SELECTION_RESTORATION
保存当前元素中选择的文本(input/textarea/contentEditable),调用结束后再恢复;EVENT_SUPPRESSION
调用ReactBrowserEventEmitter.setEnabled
抑制其他事件;ON_DOM_READY_QUEUEING
用队列保存componentDidUpdate
和componentDidMount
在Transaction中回调 。
ReactReconcileTransaction
执行了mountComponentIntoNode,通过ReactReconciler.mountComponent(实际是performInitialMount
,下一章会提到)生成markup。到这一步就开始Mount了。之前的内容线框图如下:
综上,React中的Transaction跟数据库的Transaction不同,更多是基于效率而不是一致性的考虑,也没有回滚操作。理解某个Transaction主要是看它的Wrapper做了哪些操作。
Mount
第一章已经提到过,TopLevelWrapper
位于顶层,它的child即用户定义的组件由ReactCompositeComponent
处理。
ReactCompositeComponent
的关键方法是mountComponent,获取公共props和context,传给实例,调用Transaction的getUpdateQueue
方法,将返回的ReactUpdateQueue赋值给实例的updater
,赋值代码如下:
// https://github.com/facebook/react/blob/v15.6.1/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L254
inst.props = publicProps;
inst.context = publicContext;
inst.refs = emptyObject;
inst.updater = updateQueue; // 跟setState相关,之后会提到
往后执行到performInitialMount就进入React的生命周期(Lifecycle)了。
ReactReconciler.mountComponent
会根据标签不同做不同处理,如果是audio/video/form/img这类标签,不能在document上做事件代理而要绑定到标签上;如果是input/textarea要对输入内容进行处理,这些操作由ReactReconcileTransaction
批量执行。之后验证props,使用createElement(NS)创建DOM元素,以上操作参看这张流程图:
创建之后由_updateDOMProperties判断是否挂载,三个参数分别是lastProps, nextProps, transaction
,lastProps
和nextProps
不一致时才做处理(当前阶段lastProps
为null)。具体来说是先遍历lastProps
,如果属性存在于nextProps
先不做处理(continue)。如果不存在,检查是否为样式属性(STYLE),是就赋给lastStyle
,之后再通过CSSPropertyOperations.setValueForStyles刷新样式,否则删除DOM属性和已设置的事件。再遍历nextProps
,检查属性是否跟lastProps
相同,之后的逻辑跟处理lastProps
时类似,区别是如果组件绑定了事件,会执行enqueuePueListener处理,上述流程线框图如下:
接着处理子对象,_createInitialChildren
调用了ReactMultiChild.mountChildren,这是个递归的过程,直到子对象是HTML标签为止,返回的markup通过ReactMount._mountImageIntoNode操作,将容器内容替换为生成的HTML,最后发出挂载结束的通知,整个挂载过程就完成了。
this.setState
setState
方法来自ReactComponent,实际执行的是ReactUpdateQueue.enqueueSetState
。将state(partialState
)推入_pendingStateQueue,执行batchingStrategy.batchedUpdates
或是把组件推入dirtyComponents。之后遍历dirtyComponents
,通过ReactUpdatesFlushTransaction
调用ReactUpdates.runBatchedUpdates。函数中的ReactReconciler.performUpdateIfNecessary
会调用实例也就是ReactCompositeComponent
的performUpdateIfNecessary,执行updateComponent
。
组件更新的逻辑,React文档已经介绍得很详细了。setState
或修改props会调用updateComponent。从源码上看,修改props让willReceive = true,满足了调用componentWillReceiveProps
的条件。方法中shouldUpdate
默认为true,根据shouldComponentUpdate
返回的内容决定是否更新组件:
当shouldUpdate为true时,调用_performComponentUpdate,执行_updateRenderedComponent
,方法中的ReactReconciler.receiveComponent
实际调用ReactDOMComponent
的方法,重新指派组件实例,更新组件和子元素。
查找子元素,如果子元素仍然是React组件,就调用updateChildren,直到是字符串或数字。更新完成后,组件的componentDidUpdate
会被调用,至此React生命周期结束。
流程如图:
Afterword
有很多类ReactJS的框架,比如Preact和inferno,侧面证明了ReactJS的设计确实值得借鉴。即使不读源码,按照流程图DIY一个类似的框架也是很有趣的。各种React的相关项目也非常热门,比如这个项目(Under-the-hood:ReactJS)发布没几天就有近2000的star了。本文标题是看到Under the hood临时想到的,虽然好像并不贴切…