ReactFiber的理解与组成

544 阅读17分钟

Fiber的理解

React Fiber 是 React 中一种新的调度算法,是从 React 16 版本开始引入的。Fiber 的目标是实现增量式渲染,这意味着 React 应用程序可以更好地响应用户输入,更好地处理动画,以及更好地适应不同的设备和网络条件。

在之前的版本中,React 使用的是栈 reconciler,这种算法是递归的,一旦开始渲染,就无法中断。这意味着当 React 开始渲染一个组件时,它会一直进行到完成,中间不能被打断。如果组件的渲染工作很重,可能会导致用户界面的卡顿。

而 Fiber reconciler 是一个基于链表的增量式渲染算法。它把渲染工作分割成一个个小的任务单元,每个任务单元被称为 Fiber。每个 Fiber 都包含了一个组件的渲染任务,以及与这个任务相关的一些信息,比如父节点、子节点等等。React Fiber 使用一种优先级调度的策略,可以中断渲染任务,根据任务的优先级和剩余时间决定何时恢复任务,从而实现了任务的优先级控制和中断恢复。

通过 Fiber 架构,React 能够更好地处理复杂的用户界面,提高了应用程序的性能和响应速度。 它为 React 的并发模式(Concurrent Mode)和异步渲染提供了基础,使得 React 应用程序在不同的设备和网络环境下都能够更好地运行。

纤程(Fiber)概念在计算机科学中确实存在。在操作系统和编程领域,纤程通常是一种轻量级的线程实现,由应用程序负责管理,而非由操作系统内核进行调度。纤程与传统的操作系统线程不同,它们不会被操作系统自动调度,而是由应用程序显式地控制它们的执行。

纤程采用合作式多任务(Cooperative Multitasking)策略,即一个纤程运行时,需要显式地让出执行权给其他纤程,而不会被强制性地中断。这种方式相比于传统的线程(采用先占式多任务,Pre-emptive Multitasking),可以更精细地控制线程的切换时机,从而提高了系统的灵活性。

纤程通常有自己的堆栈和程序计数器等运行时上下文,但它们不会被操作系统进行调度,而是由应用程序根据需要进行手动切换。这种特性使得纤程更适合于某些特定的编程场景,例如需要高度自定义线程行为或在资源受限的环境中运行。

在React中,Fiber的概念也是类似的。React Fiber 是一种增量渲染的算法,它将组件的渲染过程分割成一系列的任务单元,每个任务单元就是一个Fiber。这些Fiber可以看作是React中的纤程,由React自己管理和调度,使得React应用程序能够更灵活地响应用户输入和执行渲染任务。

Fiber架构

在React Fiber架构中,应用程序的运行过程可以大致分为三个阶段:prerender(预渲染)、render(渲染)和commit(提交)。这些阶段描述了React应用程序内部在处理组件更新和渲染时的不同阶段。

import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    return <div>Hello, {this.props.name}!</div>;
  }
}

const rootElement = document.getElementById('root');

// 在首次渲染阶段,React会构建虚拟DOM树但不会实际渲染到页面上
// 以下代码是模拟React首次渲染阶段的过程

// 构建组件的虚拟DOM树
const fiberRoot = {
  stateNode: rootElement, // 根DOM节点
  props: null, // 初始props为空
  state: null, // 初始state为空
  child: {
    type: App, // 组件类型
    props: { name: 'World' }, // 组件的props
    state: null, // 组件的state
    child: null, // 子节点为空
  },
};

// 在实际的React中,以下代码是由React内部调用的
ReactDOM.render(<App name="World" />, rootElement);

Prerender(预渲染):

首次渲染阶段: 在应用程序首次加载时,React会进行预渲染。在这个阶段,React会构建组件的虚拟DOM树(Fiber树),但并不会将其实际渲染到DOM上。这个阶段的目的是为了构建出组件的初始状态,但并不触发实际的DOM操作。

