破题
面试官如何评定一个人的能力层次??
讲话有重点,层次要分明
切忌:不要只背诵渲染流程中涉及的函数。需要提炼关键内容,转化为别人可以听懂的言语。
本讲问题与第 4 讲相似,突出重点,即通过主线串联整个分散的论点。
渲染过程中的重点是什么??渲染过程中的层次如何划分??
承题
前几讲说涉及到的渲染知识点:React 渲染节点的挂载、React 组件的生命周期、setState 触发渲染更新、diff 策略与 patch 方案。渲染过程中的内容繁杂,有许多事情需要处理,计算机术语称为:事务 。事务具有原子性,不可分割。
事务是通过 调度 的方式协调执行的。
有了全局规划的调度,有了具体的事务,那渲染流程接着就要 阶段划分。以不同阶段的事务与策略为主线,就可做到 讲话有重点;以阶段划分节点,就可做到 层次要分明。
初步答题框架形成:
渲染过程中必讲概念 - 协调(调和、Reconciler)
协调
Reconciler 是协助 React 确认状态变化时要更新哪些 DOM 元素的 diff 算法。
React 源码中有一个叫做 reconcilers 模块,它通过抽离公共函数、diff 算法、声明式渲染、自定义组件、state、生命周期方法和 refs 等特性实现跨平台工作。
Reconciler 模块以 v16 为分界线,分为两个版本:
- Stack Reconciler 是 React15 前的渲染方案,核心是以 递归方式 逐级调度栈中节点到父节点的渲染。
Fiber Reconciler 是 React16后的渲染方案,核心是 增量渲染,也就是讲渲染工作分割为多个区块,并将其分散到多个帧中去执行。它的设计初衷是提高 React 在动画、画布和手势等场景下的性能。
渲染
Stack Reconciler
挂载
此处的挂载不是组件的挂载,而是将整个 React 挂载到 ReactDOM.render 上,就如 App 根组件挂载到 root 节点一样。
JSX 被 Babel 编译成 React.createElement 是在本地 Node 进程中完成的,并不是通过浏览器中的 React 完成。
ReactDOM.render 调用之后,实际上是 透传参数给 ReactMount.render :ReactDOM 是对外暴露的接口;
- ReactMount 是真正的执行者,完成初始化 React 组件的整个过程。
初始第一步,就是通过 React.createElement 创建 React Element。不同组件类型会被构建为不同 Element:
- App 组件被标记为 type function ,作为用户自定义组件,被 ReactCompositeComponent 包裹一次,生成一个对象实例;
- div 标签作为 React 内部已知的 DOM 类型,会实例化为 ReactDOMComponent;
- 文本 会被直接判断为字符串,实例化为 ReactDOMComponent;
判断不同组件类型,生成不同的 Element,在源码大致如下,其中 isInternalComponentType
是判断当前组件是否为内部已知组件:
if (typeof element.type === 'string') {
instance = ReactHostComponent.createInternalComponent(element);
} else if (isInternalComponentType(element.type)) {
instance = new element.type(element);
} else {
instance = new ReactCompositeComponentWrapper();
}
到此,只是完成了 实例化,还需与 React 产生联动,如:改变状态、更新界面等。在 setState 状态改变后,有一个变更收集、批量处理的过程。在这里 ReactUpdates 模块专门用于 批量处理,而批量处理的前后操作,是由 React 通过 建立事务 的概念来处理的。
React 事务是基于 Transaction 类继承拓展。每个 Transaction 实例都是一个封闭空间,保持不可变的任务常量,并提供对应的事务处理接口。
事务优点:原子性、隔离性、一致性;
在事务中会调用ReactCompositeComponent.mountComponent
函数进入到 React 生命周期,源码如下:
if (inst.componentWillMount) {
inst.componentWillMount();
if (this._pendingStateQueue) {
inst.state = this._processPendingState(inst.props, inst.context);
}
}
解析如上代码:
先判断是否有componentWillMount
,然后初始化state
状态。当state
计算完成后,调用 App 组件中声明的 render 函数,接着 render 返回的结果,会处理为新的 React Element,再走一步上面提到的流程,不停 往下解析 ,逐步递归,直到 HTML 元素。这就是 App 组件的首次渲染。
更新
当调用setState
时发生什么。setState
会调用 Component 类中的 enqueueSetState
函数。
this.updater.enqueueSetState(this, partialState);
在执行enqueueSetState
后,会调用ReactCompositeComponent
实例中的_pendingStateQueue
,将新的状态变更加入到实例的等待状态队列中,再调用 ReactUpdates
模块中的enqueueUpdate
函数执行更新。这个过程会检查更新是否已在进行中:
- 若是,则把组件添加到
dirtyComponents
中; - 如不是,先初始化更新事务,然后把组件加入到
dirtyComponents
列表;
此处的初始化更新事务,就是setState
章节提到的batchingstrategy.isBatchingUpdates
的开关,接下来绘制更新事务中处理所有记录的dirtyComponents
。
卸载
对自定义组件,及ReactCompositeComponent
,卸载过程需递归调用生命周期函数。
class CompositeComponent{
unmount(){
var publicInstance = this.publicInstance
if(publicInstance){
if(publicInstance.componentWillUnmount){
publicInstance.componentWillUnmount()
}
}
var renderedComponent = this.renderedComponent
renderedComponent.unmount()
}
}
对ReactDOMComponent
,卸载子元素需清除事件监听,并清理一些缓存。
class DOMComponent{
unmount(){
var renderedChildren = this.renderedChildren
renderedChildren.forEach(child => child.unmount())
}
}
小结
Stack Reconciler 下,React 渲染的整体策略是 递归 ,并通过 事务 建立 React 与 VDOM 的联系,并完成调度。
整体函数调用流程图
Fiber Reconciler
Fiber(协程)
协程:运行模式 协作式多任务;
线程:抢占式多任务;
两者不同??
Fiber Reconciler 引入两个新概念:Fiber 、effect
- Fiber 基于过去的 React Element 进行二次封装,通过
createFiberElement
函数创建 Fiber 对象。Fiber 对象不仅包含了 React Element,也包含了指向 父、子、兄节点的引用,为 diff 工作的 双向链表 实现提供基础。 -
Fiber Reconciler 协调过程
Fiber Reconciler 生命周期阶段图,协调过程有两部分:render、commit。具体可查看答题部分。
小结
Fiber Reconciler 挂载阶段,ReactDOM 已经不存在了,是直接构建 Fiber 树。而更新流程大致一样,依旧通过
isBatchingUpdates
控制。
Fiber Reconciler 最大的不同有两点: 协作式多任务模式;
- 基于循环遍历计算 diff;
答题
- 渲染过程大致相同,协调不同。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 等。此阶段 同步执行 ,不可中断暂停。所以在componentDidMount
、componentDidUpdate
、componentWillUnmount
不要执行重度消耗算力的任务(解决方案:可用 web work 等技术手段)。
进阶
问:为啥 Stack Reconciler 模式下 render 函数不支持 return 数组??
答:Stack Reconciler 采用的是 递归遍历 方式,递归时只可返回一个节点元素,肯定不支持数组了。
在 Fiber 中,reconcilerChildrenArray
解决此问题。