作为壹个构建用户界面的库,React的核心始终围绕着更新这壹个重要的目标,将更新和极致的用户体验结合起来是React团队壹直在努力的事情。为什麽React可以将用户体验做到这麽好?我想这是基於以下两点原因:
本文是对React原理解读系列的第1篇文章,後续的文章会定期更新,欢迎持续关注。在正式开始之前,我们先基於以上的这两点展开介绍,以便对壹些概念可以先有个基础认知。
配合的源码调试环境在这里 ,会跟随React主要版本进行更新,欢迎随意下载调试。
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;
}
首先要明白,React要完成壹次更新分为两个阶段: render阶段和commit阶段,两个阶段的工作可分别概括为新fiber树的构建和更新最终效果的应用。
render阶段实际上是在内存中构建壹棵新的fiber树(称为workInProgress树),构建过程是依照现有fiber树(current树)从root开始深度优先遍历再回溯到root的过程,这个过程中每个fiber节点都会经历两个阶段:beginWork和completeWork。组件的状态计算、diff的操作以及render函数的执行,发生在beginWork阶段,effect链表的收集、被跳过的优先级的收集,发生在completeWork阶段。构建workInProgress树的过程中会有壹个workInProgress的指针记录下当前构建到哪个fiber节点,这是React更新任务可恢复的重要原因之壹。
如下面的动图,就是render阶段的简要过程:
在render阶段结束後,会进入commit阶段,该阶段不可中断,主要是去依据workInProgress树中有变化的那些节点(render阶段的completeWork过程收集到的effect链表),去完成DOM操作,将更新应用到页面上,除此之外,还会异步调度useEffect以及同步执行useLayoutEffect。
这两个阶段都是独立的React任务,最後会进入Scheduler被调度。render阶段采取的调度优先级是依据本次更新的优先级来决定的,以便高优先级任务的介入可以打断低优先级任务的工作;commit阶段的调度优先级采用的是最高优先级,以保证commit阶段同步执行不可被打断。
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共三个版本,我曾经对文章中涉及到的三个版本的代码进行过核对,逻辑基本无差别,可放心阅读。
>>: Joining Two More Locales for the 2021 Lottery Millionaire Raffle!
今天要来讲怎麽用 cpp 写一个 Topic, Topic 是一种异步的通讯方式,一般来说每一个节点...
Tableau 优点 Tableau 是一种企业级的商业智能 (BI, Business Intel...
团队如果有一两个人能力仅免强胜任 会拉低团队所有人的表现. IF 你团队有五名优秀的下属 那这两个...
针对 dropdown 的部分,我们要来细节微调他的 style ,让他符合 vogue 上的设计,...
#布局 接续昨天的例子,我们如果新增一个Greeting("Jetpack Compose...