在React应用程序首次加载时,会经历首次渲染阶段。在这个阶段,React会进行预渲染(prerendering)。具体来说,以下是首次渲染阶段的步骤:

  • 构建虚拟DOM树(Fiber树): React会遍历应用程序的组件树,并为每个组件创建对应的虚拟DOM节点(Fiber节点),这个过程构成了虚拟DOM树,也就是Fiber树。这些节点包含了组件的类型、props、state等信息。
  • 构建初始状态: 在构建Fiber树的过程中,React会根据组件的初始状态、props等信息,计算出每个组件的初始状态。这个初始状态会被保存在Fiber节点中。
  • 不触发实际的DOM操作: 尽管React构建了虚拟DOM树,但在首次渲染阶段,它并不会将虚拟DOM树中的内容实际渲染到页面上。这是因为在首次渲染阶段,React只是在内部构建了组件的初始状态和虚拟DOM结构,但并不会将这些内容显示给用户。

对于第二点,在React中,构建Fiber树的过程包括了计算组件的初始状态。这个初始状态指的是组件在首次渲染时的状态,它是根据组件的初始数据(通常是props)、组件内部的状态(如果是类组件的话)、以及可能的其他初始条件(例如context)来确定的。

在构建Fiber树时,React会根据组件的类型(函数组件、类组件等)以及传入的props等信息,计算出每个组件的初始状态。这个计算过程通常发生在组件被实例化(或者函数组件被调用)的时候。在这个阶段,React会执行组件的构造函数(如果是类组件的话)、函数体(如果是函数组件的话),并根据组件内部的逻辑和传入的props等信息,得出组件的初始状态。

这个初始状态的计算过程非常重要,因为它决定了组件在首次渲染时应该显示什么内容。例如,一个计数器组件的初始状态可能是0,一个显示用户信息的组件的初始状态可能是空,等等。这些初始状态会被保存在Fiber节点的数据结构中,在组件的生命周期中被使用和更新。构建初始状态的过程是React在组件渲染之前进行的计算和准备工作,它确保了组件在首次渲染时具有正确的初始数据,从而能够正确地显示在页面上。

对于第三点,在React中,首次渲染阶段是指应用程序首次加载时,React构建虚拟DOM树,但并不会将虚拟DOM树中的内容实际渲染到页面上。这是因为在这个阶段,React只是在内部构建了组件的初始状态和虚拟DOM结构,但并不会将这些内容直接呈现给用户。

虚拟DOM树是React内部用来表示UI结构的一种数据结构,它是一个轻量级的JavaScript对象树,用来描述页面上各个组件的层次结构和关系。React通过这个虚拟DOM树来进行高效的比较和更新操作,从而实现页面的动态更新。

在首次渲染阶段,React会构建组件的初始状态,这个状态通常根据组件的props、组件的构造函数(如果是类组件)、函数体(如果是函数组件)等信息计算得出。 同时,React也会构建每个组件对应的虚拟DOM结构,这个结构描述了组件在页面上应该呈现的内容。

然而,在这个阶段,虽然构建了初始状态和虚拟DOM树,但React并不会直接将其渲染到页面上。相反,React会等到整个虚拟DOM树构建完成后,才会在内部进行一次优化的协调(Reconciliation)过程,确定哪些部分需要实际渲染到页面上。这个过程包括了diff算法的执行,用于确定哪些部分需要被更新,哪些部分保持不变。

只有在这个协调阶段完成后,React才会进入提交阶段,将需要更新的部分实际渲染到页面上。所以,在首次渲染阶段,React只是在内部构建了初始状态和虚拟DOM结构,但并不会将其直接呈现给用户。

这个阶段的主要目的是为了构建应用程序的初始状态和组件结构,为后续的更新和渲染做好准备。在首次渲染阶段结束后,如果没有其他的操作,用户是看不到任何页面内容的,因为虚拟DOM树并没有被实际渲染到页面上。渲染的实际操作将在后续的渲染阶段(Render阶段)和提交阶段(Commit阶段)中进行。

Render(渲染):

Reconciliation(协调):

在组件状态发生变化或者props更新时,React会进入渲染阶段。在这个阶段,React会进行协调(Reconciliation)操作,比较前后两次渲染的结果,找出需要更新的部分。这个阶段也包括了生命周期函数的调用和函数组件的执行。

在React的渲染阶段,当组件的状态发生变化或者props更新时,React会执行重新渲染(reconciliation)操作。这个阶段的主要任务是比较前后两次渲染的结果,找出需要更新的部分,然后进行相应的更新。这种比较和更新的过程是为了确保虚拟DOM树与实际DOM树保持同步,以便正确地反映应用程序的状态。

