- 作者:老汪软件技巧
- 发表时间:2024-12-15 15:06
- 浏览量:
v18版本之前 Legacy 模式下的 React 应用中,更新本质上有两种场景:
ReactDOM.render
假设现在开始初始化我们的应用,一个传统的 React 应用从 ReactDOM.render 方法开始:
import ReactDOM from 'react-dom'
/* 通过 ReactDOM.render */
ReactDOM.render(
<App />,
document.getElementById('app')
)
ReactDOM.render主要做的事情是形成一个 Fiber Tree 挂载到 app 上,包括:
由此引出几个关键概念和疑问:Fiber,调度( Scheduler )和调和( Reconciler )。
Fiber
什么是fiber?
fiber 是在 React 中的最小粒度的执行单元,也可以理解成就是 React 虚拟Dom 的节点。
为什么要用fiber?
但有个问题可能大家都清楚,那就是 GUI 渲染线程和 JS 引擎线程是相互排斥的,比如开发者用 js 写了一个遍历大量数据的循环,在执行 js 时候,会阻塞浏览器的渲染绘制,给用户直观的感受就是卡顿,在Reactv15以及之前的版本,React 对于虚拟 DOM 是采用递归方式遍历更新的,比如一次更新,就会从应用根部递归更新,递归一旦开始,中途无法中断,代码大了会给前端交互上的体验造成卡顿的问题越来越明显,所以推出了这套基于Fiber节点的架构,用对应的调度( Scheduler )和调和( Reconciler )机制来解决这个问题。
它如何解决卡顿?
总体来说就是,每一个 fiber 都可以作为一个执行单元来处理,每一个 fiber 可以根据自身的过期时间expirationTime( v17 版本叫做优先级lane)来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染,做一些动画,重排( reflow ),重绘 repaints 之类的事情,然后等浏览器空余时间,在通过scheduler(调度器),再次恢复执行单元上来,这样就能本质上中断了渲染,提高了用户体验。
Element,Fiber,Dom三个概念什么关系?
Fiber之间如何建立关联?
每一个 element 都会对应一个 fiber ,每一个 fiber 是通过 return , child ,sibling 三个属性建立起联系的,并用独特的tag标记对应记录Element的类型,同时保存了alternate,effect,memoizedProps,childLanes,expirationTime等调度和调和需要的信息。
到现在,我们至少需要有个确定的概念就是,在 React 内部,不止我们的 JSX 形成的 Element 树,还有一个与之对应的Fiber树,同时一一对应的真实 Dom 树,我们写下的 JSX 代码,在 React 内部得经过这三层模型处理成我们最后可以交互的页面。接下来就要从初始化和一次更新入手,看一下 fiber 是如何具体工作的。
Fiber 更新机制初始化
第一步:从render方法创建fiberRoot和rootFiber
一个 React 应用可以有多个 rootFiber ,但是只能有一个作为 fiberRoot(应用根节点)。
第二步:workInProgress和current
一旦两个root对象关联完成,进入调和阶段,之前我们了解到Fiber节点之间通过几个属性链接成树,现在更进一步,需要了解到 React 的 Fiber 使用的是双缓冲树这个数据结构去构建的,就意味着存在着在JSX Element和真实Dom之间其实不止一个而是有两颗 Fiber 树,分别是:
开始第一次调和,会进入 beginwork 流程,首先会复用当前 current 树( 第一次只有rootFiber一个节点 )的alternate作为 workInProgress 树的根(如果是第一次没有就创建一个Fiber作为workInProgress,并双向赋值alternate属性)
第三步:深度调和子节点,渲染视图
接下来会按照上述第二步,在新创建的 alternates 上,依次完成整个 fiber 树的遍历创建,类似于这样:
最后会把此时的 workInProgress 作为最新的current渲染树,把fiberRoot 的 current 指针指向 workInProgress 使其变为 current Fiber 树。到此完成 React 应用第一次初始化流程。
那之后如果,点击一次按钮,我们的 React 发生更新,会如何更新Fiber呢? 交替复写workInProgress!第一次初始化把新构建的workInProgress(上面图的右边)最后作为了当前的current,那左边这个RootFiber不是空出来了吗?!是的,第一次的更新将在左边发生!而后续的第三次,第四次,等等更新将交替在左右树上更新,然后不断切换fiberRoot的current属性指向就可以了,每个Fiber节点之间都使用alternate连接,以此来承载虚拟Dom的Diff对比:
调和流程
接下来我们来具体看看这个树是如何遍历完成创建和更新的,涉及到 render 和 commit 两个fiber Reconciler 调和的核心阶段
Render阶段
function workLoop (){
while (workInProgress !== null ) {
workInProgress = performUnitOfWork(workInProgress);
}
}
在调和过程中,每一个发生更新的 fiber 都会作为一次 workInProgress 执行 performUnitOfWork方法去遍历。如果渲染没有被中断,那么 workLoop 会遍历一遍 fiber 树,而 performUnitOfWork 方法里又包括两个阶段 beginWork 和 completeWork 。
function performUnitOfWork(){
next = beginWork(current, unitOfWork, renderExpirationTime);
if (next === null) {
next = completeUnitOfWork(unitOfWork);
}
}
beginWork负责向下调和,大概内容是:
而completeUnitOfWork负责向上调和:
这么一上一下两个方法的指针交替游走,完成Fiber树的遍历,构成了整个调和的render阶段。
Commit阶段
这个阶段拿到前面 render 阶段遍历好的 effectList 链表直接做遍历更新Dom,做增删改查,一方面是对一些重要生命周期和副作用钩子的处理,比如 componentDidMount ,函数组件的 useEffect ,useLayoutEffect 等;还有一些细节比如 ref 的处理。
可以分为三个部分:
至此,从ReactDom.render页面初始化,到一次更新调和完成的 React 内部更新的主流程已经明朗,但似乎目前为止依然没有解决卡顿问题,并且 React 似乎无法打破从 root 开始遍历‘找不同’的命运,那怎么办,既然更新过程阻塞了浏览器的绘制,那么把 React 的更新,交给浏览器自己控制不就可以了吗,如果浏览器有绘制任务那么执行绘制任务,在空闲时间执行更新任务,就能解决卡顿问题了,配合这套Fiber调和更新架构的调度( Scheduler )机制就是具体的实现方式。
调度流程控制进入调度
如果有一个组件 A ,如果想要它更新,那么场景有如下情况:
无论是哪种方式创造的组件更新,本质就是:
function scheduleUpdateOnFiber(fiber,lane){
/* 递归向上标记更新优先级 */
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if(root === null) return null
/* 如果当前 root 确定更新,那么会执行 ensureRootIsScheduled */
ensureRootIsScheduled(root, eventTime);
}
scheduleUpdateOnFiber 主要做了两件事:
markUpdateLaneFromFiberToRoot 如何标记的优先级?
ensureRootIsScheduled最后如何更新?
所有非初始化类型的更新任务,那么最终会走到这个方法,主要做的事情有:
什么情况下会存在 existingCallbackPriority === newCallbackPriority,退出调度的情况?
我们注意到在一次更新中最后 callbackPriority 会被赋值成 newCallbackPriority 。那么如果在正常模式下(非异步)一次更新中触发了多次setState或者useState,那么第一个 setState 进入到 ensureRootIsScheduled 就会有 root.callbackPriority = newCallbackPriority,那么接下来如果还有 setState | useState,那么就会退出,将不进入调度任务中,原来这才是批量更新的原理,多次触发更新只有第一次会进入到调度中。
进入调度
当进入到 scheduleSyncCallback 中会发生什么呢?
调度阶段正在编写...