前端宝典之三:React源码解析之Fiber架构

桃子叔叔 2024-08-23 16:03:01 阅读 69

本文主要内容:

1、React Concurrent

2、React15架构

3、React16架构

4、Fiber架构

5、任务调度循环和fiber构造循环区别

一、React Concurrent

React在解决CPU卡顿是会用到<code>React Concurrent的概念,它是React中的一个重要特性和模块,主要的特点和原理如下

一、主要特点和优势

1、时间切片(Time Slicing)

允许将长时间运行的任务分割成小块,在不阻塞主线程的情况下逐步执行。例如,在一个复杂的数据计算或渲染大型组件树时,React可以将这些任务分成多个小片段,在每一帧的空闲时间里执行一部分,这样就不会导致页面出现卡顿,保证了页面的响应性。

对于用户体验来说至关重要,用户在操作过程中不会感觉到界面被冻结,仍然可以进行其他交互,比如滚动页面、点击按钮等。

2、 并发渲染(Concurrent Rendering)

React可以同时处理多个渲染任务,根据任务的优先级和紧急程度来合理安排渲染顺序。

对于有紧急更新(如用户交互导致的状态变化)和非紧急更新(如后台数据的缓慢加载更新)的场景非常有用。紧急更新会被优先处理,以确保用户操作能够立即得到反馈,非紧急更新则在合适的时机进行,避免影响性能。

3、Suspense 组件和懒加载(Lazy Loading)

Suspense组件用于在等待异步数据加载时显示一个加载状态或者回退内容。比如,当一个页面依赖于从服务器获取的数据来渲染组件时,在数据加载过程中可以显示一个加载动画,提升用户体验。

懒加载允许在真正需要的时候才加载组件或模块。例如,对于一个大型应用,某些页面或功能模块可能不是在初始加载时就需要的,通过懒加载可以减少初始加载时间,提高应用的启动速度。当用户导航到相应的页面或触发特定功能时,对应的组件或模块才会被加载。

二、工作原理

1、 任务调度

React内部有一个任务调度器,它负责管理和分配任务的执行时间。调度器会根据浏览器的每一帧的时间预算,将任务分割成合适的时间片来执行。

对于不同优先级的任务,调度器会采用不同的策略。高优先级的任务(如用户交互响应)会尽快得到执行,而低优先级的任务(如数据预取)则会在系统空闲时执行。

2、纤维架构(Fiber Architecture)的支持

React的纤维架构是实现并发功能的基础。纤维是一种轻量级的数据结构,每个纤维代表一个组件。在渲染过程中,React可以暂停、恢复和重新调度纤维的渲染。

纤维架构使得React能够在渲染过程中进行中断和恢复,以便在合适的时机处理其他任务。例如,当一个组件的渲染被中断时,React会保存当前的渲染状态,然后在后续合适的时间点继续渲染。

二、React15架构

在 React 15 架构中:

1、 Reconciler(协调器)负责找出变化的组件

reconciler(协调器)采用的是 stack reconciler 解决方案。它在更新子组件时会进行递归操作,且一旦开始更新就无法中断。当组件层级较深时,这种同步的递归更新可能会导致在一帧内无法完成更新,从而使用户交互出现卡顿的情况。为了解决卡顿16版本进行了功能重构。

(一)工作方式

React中可以通过this.setState、 this.forceUpdate、ReactDom.render等API触发更新。

每当有更新发生时,reconciler会做如下工作:

触发更新。

调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM虚拟DOM和上次更新时的虚拟DOM对比通过对比找出本次更新中变化的虚拟DOM通知Renderer将变化的虚拟DOM渲染到⻚面上

在 React 15 版本中,Reconciler(协调器)的工作方式具有以下特点和原理:

(二)功能

1、 构建虚拟 DOM 树

