作为一个构建用户界面的库,React的核心始终围绕着更新这一个重要的目标,将更新和极致的用户体验结合起来是React团队一直在努力的事情。为什么React可以将用户体验做到这么好?我想这是基于以下两点原因:
- Fiber架构和Scheduler出色的调度模式可以实现异步可中断的更新行为。
- 优先级机制贯穿更新的整个周期
本文是对React原理解读系列的第一篇文章,在正式开始之前,我们先基于这两点展开介绍,以便对一些概念可以先有个基础认知。
配合的源码调试环境在这里 ,会跟随React主要版本进行更新,欢迎随意下载调试。
Fiber是什么
Fiber是什么?它是React的最小工作单元,在React的世界中,一切都可以是组件。在普通的HTML页面上,人为地将多个DOM元素整合在一起可以组成一个组件,HTML标签可以是组件(HostComponent),普通的文本节点也可以是组件(HostText)。每一个组件就对应着一个fiber节点,许多个fiber节点互相嵌套、关联,就组成了fiber树,正如下面表示的Fiber树和DOM的关系一样:
Fiber树 DOM树
div#root div#root
| |
<App/> div
| / \
div p a
/ ↖
/ ↖
p ----> <Child/>
|
a
一个DOM节点一定对应着一个Fiber节点,但一个Fiber节点却不一定有对应的DOM节点。
fiber 作为工作单元它的结构如下:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Fiber元素的静态属性相关
this.tag = tag;
this.key = key; // fiber的key
this.elementType = null;
this.type = null; // fiber对应的DOM元素的标签类型,div、p...
this.stateNode = null; // fiber的实例,类组件场景下,是组件的类,HostComponent场景,是dom元素
// Fiber 链表相关
this.return = null; // 指向父级fiber
this.child = null; // 指向子fiber
this.sibling = null; // 同级兄弟fiber
this.index = 0;
this.ref = null; // ref相关
// Fiber更新相关
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null; // 存储update的链表
this.memoizedState = null; // 类组件存储fiber的状态,函数组件存储hooks链表
this.dependencies = null;
this.mode = mode;
// Effects
// flags原为effectTag,表示当前这个fiber节点变化的类型:增、删、改
this.flags = NoFlags;
this.nextEffect = null;
// effect链相关,也就是那些需要更新的fiber节点
this.firstEffect = null;
this.lastEffect = null;
this.lanes = NoLanes; // 该fiber中的优先级,它可以判断当前节点是否需要更新
this.childLanes = NoLanes;// 子树中的优先级,它可以判断当前节点的子树是否需要更新
/*
* 可以看成是workInProgress(或current)树中的和它一样的节点,
* 可以通过这个字段是否为null判断当前这个fiber处在更新还是创建过程
* */
this.alternate = null;
}
fiber架构下的React是如何更新的
首先要明白,React要完成一次更新分为两个阶段: render阶段和commit阶段,两个阶段的工作可分别概括为新fiber树的构建和更新最终效果的应用。
render阶段
render阶段实际上是在内存中构建一棵新的fiber树(称为workInProgress树),构建过程是依照现有fiber树(current树)从root开始深度优先遍历再回溯到root的过程,这个过程中每个fiber节点都会经历两个阶段:beginWork和completeWork。组件的状态计算、diff的操作以及render函数的执行,发生在beginWork阶段,effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段。构建workInProgress树的过程中会有一个workInProgress的指针记录下当前构建到哪个fiber节点,这是React更新任务可恢复的重要原因之一。
如下面的动图,就是render阶段的简要过程:
commit阶段
在render阶段结束后,会进入commit阶段,该阶段不可中断,主要是去依据workInProgress树中有变化的那些节点(render阶段的completeWork过程收集到的effect链表),去完成DOM操作,将更新应用到页面上,除此之外,还会异步调度useEffect以及同步执行useLayoutEffect。
这两个阶段都是独立的React任务,最后会进入Scheduler被调度。render阶段采取的调度优先级是依据本次更新的优先级来决定的,以便高优先级任务的介入可以打断低优先级任务的工作;commit阶段的调度优先级采用的是最高优先级,以保证commit阶段同步执行不可被打断。
Scheduler 的作用
Scheduler用来调度执行上面提到的React任务。
何为调度?依据任务优先级来决定哪个任务先被执行。调度的目标是保证高优先级任务最先被执行。
何为执行?Scheduler执行任务具备一个特点:即根据时间片去终止任务,并判断任务是否完成,若未完成则继续调用任务函数。它只是去做任务的中断和恢复,而任务是否已经完成则要依赖React告诉它。Scheduler和React相互配合的模式可以让React的任务执行具备异步可中断的特点。
优先级机制
为了区分任务的轻重缓急,React内部有一个从事件到调度的优先级机制。事件本身自带优先级属性,它导致的更新会基于事件的优先级计算出更新自己的优先级,更新会产生更新任务,更新任务的优先级由更新优先级计算而来,更新任务被调度,所以需要调度优先级去协调调度过程,调度优先级由更新任务优先级计算得出,就这样一步一步,React将优先级的概念贯穿整个更新的生命周期。
React优先级相关的更多介绍请移步 React中的优先级。
双缓冲机制
双缓冲机制是React管理更新工作的一种手段,也是提升用户体验的重要机制。
当React开始更新工作之后,会有两个fiber树,一个current树,是当前显示在页面上内容对应的fiber树。另一个是workInProgress树,它是依据current树深度优先遍历构建出来的新的fiber树,所有的更新最终都会体现在workInProgress树上。当更新未完成的时候,页面上始终展示current树对应的内容,当更新结束时(commit阶段的最后),页面内容对应的fiber树会由current树切换到workInProgress树,此时workInProgress树即成为新的current树。
function commitRootImpl(root, renderPriorityLevel) {
...
// finishedWork即为workInProgress树的根节点,
// root.current指向它来完成树的切换
root.current = finishedWork;
...
}
两棵树在进入commit阶段时候的关系如下图,最终commit阶段完成时,两棵树会进行切换。
在未更新完成时依旧展示旧内容,保持交互,当更新完成立即切换到新内容,这样可以做到新内容和旧内容无缝切换。
总结
本文基本概括了React大致的工作流程以及角色,本系列文章会以更新过程为主线,从render阶段开始,一直到commit阶段,讲解React工作的原理。除此之外,会对其他的重点内容进行大篇幅分析,如事件机制、Scheduler原理、重点Hooks以及context原理。
本系列文章耗时较长,落笔撰写时,17版本还未发布,所以参照的源码版本为16.13.1、17.0.0-alpha.0以及17共三个版本,我曾经对文章中涉及到的三个版本的代码进行过核对,逻辑基本无差别,可放心阅读。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。