破题

面试官如何评定一个人的能力层次??
讲话有重点,层次要分明
切忌:不要只背诵渲染流程中涉及的函数。需要提炼关键内容,转化为别人可以听懂的言语。
本讲问题与第 4 讲相似,突出重点,即通过主线串联整个分散的论点。
渲染过程中的重点是什么??渲染过程中的层次如何划分??

承题

前几讲说涉及到的渲染知识点:React 渲染节点的挂载、React 组件的生命周期、setState 触发渲染更新、diff 策略与 patch 方案。渲染过程中的内容繁杂,有许多事情需要处理,计算机术语称为:事务 。事务具有原子性,不可分割。
事务是通过 调度 的方式协调执行的。
有了全局规划的调度,有了具体的事务,那渲染流程接着就要 阶段划分。以不同阶段的事务与策略为主线,就可做到 讲话有重点;以阶段划分节点,就可做到 层次要分明。
初步答题框架形成:
面试 - 11- 解释React的渲染流程 - 图1
渲染过程中必讲概念 - 协调(调和、Reconciler)

协调

Reconciler 是协助 React 确认状态变化时要更新哪些 DOM 元素的 diff 算法。
React 源码中有一个叫做 reconcilers 模块,它通过抽离公共函数、diff 算法、声明式渲染、自定义组件、state、生命周期方法和 refs 等特性实现跨平台工作。
Reconciler 模块以 v16 为分界线,分为两个版本:

  1. Stack Reconciler 是 React15 前的渲染方案,核心是以 递归方式 逐级调度栈中节点到父节点的渲染。
  2. Fiber Reconciler 是 React16后的渲染方案,核心是 增量渲染,也就是讲渲染工作分割为多个区块,并将其分散到多个帧中去执行。它的设计初衷是提高 React 在动画、画布和手势等场景下的性能。

    渲染

    Stack Reconciler

    挂载

    此处的挂载不是组件的挂载,而是将整个 React 挂载到 ReactDOM.render 上,就如 App 根组件挂载到 root 节点一样。
    JSX 被 Babel 编译成 React.createElement 是在本地 Node 进程中完成的,并不是通过浏览器中的 React 完成。
    ReactDOM.render 调用之后,实际上是 透传参数给 ReactMount.render :

  3. ReactDOM 是对外暴露的接口;

  4. ReactMount 是真正的执行者,完成初始化 React 组件的整个过程。

初始第一步,就是通过 React.createElement 创建 React Element。不同组件类型会被构建为不同 Element:

  • App 组件被标记为 type function ,作为用户自定义组件,被 ReactCompositeComponent 包裹一次,生成一个对象实例;
  • div 标签作为 React 内部已知的 DOM 类型,会实例化为 ReactDOMComponent;
  • 文本 会被直接判断为字符串,实例化为 ReactDOMComponent;

判断不同组件类型,生成不同的 Element,在源码大致如下,其中 isInternalComponentType是判断当前组件是否为内部已知组件:

  1. if (typeof element.type === 'string') {
  2. instance = ReactHostComponent.createInternalComponent(element);
  3. } else if (isInternalComponentType(element.type)) {
  4. instance = new element.type(element);
  5. } else {
  6. instance = new ReactCompositeComponentWrapper();
  7. }

到此,只是完成了 实例化,还需与 React 产生联动,如:改变状态、更新界面等。在 setState 状态改变后,有一个变更收集、批量处理的过程。在这里 ReactUpdates 模块专门用于 批量处理,而批量处理的前后操作,是由 React 通过 建立事务 的概念来处理的。
React 事务是基于 Transaction 类继承拓展。每个 Transaction 实例都是一个封闭空间,保持不可变的任务常量,并提供对应的事务处理接口。
事务优点:原子性、隔离性、一致性;
在事务中会调用ReactCompositeComponent.mountComponent函数进入到 React 生命周期,源码如下:

  1. if (inst.componentWillMount) {
  2. inst.componentWillMount();
  3. if (this._pendingStateQueue) {
  4. inst.state = this._processPendingState(inst.props, inst.context);
  5. }
  6. }

解析如上代码:
先判断是否有componentWillMount,然后初始化state状态。当state计算完成后,调用 App 组件中声明的 render 函数,接着 render 返回的结果,会处理为新的 React Element,再走一步上面提到的流程,不停 往下解析 ,逐步递归,直到 HTML 元素。这就是 App 组件的首次渲染。

更新

当调用setState时发生什么。setState会调用 Component 类中的 enqueueSetState函数。

  1. this.updater.enqueueSetState(this, partialState);

