为什么说前端开发绕不开ReactFiber?

前有科技后进阶 2024-02-13 18:45:38

大家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

前言

React Fiber(发音/[ˈfaɪbər]/) 是 React 16 中的新协调算法,大多数前端开发可能听说过 React 15 中的 virtualDOM,但它是旧的协调器算法(也称为 Stack Reconciler),内部使用堆栈。相同的协调器与不同的渲染器,例如: DOM、Native 和 Android View 共享。 因此,将其称为 virtualDOM 可能会导致混乱。

1.什么是 React Fiber

fiber(注意首字母是小写) 是一个简单的 JavaScript 对象,代表 React 元素或 DOM 树的节点,是一个工作单元。 相比之下,Fiber(大写) 是 React Fiber 调节器。

React Fiber 是对旧协调器(Reconciler)完全向后兼容的重写,这种新协调算法称为 Fiber Reconciler。 该名字来自 fiber,表示 DOM 树的节点。

Fiber 协调器的主要目标是:

增量渲染平滑的 UI 动画/手势用户交互的快速响应能力

协调器还允许开发者将工作划分为多个块并将渲染工作划分为多个帧,同时增加了定义每个工作单元的优先级以及暂停、重用和中止的能力。

React 新版本还带来了一系列其他一些功能,包括:从渲染函数返回多个元素、支持更好的错误处理(使用 componentDidCatch 方法来获取更清晰的错误消息)和 Portals。

在计算新的渲染更新时,React 会多次引用主线程。 其内部定义了每个更新的优先级从而允许跳过低优先级工作。

2.React Fiber 三要素2.1 Reconciliation(协调)

如 React 官方所述,协调是比较两个 DOM 树的算法。协调过程使 React 工作得更快,本质上是 React 更新浏览器 DOM 的完整过程,协调背后包括两个重要概念,即虚拟 DOM 和 DOM 比较算法。

虚拟 DOMReact 将 JSX 组件渲染到浏览器 DOM,但保留实际 DOM 的副本, 该副本就是虚拟 DOM,可以将其视为真实 DOM 或浏览器 DOM 的孪生。 React 实际渲染过程会发生以下操作:React 存储浏览器 DOM 的副本,称为虚拟 DOM当数据更改时,React 会创建一个新的 Virtual DOM 并将其与前一个进行比较比较是通过 Diffing 算法完成,比较都发生在内存中,浏览器未发生任何更改比较后 React 继续创建一个具有更改的新虚拟 DOM。用尽可能少的更改来更新浏览器 DOM,而无需再次渲染整个 DOM,从而极大改变应用程序的效率DOM Diffing Algorithm

Diffing Algorithm 表示 Virtual DOM 不同版本之间的比较算法。

Diffing Algorithm 基于以下事实和优化策略:

不同类型的两个元素会产生不同的树广度优先搜索 (BFS) :ReactJS 使用 BFS(Breadth First Search) 遍历树。比如下面的树:元素 B 和 H 的状态发生了变化,当 ReactJS 使用 BFS 到达元素 B 时,它会默认重新渲染元素 H,这就是为什么使用 BFS 进行树遍历的原因。

当比较相同类型的两个元素时,保持底层节点相同,只更新属性或样式的变化React 使用优化,以便可以使用该算法在 O(N) 中有效地计算最小差异2.2 Scheduling

如 React 官方所述,假设有一些低优先级的工作(例如:大型计算函数、或者最近 fetch 请求元素的渲染)和一些高优先级的工作(例如:动画)需要同时渲染, 应该有一个选项来自由控制。

在旧的 Stack Reconciler 中,递归遍历和调用整个更新树的渲染方法发生在单个流程中,从而可能会导致卡顿和丢帧。

React 的 Scheduling 可以基于时间(比如 deadline)或基于优先级,高优先级工作应安排在低优先级工作之前。

2.3 RequestIdleCallback

requestAnimationFrame 用于在下一个动画帧之前调用高优先级函数, 而 requestIdleCallback 用于在帧结束时的空闲时间调用低优先级的方法。