在渲染阶段,React会执行以下任务:

  • 协调(Reconciliation):React使用协调算法比较前后两次渲染的虚拟DOM树,找出需要更新的部分。这个过程包括了diff算法的执行,用于确定哪些部分需要被更新,哪些部分保持不变。
  • 生命周期函数的调用:如果组件定义了生命周期函数(如componentDidMount、componentDidUpdate等),这些函数将在相应的时机被调用。这些生命周期函数提供了在组件不同阶段执行特定逻辑的机会。
  • 函数组件的执行:对于函数组件,React会执行函数组件的函数体,获取其返回的虚拟DOM树。函数组件是无状态的,所以在每次渲染时都会完全执行函数体,以获取最新的虚拟DOM树。

协调(Reconciliation)是 React 中一种用于处理组件更新的算法。在组件状态或者 props 发生变化时,React 会执行协调算法,比较前后两次渲染的虚拟 DOM 树(或者更准确地说是 Fiber 树),找出需要更新的部分。这个过程确保了 React 仅更新必要的部分,而不是重新渲染整个页面,从而提高性能。

具体来说,协调过程包括以下几个步骤:

  • Diff 算法的执行:React 使用 Diff 算法(也称为协调算法)来比较前后两棵树的节点,确定哪些节点需要被更新、插入或删除。Diff 算法尝试最小化变更操作,以减少更新的开销。
  • 确定需要更新的部分:Diff 算法比较两棵树的节点,找出变化的部分。这可能涉及到节点的属性变化、文本内容的改变等。React 会将这些变化标记为需要更新的部分。
  • 保持不变的部分:如果某个节点在两次渲染中保持不变(即节点的类型、属性等没有发生变化),React 会保留这部分节点,避免不必要的重新渲染。这一步是为了优化性能,避免不必要的工作。
  • 递归处理子节点:协调算法会递归地处理组件的子节点,确保整棵树都被比较和更新。这个过程保证了组件树的一致性。

总的来说,协调算法的目标是在保持用户界面一致性的前提下,尽量减少更新的操作,提高渲染性能。React 的 Fiber 架构通过引入可中断的渲染过程,使得协调算法更加灵活,可以更好地适应现代应用程序的需求。

函数组件(Functional Components)是一种在React中定义组件的方式,它是一种无状态的组件,通常由一个函数来表示。当函数组件被渲染时,React会执行该函数体,获取函数的返回值,该返回值通常是一个虚拟DOM树(也叫React元素)。以下是函数组件的基本结构:

function MyFunctionalComponent(props) {
  // 函数体逻辑
  return <div>Hello, {props.name}!</div>;
}

在函数组件的执行过程中,React会传递props作为参数,并执行函数体内的逻辑。函数组件内部没有实例,因此它不具备生命周期方法(如componentDidMount等),也没有内部状态(state)。每次函数组件被渲染时,都会重新执行函数体,以获取最新的虚拟DOM树。当函数组件被用于React元素树的渲染时,React会执行函数组件的函数体,获取返回的虚拟DOM树,并将其插入到组件树中相应的位置。这样,函数组件就被成功地渲染到了页面上。

总之,在渲染阶段,React会确保组件的状态和属性被正确地映射到虚拟DOM树上,并且执行相应的生命周期函数和函数组件,以便最终更新实际的DOM树。

Commit(提交):

提交阶段: 在协调阶段完成后,React会进入提交阶段。在这个阶段,React会将更新的结果(即需要变更的部分)实际渲染到DOM上。这个阶段包括了DOM操作和副作用的执行,例如componentDidMount和useEffect的回调函数。

需要注意的是,React Fiber架构的灵活性允许React在这些阶段中根据需要中断、恢复、或者重新安排任务的执行顺序,从而提高了React应用程序的性能和用户体验。这种灵活性是Fiber架构的一个重要特点。

在React的提交阶段,经过协调阶段的比较和计算,React确定了需要进行变更的部分,然后将这些变更实际渲染到DOM上。这个阶段主要包括了以下几个任务:

  • DOM操作:在提交阶段,React会执行实际的DOM操作,包括插入、更新、删除等。这些操作是根据前面协调阶段的比较结果来执行的,确保DOM与虚拟DOM保持同步。

