function instantiateReactComponent(node){
// 文本节点的情况
if(typeof node === 'string' || typeof node === 'number'){
return new ReactDOMTextComponent(node);
// 浏览器默认节点的情况
if(typeof node === 'object' && typeof node.type === 'string'){
//注意这里,使用了一种新的component
return new ReactDOMComponent(node);
// 自定义的元素节点,类型为构造函数
if(typeof node === 'object' && typeof node.type === 'function'){
// 注意这里,使用新的component,专门针对自定义元素
return new ReactCompositeComponent(node);
这时候有的人可能会有所疑问:这些个ReactDOMComponent, ReactCompositeComponentWrapper怎么开发的时候都没有见过?
其实这些都是React的私有类,React自己使用,不会暴露给用户的。它们的常用方法有:mountComponent,updateComponent等。其中mountComponent 用于创建组件,而updateComponent用于用户更新组件。而我们自定义组件的生命周期函数以及render函数都是在这些私有类的方法里被调用的。
既然这些私有类的方法那么重要我们就先来简单了解一下吧~
ReactDOMComponent
首先是ReactMComponent的mountComponent方法,这个方法的作用是:将element转成真实DOM节点,并且插入到相应的container里,然后返回realDOM。
由此可知ReactDOMComponent的mountComponent是element生成真实节点的关键。
下面看个栗子它是怎么做到的吧。
假设有这样一个type类型是原生DOM的element:
type: 'div',
props: {
className: 'cn',
children: 'Hello world',
简单mountComponent的实现:
mountComponent(container) {
const domElement = document.createElement(this._currentElement.type);
const textNode = document.createTextNode(this._currentElement.props.children);
domElement.appendChild(textNode);
container.appendChild(domElement);
return domElement;
其实实现的过程很简单,就是根据type生成domElement,再将子节点append进来返回。当然,真实的mountComponent没有那么简单,感兴趣的可以自己去看源码啦。
讲完ReactDOMComponent,再来看看ReactCompositeComponentWrapper。
ReactCompositeComponentWrapper
这个类的mountComponent方法作用是:实例化自定义组件,最后是通过递归调用到ReactDOMComponent的mountComponent方法来得到真实DOM。
注意:也就是说他自己是不直接生成DOM节点的。
那这个递归是一个怎样的过程呢?我们通过首次渲染来看下。
假设我们有一个Example的组件,它返回<div>hello world</div> 这样一个标签。
首次渲染的过程如下:
首先从React.render开始,由于我们刚刚说,render函数被调用的时候会返回一个element,所以此时返回给我们的element是:
type: function Example,
props: {
children: null
由于这个type是一个自定义组件类,此时要初始化的类是ReactCompositeComponentWrapper,接着调用它的mountComponent方法。这里面会做四件事情,详情可以看上图。其中,第二步的render的得到的element为:
type: 'div',
props: {
children: 'Hello World'
由于这个type是一个原生DOM标签,此时要初始化的类是ReactDOMComponent。接下来它的mountComponent方法就可以帮我们生成对应的DOM节点放在浏览器里啦。
这时候有人可能会有疑问,如果第二步render出来的element 类型也是自定义组件呢?
这时候它就会去调用ReactCompositeComponentWrapper的mountComponent方法,从而形成了一个递归。不管你的自定义组件嵌套多少层,最后总会生成原生dom类型的element,所以最后一定能调用到ReactDOMComponent的mountComponent方法。
感兴趣的可以自己在打断点看下这个递归的过程。
由我打的断点图可以看出在ReactCompositeComponent的mountComponent被调用多次之后,最后调用到了ReactDOMComponent的mountComponent方法。
image
由图可知,在第一步得到instance对象之后,就会去看instance.componentWillMount是否有被定义,有的话调用,而在整个渲染过程结束之后调用componentDidMount。
以上,就是渲染原理的部分,让我们来总结以下:
JSX代码经过babel编译之后变成React.createElement的表达式,这个表达式在render函数被调用的时候执行生成一个element。
在首次渲染的时候,先去按照规则初始化element,接着ReactComponentComponentWrapper通过递归,最终调用ReactDOMComponent的mountComponent方法来帮助生成真实DOM节点。
React 渲染原理(2)--执行setState之后做了什么
由于总结的内容比较多并且是重点,单独整理了一篇文章
【React进阶系列】 setState机制
至此,React 的所有知识点大概梳理了一下。熟练掌握React的核心知识对学习React Fiber很有帮助。对React精通的大佬可以绕过。
接下来通过和上面的知识进行对比的形式来讲解React 对 Fiber的改造。
React 16之前
React 16 之前的不足
当时被大家拍手叫好的 VDOM,为什么今日会略显疲态,这还要从它的工作原理说起。在 react 发布之初,设想未来的 UI 渲染会是异步的,从 setState() 的设计和 react 内部的事务机制可以看出这点。在 react@16 以前的版本,reconciler(现被称为 stack reconciler )采用自顶向下递归,从根组件或 setState() 后的组件开始,更新整个子树。如果组件树不大不会有问题,但是当组件树越来越大,递归遍历的成本就越高,持续占用主线程,这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,造成顿卡的视觉效果。
理论上人眼最高能识别的帧数不超过 30 帧,电影的帧数大多固定在 24,浏览器最优的帧率是 60,即16.5ms 左右渲染一次。 浏览器正常的工作流程应该是这样的,运算 -> 渲染 -> 运算 -> 渲染 -> 运算 -> 渲染 …
之前的问题主要的问题是任务一旦执行,就无法中断,js 线程一直占用主线程,导致卡顿。
可能有些接触前端不久的不是特别理解上面为什么 js 一直占用主线程就会卡顿,我这里还是简单的普及一下。
浏览器每一帧都需要完成哪些工作?
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。
1s 60 帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16ms。
绘制 Paint
在上一小节提到的调和阶段花的时间过长,也就是 js 执行的时间过长,那么就有可能在用户有交互的时候,本来应该是渲染下一帧了,但是在当前一帧里还在执行 JS,就导致用户交互不能麻烦得到反馈,从而产生卡顿感。
把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行。 这种策略叫做 Cooperative Scheduling(合作式调度),操作系统常用任务调度策略之一。
在上面我们已经知道浏览器是一帧一帧执行的,在执行完子任务,下一帧到来之前,主线程通常会有一小段空闲时间,requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务。
从上图也可看出,和 requestAnimationFrame 每一帧必定会执行不同,requestIdleCallback 是捡浏览器空闲来执行任务。
如此一来,假如浏览器一直处于非常忙碌的状态,requestIdleCallback 注册的任务有可能永远不会执行。此时可通过设置 timeout (见下一节 API 介绍)来保证执行。
这个方案看似确实不错,但是怎么实现可能会遇到几个问题:
如何拆分成子任务?
一个子任务多大合适?
怎么判断是否还有剩余时间?
有剩余时间怎么去调度应该执行哪一个任务?
接下里整个 Fiber 架构就是来解决这些问题的。
什么是 Fiber
为了解决上一节提到解决方案遇到的问题,我们首先需要一种方法将任务分解为单元。从某种意义上说,这就是 Fiber,将可中断的任务拆分成多个子任务,通过按照优先级来自由调度子任务,分段更新,从而将之前的同步渲染改为异步渲染。
这是一种’契约‘调度,要求我们的程序和浏览器紧密结合,互相信任。比如可以由浏览器给我们分配执行时间片(通过 requestIdleCallback实现, 下文会介绍),我们要按照约定在这个时间内执行完毕,并将控制权还给浏览器。
Fiber与requestIdleCallback
Fiber所做的就是需要分解渲染任务,然后根据优先级使用API调度,异步执行指定任务:
1.低优先级任务由 requestIdleCallback处理;
2.高优先级任务,如动画相关的由 requestAnimationFrame处理;
requestIdleCallback可以在多个空闲期调用空闲期回调,执行任务;
requestIdleCallback方法提供 deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而导致掉帧;
requestIdleCallback API
var handle = window.requestIdleCallback(callback[, options])
function myNonEssentialWork (deadline) {
// 当回调函数是由于超时才得以执行的话,deadline.didTimeout为true
// deadline.timeRemaining() 获取每一帧还剩余的时间
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
console.log('done', tasks, deadline.timeRemaining())
tasks.length = tasks.length - 1
if (tasks.length > 0) {
requestIdleCallback(myNonEssentialWork);
超时的情况,其实就是浏览器很忙,没有空闲时间,此时会等待指定的 timeout 那么久再执行,通过入参 dealine 拿到的 didTmieout 会为 true,同时 timeRemaining () 返回的也是 0。超时的情况下如果选择继续执行的话,肯定会出现卡顿的,因为必然会将一帧的时间拉长。
React 的Fiber架构
新的的调和器(Fiber-Recocilation)
为了和之前的Task-Reconciler做区分,我们把Fiber的 Reconciler叫做Fiber-Reconciler。 是 React 里的调和器,这也是任务调度完成之后,如何去执行每个任务,如何去更新每一个节点的过程
🔴Stack-Recocilation:JSX中创建(或更新)一些元素,react会根据这些元素创建(或更新)Virtual DOM,然后react根据更新前后virtual DOM的区别,去修改真正的DOM。注意,在stack reconciler下,DOM的更新是同步的,通过递归的方式进行渲染,发现一个或几个instance有更新,会立即执行DOM更新操作。
image.png
🔴Fiber-Recocilation:React 16版本提出了一个更先进的调和器,它允许渲染进程分段完成,而不必须一次性完成,中间可以返回至主进程控制执行其他任务。而这是通过计算部分组件树的变更,并暂停渲染更新,询问主进程是否有更高需求的绘制或者更新任务需要执行,这些高需求的任务完成后才开始渲染。这一切的实现是在代码层引入了一个新的数据结构-Fiber对象,每一个组件实例对应有一个fiber实例,此fiber实例负责管理组件实例的更新,渲染任务及与其他fiber实例的联系。通过stateNode属性管理Instance自身的特性。通过child和sibling表征当前工作单元的下一个工作单元,return表示处理完成后返回结果所要合并的目标,通常指向父节点。整个结构是一个链表树,结构如下:
Fiber-Reconciler的阶段划分
为了和之前的task Reconciler做区分,我们把Fiber的 Reconciler叫做Fiber Reconciler。 是 React 里的调和器,这也是任务调度完成之后,如何去执行每个任务,如何去更新每一个节点的过程
如果你现在使用最新的 React 版本(v16), 使用 Chrome 的 Performance 工具,可以很清晰地看到reconciler 过程分为2个阶段(phase):Reconciliation(协调阶段) 和 Commit(提交阶段).
⚛️ 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为'副作用(Effect)' . 以下生命周期钩子会在协调阶段被调用:
constructor
componentWillMount 废弃
componentWillReceiveProps 废弃
static getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate 废弃
render
⚛️ 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断. 这些生命周期钩子在提交阶段被执行:
getSnapshotBeforeUpdate() 严格来说,这个是在进入 commit 阶段前调用
componentDidMount
componentDidUpdate
componentWillUnmount
也就是说,在协调阶段如果时间片用完,React就会选择让出控制权。因为协调阶段执行的工作不会导致任何用户可见的变更,所以在这个阶段让出控制权不会有什么问题。
需要注意的是:因为协调阶段可能被中断、恢复,甚至重做,⚠️React 协调阶段的生命周期钩子可能会被调用多次!, 例如 componentWillMount 可能会被调用两次。
因此建议 协调阶段的生命周期钩子不要包含副作用. 索性 React 就废弃了这部分可能包含副作用的生命周期方法,例如componentWillMount、componentWillUpdate. v17后我们就不能再用它们了, 所以现有的应用应该尽快迁移.
现在你应该知道为什么'提交阶段'必须同步执行,不能中断的吧? 因为我们要正确地处理各种副作用,包括DOM变更、还有你在componentDidMount中发起的异步请求、useEffect 中定义的副作用... 因为有副作用,所以必须保证按照次序只调用一次,况且会有用户可以察觉到的变更, 不容差池。
数据结构的演进
Stack-Recocilation运行时存在3种实例:
DOM //真实DOM节点
-------
Instances //instance是组件的实例,但是注意function形式的component没有实例
-------
Elements //Elements其实就基本就可以解释为Virtual DOM,是利用js对象的形式来描述一个DOM节点(type,props,...children)
// Elements是的数据结构简化如下:
let vNode = {
type: 'div',
key: '1',
props: {
className: 'title'
children: [
{type: 'p', key: '2',text: 'hello'}
在首次渲染过程中构建出vDOM tree,后续需要更新时(setState())tree diff,compontent diff和 element diff根据vDOM tree的数据结构(层级,类型,key)对比前后生成的vDOM tree得到DOM change,并把DOM change应用(patch)到DOM树。
Fiber数据结构
Fiber把渲染/更新过程(递归diff)拆分成一系列小任务,每次检查树上的一小部分,做完看是否还有时间继续下一个任务,有的话继续,没有的话把自己挂起,主线程不忙的时候再继续。增量更新需要更多的上下文信息,之前的vDOM tree显然难以满足,所以扩展出了fiber tree(即Fiber上下文的vDOM tree),更新过程就是根据输入数据以及现有的fiber tree构造出新的fiber tree(workInProgress tree)。因此,Instance层新增了这些实例:
DOM //
真实DOM节点
-------
effect
每个workInProgress tree节点上都有一个effect list
用来存放diff结果
当前节点更新完毕会向上merge effect list(queue收集diff结果)
- - - -
workInProgress
workInProgress tree是reconcile过程中从fiber tree建立的当前进度快照,用于断点恢复
- - - -
fiber
fiber tree与vDOM tree类似,用来描述增量更新所需的上下文信息
-------
Elements
描述UI长什么样子(type, props,...children)
截止目前,我们对Fiber应该有了初步的了解,简单介绍一下Fiber Node的数据结构,数据结构能一定程度反映其整体工作架构。
其实,一个fiber就是一个JavaScript对象,以键值对形式存储了一个关联组件的信息,包括组件接收的props,维护的state,最后需要渲染出的内容等。fiber 节点相当于以前的虚拟 dom 节点,结构如下:
interface Fiber {
* ⚛️ 节点的类型信息
tag: number, // Fiber 类型,以数字表示,可选择的如下
- IndeterminateComponent
- FunctionalComponent
- ClassComponent // Menu, Table
- HostRoot // ReactDOM.render 的第二个参数
- HostPortal
- HostComponent // div, span
- HostText // 纯文本节点,即 dom 的 nodeName 等于 '#text'
- CallComponent // 对应 call return 中的 call
- CallHandlerPhase // call 中的 handler 阶段
- ReturnComponent // 对应 call return 中的 return
- Fragment
- Mode // AsyncMode || StrictMode
- ContextConsumer
- ContextProvider
- ForwardRef
type: any, // 节点元素类型, /与 react element 里的 type 一致
key: null | string, // fiber 的唯一标识
stateNode: any, // 对应组件或者 dom 的实例
* ⚛️ 结构信息
// 单链表树结构
return: Fiber | null,// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
child: Fiber | null,// 指向自己的第一个子节点
sibling: Fiber | null, // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
* ⚛️ 更新相关
pendingProps: any, // 新的、待处理的props
updateQueue: UpdateQueue<any> | null, // 该Fiber对应的组件产生的Update会存放在这个队列里面
memoizedProps: any, // 上一次渲染完成之后的props
memoizedState: any, // 上一次渲染的时候的state
* ⚛️ Effect 相关的
// 和节点关系一样,React 同样使用链表来将所有有副作用的Fiber连接起来
effectTag: SideEffectTag<number>, //当前节点的副作用类型,例如节点更新、删除、移动
nextEffect: Fiber | null, // 单链表用来快速查找下一个side effect
firstEffect: Fiber | null, // 子树中第一个side effect
lastEffect: Fiber | null, // 子树中最后一个side effect
* ⚛️ 替身
// 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
// 我们称他为`current <==> workInProgress`
// 在渲染完成之后他们会交换位置
alternate: Fiber | null, // WIP 树里面的 fiber,如果不在更新期间,那么就等于当前的 fiber,如果是新创建的节点,那么就没有
Fiber 包含的属性可以划分为 5 个部分:
🆕 节点类型信息 - 这个也容易理解,tag表示节点的分类、type 保存具体的类型值,如div、MyComp
🆕 结构信息 - 这个上文我们已经见过了,Fiber 使用链表的形式来表示节点在树中的定位,每一个 Fiber Node 节点与 Virtual Dom 一一对应
🆕 更新相关 - 节点的组件实例、props、state等,它们将影响组件的输出
🆕 Effect 相关的 - 这个也是新东西. 在 Reconciliation 过程中发现的'副作用'(变更需求)就保存在节点的effectTag 中(想象为打上一个标记).
那么怎么将本次渲染的所有节点副作用都收集起来呢? 这里也使用了链表结构,在遍历过程中React会将所有有‘副作用’的节点都通过nextEffect连接起来
🆕 替身 - Fiber在update的时候,会从原来的Fiber(我们称为current)clone出一个新的Fiber(我们称为alternate)。两个Fiber diff出的变化(side effect)记录在alternate上。所以一个组件在更新时最多会有两个Fiber与其对应,在更新结束后alternate会取代之前的current的成为新的current节点。
Fiber类型
上一小节,Fiber对象中有个tag属性,标记fiber类型,而fiber实例是和组件对应的,所以其类型基本上对应于组件类型,源码见ReactTypeOfWork模块:
export const IndeterminateComponent = 0; // 尚不知是类组件还是函数式组件
export const FunctionalComponent = 1; // 函数式组件
export const ClassComponent = 2; // Class类组件
export const HostRoot = 3; // 组件树根组件,可以嵌套
export const HostPortal = 4; // 子树. Could be an entry point to a different renderer.
export const HostComponent = 5; // 标准组件,如地div, span等
export const HostText = 6; // 文本
export const CallComponent = 7; // 组件调用
export const CallHandlerPhase = 8; // 调用组件方法
export const ReturnComponent = 9; // placeholder(占位符)
export const Fragment = 10; // 片段
在调度执行任务的时候会根据不同类型fiber,即fiber.tag值进行不同处理。
任务如何分片及分片的优先级
任务分片,或者叫工作单元(work unit),是怎么拆分的呢。因为在Reconciliation阶段任务分片可以被打断,用来执行优先级高的任务。如何拆分一个任务就很重要了。
为了达到任务分片的效果,就需要有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
module.exports = {
synchronous,//0,synchronous首屏(首次渲染)用,要求尽量快,不管会不会阻塞UI线程
task,//1,在next tick之前执行
animation,//2,animation通过requestAnimationFrame来调度,这样在下一帧就能立即开始动画过程
high,//3,在不久的将来立即执行
low,//4,稍微延迟执行也没关系
offscreen,//5,下一次render时或scroll时才执行
也就是说,(不考虑突发事件的)正常调度是由工作循环来完成的,基本规则是:每个工作单元结束检查是否还有时间做下一个,没时间了就先“挂起”
优先级机制用来处理突发事件与优化次序,例如:
到commit阶段了,提高优先级
高优任务做一半出错了,给降一下优先级
抽空关注一下低优任务,别给饿死了
如果对应DOM节点此刻不可见,给降到最低优先级
这些策略用来动态调整任务调度,是工作循环的辅助机制,最先做最重要的事情
任务调度的过程是:
在任务队列中选出高优先级的fiber node执行,调用requestIdleCallback获取所剩时间,若执行时间超过了deathLine,或者突然插入更高优先级的任务,则执行中断,保存当前结果,修改fiber node 的tag标记,设置为pending状态,迅速收尾并再调用一个requestIdleCallback,等主线程释放出来再继续
恢复任务执行时,检查tag是被中断的任务,会接着继续做任务或者重做
Fiber Tree
React 在 render 第一次渲染时,会通过 React.createElement 创建一颗 Element 树,可以称之为 Virtual DOM Tree,由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node,将 Fiber Node 链接起来的结构成为 Fiber Tree。它反映了用于渲染 UI 的应用程序的状态。这棵树通常被称为 current 树(当前树,记录当前页面的状态)。
Fiber Tree 一个重要的特点是链表结构,将递归遍历编程循环遍历,然后配合 requestIdleCallback API, 实现任务拆分、中断与恢复。
这个链接的结构是怎么构成的呢,这就要主要到之前 Fiber Node 的节点的这几个字段:
// 单链表树结构
return: Fiber | null, // 指向父节点
child: Fiber | null,// 指向自己的第一个子节点
sibling: Fiber | null,// 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
每一个 Fiber Node 节点与 Virtual Dom 一一对应,所有 Fiber Node 连接起来形成 Fiber tree, 是个单链表树结构,因为使用了链表结构,即使处理流程被中断了,我们随时可以从上次未处理完的Fiber继续遍历下去。如下图所示:
比如你在text(hello)中断了,那么下一次就会从 p 节点开始处理
这个数据结构调整还有一个好处,就是某些节点异常时,我们可以打印出完整的’节点栈‘,只需要沿着节点的return回溯即可。
Side Effect(副作用)
我们可以将 React 中的一个组件视为一个使用 state 和 props 来计算 UI 表示的函数。其他所有活动,如改变 DOM 或调用生命周期方法,都应该被视为副作用,或者简单地说是一种效果。文档中 是这样描述的:
您之前可能已经在 React 组件中执行数据提取,订阅或手动更改 DOM。我们将这些操作称为“副作用”(或简称为“效果”),因为它们会影响其他组件,并且在渲染过程中无法完成。
您可以看到大多 state 和 props 更新都会导致副作用。既然使用副作用是工作(活动)的一种类型,Fiber 节点是一种方便的机制来跟踪除了更新以外的效果。每个 Fiber 节点都可以具有与之相关的副作用,它们可在 effectTag 字段中编码。
因此,Fiber 中的副作用基本上定义了处理更新后需要为实例完成的 工作。对于宿主组件(DOM 元素),所谓的工作包括添加,更新或删除元素。对于类组件,React可能需要更新 refs 并调用 componentDidMount 和 componentDidUpdate 生命周期方法。对于其他类型的 Fiber ,还有相对应的其他副作用。
Effects List
React 处理更新的素对非常迅速,为了达到这种水平的性能,它采用了一些有趣的技术。其中之一是构建具有副作用的 Fiber 节点的线性列表,从而能够快速遍历。遍历线性列表比树快得多,并且没有必要在没有副作用的节点上花费时间。
此列表的目标是标记具有 DOM 更新或其他相关副作用的节点。此列表是 finishedWork 树的子集,并使用 nextEffect 属性而不是 current 和 workInProgress 树中使用的 child 属性进行链接。
Dan Abramov 为副作用列表提供了一个类比。他喜欢将它想象成一棵圣诞树,「圣诞灯」将所有有效节点捆绑在一起。为了使这个可视化,让我们想象如下的 Fiber 节点树,其中标亮的节点有一些要做的工作。例如,我们的更新导致 c2 被插入到 DOM 中,d2 和 c1 被用于更改属性,而 b2 被用于触发生命周期方法。副作用列表会将它们链接在一起,以便 React 稍后可以跳过其他节点:
export class Home extend React.component<HomeProps, any> {
componentWillReceiveProps(nextProps: HomeProps) {}
componentDidMount() {}
componentDidUpdate() {}
componentWillUnmount() {}
.....
render() {
return (
<List/>
ReactDom.render(<Home />, document.querySelector(selectors: '#hostRoot'))
当前页面包含一个列表,通过该列表渲染出一个button和一组Item,Item中包含一个div,其中的内容为数字。通过点击button,可以使列表中的所有数字进行平方。另外有一个按钮,点击可以调节字体大小。
image.png
scheduler会根据当前主线程的使用情况去处理这次update。为了实现这种特性,使用了requestIdelCallbackAPI。对于不支持这个API的浏览器,react会加上pollyfill。
总的来讲,通常,客户端线程执行任务时会以帧的形式划分,大部分设备控制在30-60帧是不会影响用户体验;在两个执行帧之间,主线程通常会有一小段空闲时间,requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务
image.png
接下来进入处理List的work loop,List中包含更新,因此此时react会调用setState时传入的updater funciton获取最新的state值,此时应该是[1,4,9]。通常我们现在在调用setState传入的是一个对象,但在使用fiber conciler时,必须传入一个函数,函数的返回值是要更新的state。react从很早的版本就开始支持这种写法了,不过通常没有人用。在之后的react版本中,可能会废弃直接传入对象的写法。
setState({}, callback); // stack conciler
setState(() => { return {} }, callback); // fiber conciler
在获取到最新的state值后,react会更新List的state和props值,然后调用render,然后得到一组通过更新后的list值生成的elements。react会根据生成elements的类型,来决定fiber是否可重用。对于当前情况来说,新生成的elments类型并没有变(依然是Button和Item),所以react会直接从fiber-tree中复制这些elements对应的fiber到workInProgress 中。并给List打上标签,因为这是一个需要更新的节点。
image.png
接下来处理第一个item。通过shouldComponentUpdate钩子可以根据传入的props判断其是否需要改变。对于第一个Item而言,更改前后都是1,所以不会改变,shouldComponentUpdate返回false,复制div,处理完成,检查时间,如果还有时间进入第二个Item。
第二个Item shouldComponentUpdate返回true,所以需要打上tag,标志需要更新,复制div,调用render,讲div中的内容从2更新为4,因为div有更新,所以标记div。当前节点处理完成。
image.png
此时,要做的是还是检查时间够不够用,如果没有时间,会等到时间再去提交修改到DOM。进入到阶段2后,reacDOM会根据阶段1计算出来的effect-list来更新DOM。
更新完DOM之后,workInProgress就完全和DOM保持一致了,为了让当前的fiber-tree和DOM保持一直,react交换了current和workinProgress两个指针。
image.png
事实上,react大部分时间都在维持两个树(Double-buffering)。这可以缩减下次更新时,分配内存、垃圾清理的时间。commit完成后,执行componentDidMount函数。
下面是一个详细的执行过程图:
⚛️ 1.第一部分从 用户操作引起setState被调用以后,把接收的 React Element 转换为 Fiber 节点,并为其设置优先级,创建 Update,根据Fiber的优先级加入到Update相应的位置,这部分主要是做一些初始数据的准备。
⚛️ 2.第二部分主要是三个函数:scheduleWork、requestWork、performWork,即调度工作、申请工作、正式工作三部曲,React 16 新增的异步调度的功能则在这部分实现,这部分就是 Schedule 阶段,完成调度主要靠scheduleCallbackWithExpriation这个方法。scheduleCallbackWithExpriation这个方法在不同环境,实现不一样,chrome等览器中使用requestIdleCallback API,没有这个API的浏览器中,通过requestAnimationFrame模拟一个requestIdleCallback,任务调度的过程是:在任务队列中选出高优先级的fiber node执行,调用requestIdleCallback获取所剩时间,若执行时间超过了deathLine,或者突然插入更高优先级的任务,则执行中断,保存当前结果,修改tag标记一下,设置为pending状态,迅速收尾并再调用一个requestIdleCallback,等主线程释放出来再继续。执行到performWorkOnRoot时,第二部分结束。
⚛️ 3.第三部分基本就是 Fiber Reconciler ,分为2个阶段:第一阶段Render/recocilation Phase,遍历所有的 Fiber 节点,通过 Diff 算法计算所有更新工作,产出 EffectList 给到 commit Phase使用,这部分的核心是 beginWork 函数;然后进入Commit Phase,这个阶段不能被打断,不再赘述。下一节将着重讲这两个阶段。
Reconciliation Phase(协调阶段)
Reconciliation Phase阶段以fiber tree为蓝本,把每个fiber作为一个工作单元,自顶向下逐节点构造workInProgress tree(构建中的新fiber tree),具体过程如下(以组件节点为例):
⚛️1.找到高优先级的待处理的节点
⚛️ 2.如果当前节点不需要更新,直接把子节点clone过来,跳到5;要更新的话打个tag
⚛️ 3.打个tag标记,更新当前节点状态(组件更新props,context等,DOM节点记下DOM change)
⚛️ 4.组件节点的话,调用shouldComponentUpdate(),false的话,跳到5
⚛️ 5.调用render()获得新的子节点,生成子节点的workInProgress节点(创建过程有alternate的用alternate,没有的复用子节点,子节点增删也发生在这里)
⚛️ 6.如果没有产生child fiber,该工作单元结束,把effect list归并到父节点,并把当前节点的sibling作为下一个工作单元;否则把当前节点的child fiber作为下一个工作单元
⚛️ 7.如果没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做
⚛️ 8.如果没有下一个工作单元了(回到了workInProgress tree的根节点),第1阶段结束,进入pendingCommit状态
实际上1-7是Reconciliation阶段的工作循环,下一节讲重点讲。7是Reconciliation阶段的出口,工作循环每次只做一件事,做完看要不要喘口气。工作循环结束时,workInProgress tree的根节点身上的effect list就是收集到的所有side effect(因为每做完一个都向上归并)
alternate、current Tree及 workInProgress Tree的关系
在第一次渲染之后,React 最终得到一个 Fiber 树,它反映了用于渲染 UI 的应用程序的状态。这棵树通常被称为 current 树(当前树)。当 React 开始处理更新时,它会构建一个所谓的workInProgress tree(工作进度树)workInProgress tree是reconcile过程中从fiber tree建立的当前进度快照,用于断点恢复。
Fiber在update的时候,会从原来的Fiber(我们称为current)clone出一个新的Fiber(我们称为alternate)。两个Fiber diff出的变化(side effect)记录在alternate上。所以一个组件在更新时最多会有两个Fiber与其对应,在更新结束后alternate会取代之前的current的成为新的current节点。
所有工作都在 workInProgress 树的 Fiber 节点上执行。当 React 遍历 current 树时,对于每个现有 Fiber 节点,React 会创建一个构成 workInProgress 树的备用节点,这一节点会使用 render 方法返回的 React 元素中的数据来创建。处理完更新并完成所有相关工作后,React 将准备好一个备用树以刷新到屏幕。一旦这个 workInProgress 树在屏幕上呈现,它就会变成 current 树。
React 的核心原则之一是一致性。 React 总是一次性更新 DOM - 它不会显示部分中间结果。workInProgress 树充当用户不可见的「草稿」,这样 React 可以先处理所有组件,然后将其更改刷新到屏幕。
在源代码中,您将看到很多函数从 current 和 workInProgress 树中获取 Fiber 节点。这是一个这类函数的签名:
function updateHostComponent(current, workInProgress, renderExpirationTime) {...}
每个Fiber节点持有备用域在另一个树的对应部分的引用。来自 current 树中的节点会指向 workInProgress 树中的节点,反之亦然。
工作循环的主要步骤
举个例子:
React.Component.prototype.setState = function( partialState, callback ) {
updateQueue.pus( {
stateNode: this,
partialState: partialState
requestIdleCallback(performWork); // 这里就开始干活了
function performWork(deadline) {
workLoop(deadline)
if (nextUnitOfWork || updateQueue.length > 0) {
requestIdleCallback(performWork) //继续干
setState先把此次更新放到更新队列 updateQueue 里面,然后调用调度器开始做更新任务。performWork 先调用 workLoop 对 fiber 树进行遍历比较,就是我们上面提到的遍历过程。当此次时间片时间不够遍历完整个 fiber 树,或者遍历并比较完之后workLoop 函数结束。接下来我们判断下 fiber 树是否遍历完或者更新队列 updateQueue 是否还有待更新的任务。如果有则调用 requestIdleCallback 在下个时间片继续干活。nextUnitOfWork 是个全局变量,记录 workLoop 遍历 fiber 树中断在哪个节点。
所有的 Fiber 节点都会在 工作循环 中进行处理。如下是该循环的同步部分的实现:
function workLoop(deadline) {
if (!nextUnitOfWork) {
//一个周期内只创建一次
nextUnitOfWork = createWorkInProgress(updateQueue)
while (nextUnitOfWork && deadline.timeRemaining() > EXPIRATION_TIME) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
if (pendingCommit) {
//当全局 pendingCommit 变量被负值
commitAllwork(pendingCommit)
刚开始遍历的时候判断全局变量 nextUnitOfWork 是否存在?如果存在表示上次任务中断了,我们继续,如果不存在我们就从更新队列里面取第一个任务,并生成对应的 fiber 根节点。接下来我们就是正式的工作了,用循环从某个节点开始遍历 fiber 树。performUnitOfWork 根据我们上面提到的遍历规则,在对当前节点处理完之后,返回下一个需要遍历的节点。循环除了要判断是否有下一个节点(是否遍历完),还要判断当前给你的时间是否用完,如果用完了则需要返回,让浏览器响应用户的交互事件,然后再在下个时间片继续。workLoop 最后一步判断全局变量 pendingCommit 是否存在,如果存在则把这次遍历 fiber 树产生的所有更新一次更新到真实的 dom 上去。注意 pendingCommit 在完成一次完整的遍历过程之前是不会有值的。
遍历树、初始化或完成工作主要用到 4 个函数:
performUnitOfWork
beginWork
completeUnitOfWork
completeWork
1.workLoop阶段
构建workInProgress tree的过程就是diff的过程,对 Fiber tree前后进行比对主要是beginWork,源码如下:
function beginWork(fiber: Fiber): Fiber | undefined {
if (fiber.tag === WorkTag.HostComponent) {
// 宿主节点diff
diffHostComponent(fiber)
} else if (fiber.tag === WorkTag.ClassComponent) {
// 类组件节点diff
diffClassComponent(fiber)
} else if (fiber.tag === WorkTag.FunctionComponent) {
// 函数组件节点diff
diffFunctionalComponent(fiber)
} else {
// ... 其他类型节点,省略
宿主节点比对:
function diffHostComponent(fiber: Fiber) {
// 新增节点
if (fiber.stateNode == null) {
fiber.stateNode = createHostComponent(fiber)
} else {
updateHostComponent(fiber)
const newChildren = fiber.pendingProps.children;
// 比对子节点
diffChildren(fiber, newChildren);
类组件节点比对也差不多:
function diffClassComponent(fiber: Fiber) {
// 创建组件实例
if (fiber.stateNode == null) {
fiber.stateNode = createInstance(fiber);
if (fiber.hasMounted) {
// 调用更新前生命周期钩子
applybeforeUpdateHooks(fiber)
} else {
// 调用挂载前生命周期钩子
applybeforeMountHooks(fiber)
// 渲染新节点
const newChildren = fiber.stateNode.render();
// 比对子节点
diffChildren(fiber, newChildren);
fiber.memoizedState = fiber.stateNode.state
子节点比对:
function diffChildren(fiber: Fiber, newChildren: React.ReactNode) {
let oldFiber = fiber.alternate ? fiber.alternate.child : null;
// 全新节点,直接挂载
if (oldFiber == null) {
mountChildFibers(fiber, newChildren)
return
let index = 0;
let newFiber = null;
// 新子节点
const elements = extraElements(newChildren)
// 比对子元素
while (index < elements.length || oldFiber != null) {
const prevFiber = newFiber;
const element = elements[index]
const sameType = isSameType(element, oldFiber)
if (sameType) {
newFiber = cloneFiber(oldFiber, element)
// 更新关系
newFiber.alternate = oldFiber
// 打上Tag
newFiber.effectTag = UPDATE
newFiber.return = fiber
// 新节点
if (element && !sameType) {
newFiber = createFiber(element)
newFiber.effectTag = PLACEMENT
newFiber.return = fiber
// 删除旧节点
if (oldFiber && !sameType) {
oldFiber.effectTag = DELETION;
oldFiber.nextEffect = fiber.nextEffect
fiber.nextEffect = oldFiber
if (oldFiber) {
oldFiber = oldFiber.sibling;
if (index == 0) {
fiber.child = newFiber;
} else if (prevFiber && element) {
prevFiber.sibling = newFiber;
index++
function createWorkInProgress(updateQueue) {
const updateTask = updateQueue.shift()
if (!updateTask) return
if (updateTask.partialState) {
// 证明这是一个setState操作
updateTask.stateNode._internalfiber.partialState = updateTask.partialState
const rootFiber =
updateTask.fromTag === tag.HostRoot
? updateTask.stateNode._rootContainerFiber
: getRoot(updateTask.stateNode._internalfiber)
return {
tag: tag.HostRoot,
stateNode: updateTask.stateNode,
props: updateTask.props || rootFiber.props,
alternate: rootFiber // 用于链接新旧的 VDOM
function getRoot(fiber) {
let _fiber = fiber
while (_fiber.return) {
_fiber = _fiber.return
return _fiber
createWorkInProgress 拿出更新队列 updateQueue 第一个任务,然后看触发这个任务的节点是什么类型。如果不是根节点,则通过循环迭代节点的 return 找到最上层的根节点。最后生成一个新的 fiber 节点,这个节点就是当前 fiber 节点的 alternate 指向的,也就是说下面会在当前节点和这个新生成的节点直接进行 diff。
function performUnitOfWork(workInProgress) {
const nextChild = beginWork(workInProgress)
if (nextChild) return nextChild
// 没有 nextChild, 我们看看这个节点有没有 sibling
let current = workInProgress
while (current) {
//收集当前节点的effect,然后向上传递
completeWork(current)
if (current.sibling) return current.sibling
//没有 sibling,回到这个节点的父亲,看看有没有sibling
current = current.return
函数 performUnitOfWork从 workInProgress 树接收一个 Fiber 节点,并通过调用 beginWork 函数启动工作。这个函数将启动所有 Fiber 执行工作所需要的活动。出于演示的目的,我们只 log 出 Fiber 节点的名称来表示工作已经完成。函数 beginWork 始终返回指向要在循环中处理的下一个子节点的指针或 null。
如果有下一个子节点,它将被赋值给 workLoop 函数中的变量 nextUnitOfWork。但是,如果没有子节点,React 知道它到达了分支的末尾,因此它可以完成当前节点。一旦节点完成,它将需要为同层的其他节点执行工作,并在完成后回溯到父节点。这是 completeUnitOfWork 函数执行的代码:
function completeUnitOfWork(workInProgress) {
while (true) {
let returnFiber = workInProgress.return;
let siblingFiber = workInProgress.sibling;
nextUnitOfWork = completeWork(workInProgress);
if (siblingFiber !== null) {
// If there is a sibling, return it
// to perform work for this sibling
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber,
// continue the loop to complete the parent.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
return null;
你可以看到函数的核心就是一个大的 while 的循环。当 workInProgress 节点没有子节点时,React 会进入此函数。完成当前 Fiber 节点的工作后,它就会检查是否有同层节点。如果找的到,React 退出该函数并返回指向该同层节点的指针。它将被赋值给 nextUnitOfWork 变量,React将从这个节点开始执行分支的工作。我们需要着重理解的是,在当前节点上,React 只完成了前面的同层节点的工作。它尚未完成父节点的工作。只有在完成以子节点开始的所有分支后,才能完成父节点和回溯的工作。
从实现中可以看出,performUnitOfWork 和 completeUnitOfWork 主要用于迭代目的,而主要活动则在beginWork 和 completeWork 函数中进行。
function completeWork(fiber) {
const parent = fiber.return
// 到达顶端
if (parent == null || fiber === topWork) {
pendingCommit = fiber
return
if (fiber.effectTag != null) {
if (parent.nextEffect) {
parent.nextEffect.nextEffect = fiber
} else {
parent.nextEffect = fiber
} else if (fiber.nextEffect) {
parent.nextEffect = fiber.nextEffect
2、 completeWork阶段
completeWork的工作主要是通过新老节点的prop或tag等,收集节点的effect-list。然后向上一层一层循环,merge每个节点的effect-list,当到达根节点#hostRoot时,节点上包含所有的effect-list。并把effect-list传给pendingcommit,进入commit阶段。
1203274-20180831173358256-1834919628.jpg
当回溯完,有了 pendingCommit,则 commitAllwork 会被调用。它做的工作就是循环遍历根节点的 effets 数据,里面保存着所有要更新的内容。commitWork 就是执行具体更新的函数,这里就不展开了(因为这篇主要想讲的是 fiber 更新的调度算法)。
所以你们看遍历 dom 数 diff 的过程是可以被打断并且在后续的时间片上接着干,只是最后一步 commitAllwork 是同步的不能打断的。这样 react 使用新的调度算法优化了更新过程中执行时间过长导致的页面卡顿现象。
接下来就是将所有打了 Effect 标记的节点串联起来,这个可以在completeWork中做, 例如:
completeWork
function completeWork(fiber) {
const parent = fiber.return
// 到达顶端
if (parent == null || fiber === topWork) {
pendingCommit = fiber
return
if (fiber.effectTag != null) {
if (parent.nextEffect) {
parent.nextEffect.nextEffect = fiber
} else {
parent.nextEffect = fiber
} else if (fiber.nextEffect) {
parent.nextEffect = fiber.nextEffect
commitAllWork
function commitAllWork(fiber) {
let next = fiber
while(next) {
if (fiber.effectTag) {
// 提交,偷一下懒,这里就不展开了
commitWork(fiber)
next = fiber.nextEffect
// 清理现场
pendingCommit = nextUnitOfWork = topWork = null
commit (提交阶段)
commit阶段可以理解为就是将 Diff 的结果反映到真实 DOM 的过程。这一阶段从函数 completeRoot 开始。在这个阶段,React 更新 DOM 并调用变更生命周期之前及之后方法的地方。
当 React 进入这个阶段时,它有 2 棵树和副作用列表。第一个树表示当前在屏幕上渲染的状态,然后在 render 阶段会构建一个备用树。它在源代码中称为 finishedWork 或 workInProgress,表示需要映射到屏幕上的状态。此备用树会用类似的方法通过 child 和 sibling 指针链接到 current 树。
然后,有一个副作用列表 -- 它是 finishedWork 树的节点子集,通过 nextEffect 指针进行链接。需要记住的是,副作用列表是运行 render 阶段的结果。渲染的重点就是确定需要插入、更新或删除的节点,以及哪些组件需要调用其生命周期方法。这就是副作用列表告诉我们的内容,它页正是在 commit 阶段迭代的节点集合。
出于调试目的,可以通过 Fiber 根的属性 current访问 current 树。可以通过 current 树中 HostFiber 节点的 alternate 属性访问 finishedWork 树。
在 commit 阶段运行的主要函数是 commitRoot 。它执行如下下操作:
在标记为 Snapshot 副作用的节点上调用 getSnapshotBeforeUpdate 生命周期
在标记为 Deletion 副作用的节点上调用 componentWillUnmount 生命周期
执行所有 DOM 插入、更新、删除操作
将 finishedWork 树设置为 current
在标记为 Placement 副作用的节点上调用 componentDidMount 生命周期
在标记为 Update 副作用的节点上调用 componentDidUpdate 生命周期
在调用变更前方法 getSnapshotBeforeUpdate 之后,React 会在树中提交所有副作用,这会通过两波操作来完成。第一波执行所有 DOM(宿主)插入、更新、删除和 ref 卸载。然后 React 将 finishedWork 树赋值给 FiberRoot,将 workInProgress 树标记为 current 树。这是在提交阶段的第一波之后、第二波之前完成的,因此在 componentWillUnmount 中前一个树仍然是 current,在 componentDidMount/Update 期间已完成工作是 current。在第二波,React 调用所有其他生命周期方法和引用回调。这些方法单独传递执行,从而保证整个树中的所有放置、更新和删除能够被触发执行。
以下是运行上述步骤的函数的要点:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
这些子函数中都实现了一个循环,该循环遍历副作用列表并检查副作用的类型。当它找到与函数目的相关的副作用时,就会执行。
在 commit 阶段,在 commitRoot 里会根据 effect的 effectTag,具体 effectTag 见源码 ,进行对应的插入、更新、删除操作,根据 tag 不同,调用不同的更新方法。并把这些更新提交到当前节点的父亲。当遍历完这颗树的时候,再通过 return 回溯到根节点。这个过程中把所有的更新全部带到根节点,再一次更新到真实的 dom 中去。如下图所示:
从根节点开始:
⚛️ 1. div1 通过 child 到 div2。
⚛️ 2. div2 和自己的 alternate 比较完把更新 commit1 通过 return 提交到 div1。
⚛️ 3. div2 通过 sibling 到 ul1。
⚛️ 4. ul1 和自己的 alternate 比较完把更新 commit2 通过 return 提交到 div1。
⚛️ 5. ul1 通过 child 到 li1。
⚛️ 6. li1 和自己的 alternate 比较完把更新 commit3 通过 return 提交到 ul1。
⚛️ 7. li1 通过 sibling 到 li2。
⚛️ 8. li2 和自己的 alternate 比较完把更新 commit4 通过 return 提交到 ul1。
⚛️ 9. 遍历完整棵树开始回溯,li2 通过 return 回到 ul1。
⚛️ 10. 把 commit3 和 commit4 通过 return 提交到 div1。
⚛️ 11. ul1 通过 return 回到 div1。
⚛️ 12. 获取到所有更新 commit1-4,一次更新到真是的 dom 中去。
双缓冲原理
当 render 的时候有了这么一条单链表,当调用 setState 的时候又是如何 Diff 得到 change 的呢?
采用的是一种叫双缓冲技术(double buffering),这个时候就需要另外一颗树:WorkInProgress Tree,它反映了要刷新到屏幕的未来状态。
WorkInProgress Tree 构造完毕,得到的就是新的 Fiber Tree,然后喜新厌旧(把 current 指针指向WorkInProgress Tree,丢掉旧的 Fiber Tree)就好了。
这样做的好处:
能够复用内部对象(fiber),比如某颗子树不需要变动,React会克隆复用旧树中的子树。
节省内存分配、GC的时间开销,
就算运行中有错误,也不会影响 View 上的数据,比如当一个节点抛出异常,仍然可以继续沿用旧树的节点,避免整棵树挂掉
每个 Fiber上都有个alternate属性,也指向一个 Fiber,创建 WorkInProgress 节点时优先取alternate,没有的话就创建一个。
创建 WorkInProgress Tree 的过程也是一个 Diff 的过程,Diff 完成之后会生成一个 Effect List,这个 Effect List 就是最终 Commit 阶段用来处理副作用的阶段。
Dan 在 Beyond React 16 演讲中用了一个非常恰当的比喻,那就是Git 功能分支,你可以将 WIP 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即使是操作失误也不会影响旧的分支。当你这个分支经过了测试和完善,就可以合并到旧分支,将其替换掉. 这或许就是’提交(commit)阶段‘的提交一词的来源吧?:
本开始想一篇文章把 Fiber 讲透的,但是写着写着发现确实太多了,想写详细,估计要写几万字,所以我这篇文章的目的仅仅是在没有涉及到源码的情况下梳理了大致 React 的工作流程,对于细节,比如如何调度异步任务、如何去做 Diff 等等细节将以小节的方式一个个的结合源码进行分析。
说实话,自己不是特别满意这篇,感觉头重脚轻,在之后的学习中会逐渐完善这篇文章。,这篇文章拖太久了,请继续后续的文章。
站在巨人肩上
⚛️React 拾遗:React.createElement 与 JSX
⚛️ React的React.createElement源码解析(一)
⚛️ React 组件Component,元素Element和实例Instance的区别
⚛️ React Fiber 那些事: 深入解析新的协调算法
⚛️ React-从源码分析React Fiber工作原理
⚛️浅谈 React Fiber
⚛️ React Fiber架构
⚛️ 完全理解React Fiber
⚛️ 这可能是最通俗的 React Fiber(时间分片) 打开方式
⚛️ Deep In React 之浅谈 React Fiber 架构(一)
⚛️ React Fiber初探
⚛️React Diff 算法
⚛️ React16性能改善的原理(二)
⚛️ 浅谈React16框架 - Fiber