React 维护了一个虚拟 DOM(Virtual DOM)的概念。在 React 15 中,Reconciler 负责根据组件的定义和状态构建虚拟 DOM 树。每个 React 组件都对应虚拟 DOM 树中的一个节点,包含组件的类型、属性、子节点等信息。

例如,当定义一个简单的 <div> 组件时,Reconciler 会为其创建相应的虚拟 DOM 节点来表示这个组件在虚拟 DOM 树中的结构和属性。

2、 协调更新

当组件的状态或属性发生变化时,Reconciler 会启动更新流程。它会比较变化前后的虚拟 DOM 树,以确定哪些部分需要进行实际的 DOM 操作来更新页面。

例如,当一个按钮组件的文本从“点击我”变为“已点击”时,Reconciler 会识别出这个变化,并确定需要更新对应的 DOM 元素中的文本内容。

3、 管理组件生命周期

Reconciler 在协调过程中还会触发组件的生命周期方法。在 React 15 中,常见的生命周期方法如 componentWillMountcomponentDidMountcomponentWillReceivePropsshouldComponentUpdatecomponentWillUpdatecomponentDidUpdate 等。

比如在组件挂载阶段,componentWillMountcomponentDidMount 会依次被调用,让开发者可以在这些方法中执行一些初始化操作或发送网络请求等。

(三)原理

1、深度优先遍历和递归

Reconciler 采用深度优先遍历的方式来递归地处理虚拟 DOM 树。从根节点开始,依次遍历每个子节点及其子树。

例如,对于如下的组件结构:<Parent><Child1><GrandChild1></GrandChild1></Child1><Child2></Child2></Parent>,它会先处理 Parent 组件,然后是 Child1,接着是 GrandChild1,再回到 Child2

在递归过程中,每个组件会根据自身的 render 方法生成新的虚拟 DOM 子树,然后 Reconciler 继续对这些子树进行处理。

2、 diff 算法比较

在更新阶段,Reconciler 使用 diff 算法来比较新旧虚拟 DOM 树的差异。它会逐个节点地进行比较,检查节点类型、属性、子节点等方面的变化。

如果节点类型不同,React 通常会认为整个子树都发生了变化,会销毁旧的 DOM 节点并创建新的 DOM 节点。例如,旧的虚拟 DOM 节点是 <div>,新的是 <span>,那么整个 <div> 及其内部的子节点都会被替换为 <span> 及其子节点。

如果节点类型相同,会进一步比较属性的变化。例如,检查 classNamestyle 等属性是否有更新,如果有变化,就会相应地更新实际的 DOM 元素的属性。

对于子节点,会通过遍历和比较的方式来确定子节点的添加、删除或移动等操作。

2、Renderer(渲染器)负责将变化的组件渲染到页面上

(一) 同步更新

React 15 的 Reconciler 执行更新是同步的。这意味着当一个 setState 触发更新时,React 会立即开始进行 Reconciliation(协调)Rendering(渲染)过程,直到整个更新过程完成。

虽然同步更新在一些简单场景下比较直观,但在复杂的应用中,如果有大量的计算或长时间的同步操作,可能会导致页面卡顿,因为浏览器在更新过程中无法响应用户交互或进行其他任务。

(二)更新原理图

在这里插入图片描述

图中<code>state.count乘以2之后,Reconciler找出变化的组件交给renderer进行更新,整个过程是同步递归实现的,直到组件都遍历完成。

在 React 16 及以后的版本中,Reconciler 进行了重大的改进,引入了 Fiber 架构,使得更新过程可以被中断和恢复,提高了性能和用户体验。

三、React16架构

1、 Scheduler(调度器)

调度任务的优先级,高优任务进入Reconciler

React Scheduler 的优先级划分主要基于以下几个方面:

一、不同优先级的分类

Immediate Priority(同步优先级)

这是最高优先级,通常用于处理用户交互等关键操作,比如处理点击事件、键盘输入等。这些操作需要立即得到响应,以提供流畅的用户体验。

