Tech

隐介藏形——ReactJS源码索引

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拆成了reactreact-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封装initializeclose方法,perform时先调用所有的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,函数里的transactionReactReconcileTransaction,这个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用队列保存componentDidUpdatecomponentDidMount在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, transactionlastPropsnextProps不一致时才做处理(当前阶段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会调用实例也就是ReactCompositeComponentperformUpdateIfNecessary,执行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临时想到的,虽然好像并不贴切…

0 0 投票数
文章评分
订阅评论
提醒
guest

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

0 评论
内联反馈
查看所有评论