function lowPriorityWork(deadline) { while (deadline.timeRemaining() > 0 && workList.length > 0) performUnitOfWork(); // 执行工作单元 if (workList.length > 0) requestIdleCallback(lowPriorityWork);}requestIdleCallback(lowPriorityWork.bind(null, deadline));

上面显示了 requestIdleCallback 的用法, lowPriorityWork 是一个回调函数,将在帧结束时的空闲时间内调用。

当调用此回调函数时,它会获取参数 deadline 对象。 正如在上面的代码片段中看到的,timeRemaining 函数返回最新的剩余空闲时间。 如果这个时间大于零,就可以继续执行其他的工作,反之则需要立即执行 lowPriorityWork。

3.Fiber 如何工作

接下来重点讲述 React Fiber 如何创建链表树(linked list tree)以及当有更新时如何做。

在此之前,先解释一下当前树(current tree)和 workInProgress 树是什么以及树的遍历如何发生。

当前刷新以渲染 UI 的树称为当前树,它是用来渲染当前 UI 的。 每当有更新时,Fiber 都会构建一个 workInProgress 树,该树是根据 React 元素的更新数据创建的。 React 在此 workInProgress 树上执行工作,并使用此更新的树进行下一次渲染。 一旦此 workInProgress 树渲染在 UI 上,它将自动成为当前树。

Fiber 树的遍历遵循以下流程:

Start:Fiber 从最顶层的 React 元素开始遍历,并为其创建一个 fiber 节点Child:转到子元素并为其创建一个 fiber 节点, 一直持续到叶子节点。Sibling:检查是否有兄弟元素, 如果有则遍历兄弟节点子树,直到找到兄弟节点的叶子节点Return:如果没有兄弟元素,则返回到父级。

每个 fiber 都有一个子属性(如果没有子属性则为 null 值)、兄弟属性和父属性,其都是 Fiber 中作为链表工作的指针。

3.1 初始渲染(Initial render)

在进一步遍历之前,React Fiber 会创建根 fiber, 每棵 Fiber 树都有一个根节点,比如:在下图中是 HostRoot。 如果在 DOM 中导入多个 React 应用程序,则可以有多个根。

第一次渲染时,不会有任何树。 React Fiber 遍历每个组件渲染函数(Render Function)的输出,并在树中为每个 React 元素创建一个 fiber 节点。 其使用 createFiberFromTypeAndProps 将 React 元素转换为 fiber。 React 元素可以是类组件或宿主组件(Host component),例如 div 或 span。 对于类组件,它创建一个实例,对于主机组件(Host component),它从 React Element 获取数据/属性。

因此,如上图所示,创建了一个 Fiber App。 更进一步,又创建了一个 fiber W,然后它转到子 div 并创建了一个 fiber L。依此类推,为其子级创建了一个 fiber LA 和 LB。 fiber LA 的返回 fiber(父 fiber)为 L,兄弟 fiber 为 LB。

3.2 更新阶段(Update Phase)

接下来讨论视图更新,例如:当用户调用了 setState。

此时,Fiber 已经有了当前树(current tree)。 对于每次更新,都会构建一个 workInProgress 树。 它从 root fiber 开始,遍历树直到叶节点。 与初始渲染阶段不同,它不会为每个 React 元素创建新的 fiber,只是使用该 React 元素预先存在的 fiber,并在更新阶段合并来自更新元素的新数据和属性。

在 React Fiber 中,每个更新都像动画一样定义其优先级,比如:用户输入的优先级高于用返回数据来渲染项目列表的优先级。 Fiber 使用 requestAnimationFrame 进行较高优先级更新,使用 requestIdleCallback 进行较低优先级更新。 因此,在调度工作时,Fiber 会检查当前更新的优先级和截止时间(帧结束后的空闲时间)。

总之,React Fiber 可以在某一帧后同时调度多个工作单元,前提是这些工作单元比其他工作单元的优先级更高同时也没有其他工作单元达到 deadline 指定的时间。于此同时,下一组工作单元将继续到其他帧中,这就是 Fiber 的暂停、重用和中止工作单元的机制。

那么,一起看看预定的工作中实际发生了什么。 完成工作有两个阶段:渲染和提交。

4.渲染阶段(Render Phase)

DOM 树的遍历和 deadline 的使用发生在渲染阶段。 这是 Fiber 的内部逻辑,因此此阶段对 Fiber 树所做的更改对用户来说是不可见的。 因此 Fiber 可以暂停、中止或划分多个帧上的工作。

可以将这个阶段称为 Reconciliation 阶段(Reconciliation Phase)。 Fiber 从 fiber 树的根部开始遍历,对每个 fiber 进行处理。 每个工作单元都会调用 workLoop 函数来执行工作,可以将工作的处理分为两个步骤:开始和完成。

4.1 开始步骤

React 的 workLoop 函数会调用 performUnitOfWork,该函数将 nextUnitOfWork 作为参数,是要执行的工作单元。 PerformUnitOfWork 函数内部调用 beginWork 函数,这是 fiber 实际工作的地方,而 performUnitOfWork 是迭代发生的地方。

// 注意:所有 Fiber 节点都使用 Fiber 节点上的 child、sibling 和 return属性并通过链表连接function completeUnitOfWork(workInProgress) { while (true) { let returnFiber = workInProgress.return; let siblingFiber = workInProgress.sibling; nextUnitOfWork = completeWork(workInProgress); if (siblingFiber !== null) { // 如果有siblings,则返回 // 为该sibling执行工作 return siblingFiber; } else if (returnFiber !== null) { // 如果 returnFiber 中没有更多工作, // 继续循环以完成父级 workInProgress = returnFiber; continue; } else { // 已经到root节点 return null; } }}// 完成工作function completeWork(workInProgress) { console.log('work completed for ' + workInProgress.name); return null;}function beginWork(workInProgress) { console.log('work performed for ' + workInProgress.name); return workInProgress.child;}// workInProgress参数是要执行的工作单元function performUnitOfWork(workInProgress) { let next = beginWork(workInProgress); if (next === null) { // 没有子级元素child next = completeUnitOfWork(workInProgress); } return next;}// 入口函数function workLoop(isYieldy) { if (!isYieldy) { while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } } else {...}}

在 beginWork 函数内部,如果 fiber 没有任何待处理的工作,只会退出 fiber,而不进入开始阶段。 在遍历树时,Fiber 会跳过已经处理过的 Fiber,直接跳转到有待处理工作的 Fiber。 如果看到大的 beginWork 函数代码块,会找到一个 switch 块, 其根据 Fiber 标签调用相应的 Fiber 更新函数。 就像主机组件的 updateHostComponent 一样, 这些功能更新 fiber。

如果有子 fiber,则 beginWork 函数返回子 fiber。如果没有 fiber,则返回 null。 PerformUnitOfWork 函数不断迭代并调用子 fiber,直到到达叶节点。 对于叶节点,beginWork 返回 null,因为没有任何子节点,此时 performUnitOfWork 函数调用 completeUnitOfWork 函数。

4.2 完成步骤(Complete Step)

completeUnitOfWork 函数通过调用 completeWork 函数来完成当前的工作单元。 如果有任何执行下一个工作单元的 fiber,则 completeUnitOfWork 返回同级 fiber,否则如果没有工作则完成返回(父)fiber。

这一直持续到返回为空,即到达根节点。 与 beginWork 一样,completeWork 也是实际工作发生的函数,completeUnitOfWork 用于迭代。

渲染阶段的结果创建一个效果列表(副作用)。 这些效果就像插入、更新或删除宿主组件的节点,或者调用类组件的节点的生命周期方法, fiber 上标有相应的效果标签。

渲染阶段结束后,Fiber 将准备好提交更新。

5.提交阶段(Commit Phase)

在此阶段,所有的工作将用于将其渲染在 UI 上。 由于此阶段的结果对用户可见,因此不能将其划分为部分渲染(partial render),该阶段是同步的。

在此阶段开始时,Fiber 具有已在 UI 上渲染的当前树、finishedWork 或在渲染阶段构建的 workInProgress 树和效果列表(Effect List)。

效果列表是 fiber 的链表,它有副作用。 因此,它是渲染阶段 workInProgress 树的节点子集,具有副作用(更新)。 效果列表(Effect List)节点使用 nextEffect 指针链接。

// https://github.com/Luminqi/learn-react/blob/master/Guide/Fiber_part2.md// commitAllHostEffects 是 React 执行 DOM 更新的函数,该函数定义了节点需要完成的操作类型并执行function commitAllHostEffects() { switch (primaryEffectTag) { case Placement: { commitPlacement(nextEffect); ... } case PlacementAndUpdate: { commitPlacement(nextEffect); commitWork(current, nextEffect); ... } case Update: { commitWork(current, nextEffect); ... } case Deletion: { // React 在 commitDeletion 函数中调用 componentWillUnmount 方法作为删除过程的一部分 commitDeletion(nextEffect); ... } }}// 下面是迭代 Effect 树并检查节点是否具有 Snapshot 的 Effect:function commitBeforeMutationLifecycles() { while (nextEffect !== null) { const effectTag = nextEffect.effectTag; if (effectTag & Snapshot) { const current = nextEffect.alternate; commitBeforeMutationLifeCycles(current, nextEffect); } nextEffect = nextEffect.nextEffect; }}function commitRoot(root, finishedWork) { commitBeforeMutationLifecycles() // 检查是否有指定Effect commitAllHostEffects(); // 执行所有DOM更新 root.current = finishedWork; commitAllLifeCycles(); // React 调用所有剩余生命周期方法 componentDidUpdate 和 componentDidMount函数 // fiber 的出现对生命周期有影响,可能多次被调用,比如:render、componentWillReceiveProps、shouldComponentUpdate、componentWillMount、componentWillUpdate}

此阶段调用的函数是 completeRoot。

在这里,workInProgress 树成为当前树,因为它用于渲染 UI。 实际的 DOM 更新(例如插入、更新、删除和调用生命周期方法)或与 refs 相关的更新发生在 Effect 列表中的节点上。这就是 Fiber 协调器的工作原理。

6.React Fiber 常见问题6.1 说一下 React Fiber

React Fiber 通过两个原生的 API,即:requestAnimationFrame 和 requestIdleCallback(接受第二个参数,超过该时间还没有执行则下一帧强制执行) 把时间、渲染工作切片,优化渲染性能,实现一秒 60 帧的最低目标(每一帧需要完成 layout、paint、js 执行、用户输入、requestAnimationFrame 等等)。

因为 React 是函数式编程,单向数据流,需要手动 setState 来更新,所以当数据改变时会默认全部重新渲染整个组件树,而构建虚拟 DOM 则是采用同步递归的方式,如果组件很复杂且嵌套深,那么这个构建虚拟 DOM 的过程就需要很多时间,而这种任务默认要执行完才会把控制权交给浏览器,一旦执行时间很长,可能就会地把浏览器卡死。

虽然可以用 pureComponent、shouldComponentUpdate、useMemo、useCallback 等方法来进行控制部分更新或缓存,但也是治标不治本。而 fiber 是可以使渲染过程被中断,可以把控制权先交还给浏览器,让位高优先级的任务,等浏览器空闲时再恢复渲染,这样就不会显得卡顿,而是一帧一帧有规律的执行任务,看起来虽然有点慢,但是一直在慢慢执行的。

注意:requetIdleCallback 兼容性很差,React 是通过 MessageChannel 模拟实现的 requetIdleCallback 的功能,当然也可以使用 BroadcastChannel。

6.2 React Fiber 的其他实现

可以使用 Generator 实现:

// 任务列表const tasks = [];function* run() { let task; while ((task = task.shift())) { // 如果有高优先级的任务 if (hasHighPriorityTask()) { // 中断 yield; } }}// 中断后恢复const iterator = run();// 这样就恢复了iterator.next();

不使用 Generator 主要是以下原因:

React 是迭代的:而使用 generator + yield 的话需要把所有代码都包装成这个形式,非常麻烦,工作量很大generator 内部是有状态:比如一个函数里有用到多个 yield 中断,就像 await 一样,有时候后面的会依赖前面的结果,可当后面的执行前,前面的又更新了,后面就无法拿到最新的值,这样就不可控了,所以就自己实现一个完全可控的6.3 React 超时一定会执行么

React 里有 5 个优先级的等级,高优先级的会被优先执行:

Immediate: 最高优先级,会马上执行的不能中断UserBlocking: 这一般是用户交互的结果,需要及时反馈Normal: 普通等级的,比如网络请求等不需要用户立即感受到变化的Low: 低优先级的,这种任务可以延后,但最后始终是要执行的Idle: 最低等级的任务,可以被无限延迟的,比如 console.log()如果是相同优先级的任务,就会按推入任务队列的顺序来执行参考资料

https://www.velotio.com/engineering-blog/react-fiber-algorithm

https://www.geeksforgeeks.org/reactjs-reconciliation/

https://shiharadilshan.medium.com/react-reconciliation-and-diffing-algorithm-5faa9531175

https://github.com/rohan-paul/Awesome-JavaScript-Interviews/blob/master/React/Virtual-DOM-and-Reconciliation-Algorithm.md

https://blog.ag-grid.com/inside-fiber-an-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/

https://github.com/Luminqi/learn-react/blob/master/Guide/Fiber_part2.md

https://blog.51cto.com/u_15403705/6509090

https://fe.azhubaby.com/React/Fiber.html

0 阅读:208

前有科技后进阶

简介:感谢大家的关注