当任务具有同步优先级时,React 会尽可能快地执行它,甚至可能中断正在进行的其他任务来处理它。

UserBlocking Priority(用户阻塞优先级)

此优先级用于那些会影响用户体验,但不是立即关键的任务。例如,一些数据预取操作,虽然不是立即必需,但如果延迟太久会让用户感觉到卡顿。

通常会在不影响当前用户交互的情况下尽快执行。

Normal Priority(普通优先级)

这是默认的优先级级别,适用于大多数常规的更新和渲染任务。比如组件的状态更新导致的重新渲染通常是普通优先级。

React 会在合适的时机调度和执行这些任务,以平衡性能和响应性。

Low Priority(低优先级)

用于不太紧急的任务,比如一些后台数据同步或者非关键的日志记录等。

这些任务可能会在其他更高优先级的任务完成后,或者在系统空闲时才会被执行。

Idle Priority(空闲优先级)

这是最低的优先级,用于那些可以在任何时间执行,并且不会影响用户体验的任务,比如一些清理操作或者统计信息的收集等。

通常在浏览器空闲时间执行,不会抢占其他任务的执行时间。

2、 Reconciler(协调器)

负责找出变化的组件

React16的Reconciler更新机制发生了变化

在版本15中是递归方式

在版本16中是可以中断的循环过程

function performSyncWorkOnRoot(root) {

// 开始同步更新根节点

const finishedWork = workLoopSync();

// 完成更新后进行提交阶段

commitRoot(root, finishedWork);

}

function workLoopSync() {

while (workInProgress!== null && !shouldYield()) {

performUnitOfWork(workInProgress);

}

return workInProgress;

}

function performUnitOfWork(fiber) {

const next = beginWork(fiber);

if (next === null) {

completeUnitOfWork(fiber);

} else {

workInProgress = next;

}

}

workLoopSync是一个同步工作循环函数。它的主要目的是在同步模式下处理工作单元(可能是 React 组件的更新任务)。

循环条件包含两个部分:

workInProgress!== null:只要还有未完成的工作(workInProgress不为空),循环就会继续。workInProgress通常代表当前正在处理的 Fiber 节点。!shouldYield():在每一次循环迭代中,会检查是否应该让出执行权(yield)。如果shouldYield()返回false,表示可以继续执行当前的工作,不会中断循环;如果返回true,则表示可能需要暂停当前工作,给其他任务或浏览器一些时间来处理其他事情(例如响应用户输入、渲染动画等)。

例如:在处理一个大型组件树时,如果浏览器的每一帧时间内还有剩余时间来处理更多的更新工作,workLoopSync会继续处理下一个工作单元;如果当前帧的时间已经用尽,shouldYield()可能会返回true,导致循环暂停,等待下一个机会继续执行。

3、Renderer(渲染器)

负责将变化的组件渲染到页面上

在React16中的变化:

ReconcilerRenderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记

整个SchedulerReconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。

在这里插入图片描述

其中红框中的步骤随时可能由于以下原因被中断:

有其他更高优任务需要先更新当前帧没有剩余时间

由于红框中的工作都在内存中进行,不会更新页面上的DOM,所以即使反复中断,用户也不会看见更新不完全的DOM

四、Fiber架构

以下是关于 React Fiber 的详细介绍:

1、React Fiber 定义

React Fiber 是 React 16 对其核心算法(Reconciler,协调器)进行的一次重写,它是一种新的协调算法和架构模式。目的是使 React 能够更好地处理大型应用程序和更复杂的更新,同时提高应用的响应性和用户体验。

特点:支持状态更新、优先级调度、异步可中断

1. 作为架构来说

之前<code>React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack ReconcilerReact16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler

2. 作为静态的数据结构来说

每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的DOM节点等信息;

3. 作为动态的工作单元来说

每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…);

2、原理解析

任务分割与优先级处理

React Fiber 将更新任务分割成一个个小的工作单元(Fiber 节点)。每个 Fiber 节点代表一个组件或者 DOM 节点的更新任务。

