通过 React 源代码, 梳理一下 React 整个渲染流程, 咱们来了解一下实际的 Virtual DOM 实现, 同时也当个个人的 React 备忘录。
之前介绍过 Virtual DOM的原理, 里面将 Virtual DOM 实现流程分为了三步:
- Virtual DOM 的表示方式
- 两颗 VDOM Tree 的差异比较 (diff 阶段)
- 差异应用到实际 DOM 的更新 (patch 阶段)
这样的例子里, diff 操作和 patch 操作是两个阶段, 先 diff 再 patch, 对 Virtual DOM 的原理可以解释的更清楚, 但实际的实现中, 为了达到组件化和 Virtual DOM 的通用性, diff 阶段和 patch 阶段往往是融合在一起的. 这里通过源代码梳理一下 React 实现的流程, 了解实际的VDOM实现, 同时也当个备忘录.
React 渲染代码流程
1 | 开始渲染 (ReactDOM.render) |
React 部分函数/代码解释
ReactReconciler.mountComponent
注册添加一个新的组件, 并返回组件对应的DOM节点的html字符串.
注意, 这个操作只注册了组件 (组件-子组件树, 以及事件函数), 并没有实际添加DOM.
ReactReconciler.unmountComponent
同上, 注销一个组件 (组件树, 对应的事件函数, 以及父组件对其的ref引用). 该操作不会删除对应DOM.
ReactInjection
, ReactDefaultInjection
React 依赖注入, 用于提供特定的逻辑实现
Transaction
React 实现的一套函数调用流程, 简单来说, 通过 Transaction.perform(method, ...args)
, 会先调用 .initialize
, 然后调用指定的方法, 最后调用 .close
. 前后调用的两个hook通过 getTransactionWrappers
提供
ReactUpdates.batchedUpdates
React 批量更新方法, 简单来说, 进入批量处理过程中后, 调用指定函数中造成的额外修改不触发实际更新, 结束之后统一触发实现更新. 实现方法是:
- 初次进入
batchedUpdates
时, 置标记, 通过定义的Transaction
调用指定函数 - 执行指定函数过程中, 再次进入
batchedUpdates
时, 直接调用函数 Transaction
调用结束后, 执行.close
hook, 调用flushBatchedUpdates
, 触发更新开始操作flushBatchedUpdates
会检查 dirtyComponents, 存在则更新; 该值由enqueueUpdate
函数添加
注意: batchedUpdates
会在React绑定的浏览器事件触发时调用, 因而在浏览器事件响应函数中, 多次调用 setState(...)
仅触发一次合并的 render
; 但是自定义回调 (比如 eventemitter) 中, 每次调用 setState(...)
, 都会立即触发一次 render
结论
在 React 实现中:
- diff 操作严格来说只存在于内置DOM组件的更新上 (即 ReactDOMComponent). 对于自定义组件(ReactCompositeComponent), 只有单个内容组件(由 render 函数返回), 其中更深的复杂内容作为 props.children 传递给内容组件处理, 所以在自定义组件上更新操作实际委托给了内容组件, 并没有 diff 操作
- diff 和 patch 阶段没有严格区分, 两者互相融合 (在每个内置DOM组件(ReactDOMComponent)自身更新完毕后, 实际的DOM已经更新, 不会等到整个VDOM Tree对比完成)
- 同上, diff 和 patch 阶段不区分, 整体更新是个简单的递归逻辑, 每个组件依次更新自己的内容组件/子组件, 相对于 diff / patch 分离, 逻辑清晰易懂 (好比递归实现和非递归实现的对比)
- 在内置DOM组件中, 差异的比较是通过key实现的, key可以手动传, 默认与顺序相关, 同时仅对比了第一层子组件, 更深层次靠接下来的递归逻辑完成. 逻辑也很简单, 整个组件树的对比复杂度为 O(M+N) - M 为组件数, N 为DOM节点数 (通常来说就是DOM节点数的线性复杂度, 除非在奇葩代码里, 组件数远大于DOM节点数)
- React/ReactNative 跨平台可行性的具体实现点在于平台特定内置组件的定制. 在浏览器里, 组件分为了两类, 一类是自定义(通用)组件 ReactCompositeComponent, 一类就是浏览器平台特定的内置 DOM 组件 ReactDOMComponent (创建时类型为字符串的对应内置DOM组件). ReactCompositeComponent 不限定平台, 实现的是通用性的组件逻辑. 所以在另一个平台中, 只需定义平台特定的终端显示组件逻辑即可 (也就是重写一个类似的 ReactDOMComponent, 大概1000多行…)