在React的提交阶段,React会将在协调阶段确定的需要更新的部分实际渲染到DOM上。这个阶段包括了插入新元素、更新已有元素和删除不再需要的元素等DOM操作。这些操作是根据前面协调阶段的比较结果来执行的,确保实际DOM与虚拟DOM(即React内部的Fiber树)保持同步。这些DOM操作确保了用户界面的变化能够正确地呈现在页面上。React会在这个阶段处理所有的副作用,比如组件的componentDidMount和componentDidUpdate生命周期函数,以及函数组件中的useEffect中定义的副作用函数。所有这些操作都是在提交阶段完成的,以确保在这个阶段DOM的状态是最新的,也是最终的用户界面呈现状态。

  • 副作用的执行:副作用(side effects)是指那些不影响渲染输出的操作,比如数据的获取、订阅、手动DOM操作等。在React中,副作用通常在生命周期函数(如componentDidMount、componentDidUpdate)和Hook(如useEffect)中执行。在提交阶段,React会执行这些副作用操作,确保它们在正确的时机被调用。

副作用(side effects)指的是那些不直接影响 React 渲染输出的操作。在 React 组件的生命周期函数(如componentDidMount、componentDidUpdate)和函数式组件中的 Hook(如useEffect)中,经常会包含一些副作用操作。这些副作用可能包括但不限于:

数据获取(Data Fetching):从服务器或其他数据源获取数据。

数据订阅(Subscriptions):订阅外部事件或数据源的更新。

手动 DOM 操作(Manual DOM Manipulation):直接操作页面上的 DOM 元素,例如改变样式、插入或删除元素等。

定时器(Timers):设置定时器,在一定时间后执行某个操作。

在 React 的提交阶段,React 会处理这些副作用。具体地,在函数组件中,副作用函数(由useEffect定义)会在提交阶段执行。在类组件中,componentDidMount和componentDidUpdate生命周期函数中的代码也会在这个阶段执行。

执行这些副作用的时机非常关键。例如,在组件渲染后,我们可能需要在 DOM 中插入一些元素,或者在数据获取后更新组件的状态。这些操作需要在 DOM 已经准备好、React 内部状态已经更新的时候执行,以确保用户界面的一致性和正确性。

React 的副作用机制(特别是useEffect)也帮助开发者管理和清理副作用,防止内存泄漏和其他潜在的问题。在副作用函数中,通常会返回一个清理函数,该函数用于在组件卸载或下一次副作用触发时清理之前的副作用。这确保了副作用的可控性和安全性。

  • Effect的清除:在提交阶段,React还会处理Effect的清除操作。如果之前的Effect在当前渲染周期中被清除(例如组件卸载时),React会在提交阶段执行清除操作,以确保不再需要的副作用得以清理。

在React中,Effect是在函数组件中执行副作用操作的机制,它主要是通过Hook(如useEffect)或类组件的生命周期函数(如componentDidMount、componentDidUpdate)来实现的。

在组件的生命周期中,可能会创建一些副作用操作,比如订阅外部数据源、设置定时器、手动操作DOM等。这些副作用操作在组件卸载或者依赖发生变化时需要被清除,以避免潜在的内存泄漏或其他不良影响。

当组件被卸载时,或者Effect的依赖发生变化时(在依赖数组中的值发生变化时,useEffect会执行清除操作并重新执行),React会在提交阶段执行Effect的清除操作。这个清除操作可以包括取消订阅、清除定时器、解绑事件监听器等。通过清除Effect,React确保了在组件不再需要这些副作用时,它们会被及时地清理掉,以避免潜在的问题。

在React的函数组件中,可以返回一个清理函数,这个函数会在组件被销毁或者Effect的依赖项发生变化时被调用。在类组件中,componentWillUnmount生命周期函数用于处理组件的清理工作。这些清理操作通常在组件被卸载时执行,确保了组件的副作用得以清理,不会影响到其他组件或应用的运行。

总之,在提交阶段,React会将之前协调阶段计算出的变更实际应用到DOM上,同时执行副作用操作,确保组件的渲染和逻辑都按照预期进行。