• 作者:老汪软件技巧
  • 发表时间: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三个概念什么关系?

image.png

Fiber之间如何建立关联?

每一个 element 都会对应一个 fiber ,每一个 fiber 是通过 return , child ,sibling 三个属性建立起联系的,并用独特的tag标记对应记录Element的类型,同时保存了alternate,effect,memoizedProps,childLanes,expirationTime等调度和调和需要的信息。

image.png

到现在,我们至少需要有个确定的概念就是,在 React 内部,不止我们的 JSX 形成的 Element 树,还有一个与之对应的Fiber树,同时一一对应的真实 Dom 树,我们写下的 JSX 代码,在 React 内部得经过这三层模型处理成我们最后可以交互的页面。接下来就要从初始化和一次更新入手,看一下 fiber 是如何具体工作的。

Fiber 更新机制初始化

第一步:从render方法创建fiberRoot和rootFiber

image.png

一个 React 应用可以有多个 rootFiber ,但是只能有一个作为 fiberRoot(应用根节点)。

第二步:workInProgress和current

一旦两个root对象关联完成,进入调和阶段,之前我们了解到Fiber节点之间通过几个属性链接成树,现在更进一步,需要了解到 React 的 Fiber 使用的是双缓冲树这个数据结构去构建的,就意味着存在着在JSX Element和真实Dom之间其实不止一个而是有两颗 Fiber 树,分别是:

开始第一次调和,会进入 beginwork 流程,首先会复用当前 current 树( 第一次只有rootFiber一个节点 )的alternate作为 workInProgress 树的根(如果是第一次没有就创建一个Fiber作为workInProgress,并双向赋值alternate属性)

image.png

第三步:深度调和子节点,渲染视图

接下来会按照上述第二步,在新创建的 alternates 上,依次完成整个 fiber 树的遍历创建,类似于这样:

image.png

最后会把此时的 workInProgress 作为最新的current渲染树,把fiberRoot 的 current 指针指向 workInProgress 使其变为 current Fiber 树。到此完成 React 应用第一次初始化流程。

image.png

那之后如果,点击一次按钮,我们的 React 发生更新,会如何更新Fiber呢? 交替复写workInProgress!第一次初始化把新构建的workInProgress(上面图的右边)最后作为了当前的current,那左边这个RootFiber不是空出来了吗?!是的,第一次的更新将在左边发生!而后续的第三次,第四次,等等更新将交替在左右树上更新,然后不断切换fiberRoot的current属性指向就可以了,每个Fiber节点之间都使用alternate连接,以此来承载虚拟Dom的Diff对比:

image.png

调和流程

接下来我们来具体看看这个树是如何遍历完成创建和更新的,涉及到 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 中会发生什么呢?

调度阶段正在编写...


上一条查看详情 +使用css3画一只熊猫的动画
下一条 查看详情 +没有了