引入了优先级的概念,不同的更新任务可以有不同的优先级。高优先级的任务(如用户交互产生的更新)可以中断低优先级的任务(如数据预取后引发的更新),优先得到处理。

可中断与恢复

在执行更新任务时,React Fiber 采用了可中断的循环方式。通过 requestIdleCallback(在不支持的浏览器中使用 setTimeout 模拟)等机制,React 可以在每一个时间片(大约 5ms)内执行一部分任务。

如果在一个时间片内没有完成当前任务,它会保存当前的状态,让出主线程的控制权,等待下一个时间片继续执行,从而实现可中断与恢复。

Fiber 节点结构

Fiber 节点包含了多个重要的属性:

return:指向父节点,用于在遍历树时回溯。child:指向第一个子节点。sibling:指向下一个兄弟节点。tag:表示节点的类型,如函数组件、类组件、原生 DOM 元素等。pendingPropsmemoizedProps:分别表示即将应用的属性和已经应用的属性。

双缓冲树

React Fiber 使用了双缓冲的 Fiber 树机制。在更新过程中,会构建一棵新的 Fiber 树(称为“workInProgress 树”),同时保留旧的 Fiber 树(“current 树”)。

当新树构建完成后,通过指针切换,瞬间将新树变为当前树进行渲染,避免了一次性大规模的 DOM 操作,提高了更新的效率和性能。

在这里插入图片描述

(1)首次执行

首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点;

(2)render阶段

根据组件返回的<code>JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber

(3)alternate阶段

此时workInProgress fiber已经构建完成,fiberRootNodecurrent指向了workInProgress fiber

(4)update阶段

假设p元素更新,这会开启一次新的render阶段并构建一棵新的workInProgress Fiber 树,且会尽可能复用现有的current Fiber

(5)alternate阶段

workInProgress fiber在更换完后,fiberRootNodecurrent指针更换

3、实例分析

假设一个简单的 React 应用,包含一个父组件和多个子组件:

import React, { useState } from 'react';

function ChildComponent({ index }) {

return <div>Child { index}</div>;

}

function ParentComponent() {

const [count, setCount] = useState(0);

const handleClick = () => {

setCount(count + 1);

};

const childComponents = [];

for (let i = 0; i < 10; i++) {

childComponents.push(<ChildComponent key={ i} index={ i} />);

}

return (

<div>

<button onClick={ handleClick}>Increment</button>

{ childComponents}

</div>

);

}

export default ParentComponent;

当用户点击按钮触发 setCount 状态更新时:

React Fiber 开始协调更新过程。从 ParentComponent 根节点对应的 Fiber 节点开始,进入工作循环。遍历子组件,为每个 ChildComponent 创建或更新对应的 Fiber 节点。如果在处理某个子组件的过程中,时间片用完或者有更高优先级的任务进来,React Fiber 会暂停当前工作,保存当前状态,等待下一次继续执行。所有组件的 Fiber 节点处理完成后,构建出完整的 workInProgress 树。最后,将新的 workInProgress 树切换为 current 树进行渲染,用户界面得到更新。

总之,React Fiber 带来了更高效、更灵活的更新机制,使 React 能够更好地应对复杂的应用场景和用户需求。

五、任务调度循环和fiber构造循环区别

1、定义

(1)任务调度循环

源码位于Scheduler.js, 它是react应用得以运行的保证, 它需要循环调用, 控制所有任务(task)的调度.

(2)fiber构造循环

源码位于ReactFiberWorkLoop.js, 控制 fiber 树的构造, 整个过程是一个深度优先遍历.

这两个循环对应的 js 源码不同于其他闭包(运行时就是闭包), 其中定义的全局变量, 不仅是该作用域的私有变量, 更用于控制react应用的执行过程.

2、区别

任务调度循环

主要负责对更新任务进行调度和优先级管理。