在执行enqueueSetState后,会调用ReactCompositeComponent实例中的_pendingStateQueue,将新的状态变更加入到实例的等待状态队列中,再调用 ReactUpdates模块中的enqueueUpdate函数执行更新。这个过程会检查更新是否已在进行中:

  • 若是,则把组件添加到dirtyComponents中;
  • 如不是,先初始化更新事务,然后把组件加入到dirtyComponents列表;

此处的初始化更新事务,就是setState章节提到的batchingstrategy.isBatchingUpdates的开关,接下来绘制更新事务中处理所有记录的dirtyComponents

卸载

对自定义组件,及ReactCompositeComponent,卸载过程需递归调用生命周期函数。

  1. class CompositeComponent{
  2. unmount(){
  3. var publicInstance = this.publicInstance
  4. if(publicInstance){
  5. if(publicInstance.componentWillUnmount){
  6. publicInstance.componentWillUnmount()
  7. }
  8. }
  9. var renderedComponent = this.renderedComponent
  10. renderedComponent.unmount()
  11. }
  12. }

ReactDOMComponent,卸载子元素需清除事件监听,并清理一些缓存。

  1. class DOMComponent{
  2. unmount(){
  3. var renderedChildren = this.renderedChildren
  4. renderedChildren.forEach(child => child.unmount())
  5. }
  6. }

小结

Stack Reconciler 下,React 渲染的整体策略是 递归 ,并通过 事务 建立 React 与 VDOM 的联系,并完成调度。
整体函数调用流程图

Fiber Reconciler

Fiber(协程)

协程:运行模式 协作式多任务;
线程:抢占式多任务;
两者不同??
Fiber Reconciler 引入两个新概念:Fiber 、effect

  1. Fiber 基于过去的 React Element 进行二次封装,通过 createFiberElement函数创建 Fiber 对象。Fiber 对象不仅包含了 React Element,也包含了指向 父、子、兄节点的引用,为 diff 工作的 双向链表 实现提供基础。
  2. effect 指在协调过程中必须执行计算的活动。

    Fiber Reconciler 协调过程

    Fiber Reconciler 生命周期阶段图,协调过程有两部分:render、commit。具体可查看答题部分。

    小结

    Fiber Reconciler 挂载阶段,ReactDOM 已经不存在了,是直接构建 Fiber 树。而更新流程大致一样,依旧通过isBatchingUpdates控制。
    Fiber Reconciler 最大的不同有两点:

  3. 协作式多任务模式;

  4. 基于循环遍历计算 diff;

Fiber Reconciler 生命周期阶段图

答题

  • 渲染过程大致相同,协调不同。v16 分界线,之前 Stack Reconciler,之后 Fiber Reconciler。协调,特指 React 的 diff 算法,也可指 React 的 reconciler 模块,它包含了 diff 算法和一些公共逻辑。
  • Stack Reconciler ,核心调度方式:递归。调度的基本处理单位是:事务,它的事务基类 Transaction。v16 之前,挂载通过 ReactMount 模块完成,更新通过 ReactUpdate 模块完成,模块之间相互分离,落脚点是事务。
  • Fiber Reconciler ,核心调度方式:循环。调度方式两个特点:一是协作式多任务模式,此模式下可将控制器归还给主线程,通过 requestIdleCallback实现;二是任务优先级策略,调度任务通过标记 Tag 方式划分优先级。1:Fiber Reconciler 基本单位是 Fiber,Fiber 基于过去的 React Element 进行二次封装,包含了指向 父、子、兄节点的引用,为 diff 工作的 双向链表 实现提供基础。
    2:新架构下,整个生命周期被划分为 render 和 commit 阶段:
    render:此阶段的执行特点:可暂停,可恢复,无副作用,主要是通过构建 workInProgress 树来计算 diff。以 current 树为基础,将每个 Fiber 作为一个基本单位,自下而上逐个节点检测并构建 workInProgress 树,此过程是循环完成。
    在执行时通过 requestIdleCallback来调度执行每组任务,每组中的每个计算任务被称为 work,每个 work 完成后,确认是否有高优先级的 work 需插入执行,若有插位执行,若无就继续。每组完成后,将调度器归还给 主线程,直到下次 requestIdleCallback调用,再继续构建 workInProgress 树。
    commit:此阶段处理 effect 列表,列表包含:依 diff 更新 DOM 树、回调生命周期、相应 ref 等。此阶段 同步执行 ,不可中断暂停。所以在componentDidMountcomponentDidUpdatecomponentWillUnmount不要执行重度消耗算力的任务(解决方案:可用 web work 等技术手段)。

面试 - 11- 解释React的渲染流程 - 图2

进阶

问:为啥 Stack Reconciler 模式下 render 函数不支持 return 数组??
答:Stack Reconciler 采用的是 递归遍历 方式,递归时只可返回一个节点元素,肯定不支持数组了。
在 Fiber 中,reconcilerChildrenArray解决此问题。