任务调度循环是以二叉堆为数据结构(详见react 算法之堆排序), 循环执行堆的顶点, 直到堆被清空.

任务调度循环的逻辑偏向宏观, 它调度的是每一个任务(task), 而不关心这个任务具体是干什么的(甚至可以将Scheduler包脱离react使用), 具体任务其实就是执行回调函数performSyncWorkOnRoot或performConcurrentWorkOnRoot.

fiber构造循环

是以树为数据结构, 从上至下执行深度优先遍历(详见react 算法之深度优先遍历).

fiber构造循环的逻辑偏向具体实现, 它只是任务(task)的一部分(如performSyncWorkOnRoot包括: fiber树的构造, DOM渲染, 调度检测), 只负责fiber树的构造.

每个 Fiber 节点代表一个组件或者 DOM 节点的更新任务,Fiber 构造循环通过遍历这些节点来协调组件的更新和渲染。

3、联系

任务调度循环和 Fiber 构造循环在 React 中是紧密关联的,它们协同工作来实现高效的组件更新和页面渲染:

一、任务触发与传递

任务调度循环是更新任务的入口和管理者。当有更新事件发生(如用户交互、状态改变、网络数据获取等),任务调度循环首先感知到这些事件并将它们转化为相应的更新任务。

这些更新任务会被分配优先级,并根据优先级排队等待执行。当轮到某个任务执行时,任务调度循环会将任务传递给 Fiber 构造循环,启动组件树的更新过程。

例如,用户点击一个按钮触发了组件的状态更新,任务调度循环会捕获这个事件,将其包装成一个更新任务并确定其优先级。然后,当条件合适时,它会将这个任务传递给 Fiber 构造循环来开始处理组件树的更新。

二、时间片与可中断性协调

任务调度循环利用时间片的概念来管理任务的执行时间。它会根据浏览器的空闲时间和帧刷新率等因素,为每个任务分配一个时间片(通常是几毫秒)。

Fiber 构造循环在执行过程中会遵循任务调度循环分配的时间片。如果在一个时间片内无法完成当前的组件更新工作,Fiber 构造循环会暂停当前的工作,保存当前的状态和进度。

任务调度循环会在下一个合适的时间片再次启动 Fiber 构造循环,继续之前未完成的工作。这样的可中断性和时间片管理机制确保了页面在更新过程中仍然保持响应性,不会因为长时间的 JavaScript 执行而导致卡顿。

比如在更新一个复杂的组件树时,Fiber 构造循环可能在处理一个深层嵌套的组件时时间片用完。此时,它会暂停工作,任务调度循环会在后续的时间片中再次安排它继续执行。

三、优先级处理的一致性

任务调度循环和 Fiber 构造循环都遵循相同的优先级策略。高优先级的任务会在低优先级任务之前得到处理,以确保关键的用户交互和紧急的更新能够及时响应。任务调度循环在任务排队时会根据优先级进行排序,而 Fiber 构造循环在遍历组件树时也会优先处理高优先级的组件更新。

例如,一个用户输入的即时响应任务(高优先级)和一个后台数据预取后的组件更新任务(低优先级),任务调度循环会首先安排高优先级任务执行,然后再考虑低优先级任务。在 Fiber 构造循环中,也会优先处理与高优先级任务相关的组件更新。

四、共同服务于页面渲染和用户体验

任务调度循环和 Fiber 构造循环的最终目标都是为了实现高效的页面渲染和良好的用户体验。

任务调度循环通过合理的任务管理和优先级调度,确保重要的更新任务能够及时执行,同时避免任务堆积和页面卡顿。

Fiber 构造循环通过高效的组件树更新和可中断的工作方式,使得页面在更新过程中能够保持响应,减少用户感知到的延迟和卡顿。

无论是在处理频繁的用户交互还是大规模的组件树更新,这两个循环相互配合,使得 React 应用能够在各种复杂情况下都能提供流畅的用户体验。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。