likes
comments
collection
share

🔥这些React的核心要点你一定要懂|我从《React技术揭秘》中学到了什么

作者站长头像
站长
· 阅读数 10

第一章 React理念

Fiber:一种将长任务分解为多个任务片段的架构

fiber 是什么

Fiber是一种架构,用于将长任务切分为多个任务片段。它在React 16中实现了异步可中断的更新。在React 15中,Reconciler采用了递归方式创建虚拟DOM,该递归过程无法中断。如果组件树层级很深,递归会占用大量时间,导致卡顿。因此,React 15中的递归虚拟DOM无法满足需求。

fiber 能干什么

在Fiber架构中,每个任务片段对应一个React元素,并保存其相关信息。引入Fiber可以实现异步可中断的更新,解决了先前递归方式的性能限制。任务按优先级划分,高优先级任务可以抢占当前任务,当前任务将被中断。由于执行是在内存中的虚拟DOM上进行的,因此不会将更新内容显示在页面上。调度器负责分配任务优先级,高优先级任务优先进入协调器。

三个阶段:调度器、协调器、渲染器

Fiber架构由三个主要阶段组成:调度器、协调器和渲染器。每个阶段执行特定的任务,以实现高效和响应式的更新。

  1. 调度器:调度器负责确定任务的执行顺序。它根据任务的优先级确定执行顺序,高优先级任务首先进入协调器。
  2. 协调器:协调器是识别发生变化的组件的协调者。它构建了两棵树:当前树和工作进行中树。当前树由主节点的current指针确定,而工作进行中树表示更新后的状态。工作进行中树替换当前树,只包含当前树中发生变化的部分。
  3. 渲染器:渲染器负责将更新后的组件渲染到实际的DOM上。它接收协调器计算的变更,并将其应用于DOM,完成更新过程。

工作阶段:渲染和提交

React的工作分为两个主要阶段:渲染阶段和提交阶段。

  1. 渲染阶段:在渲染阶段,React构建和更新组件的虚拟DOM。该阶段计算组件的变化和更新操作。
  2. 提交阶段:在提交阶段,React将在渲染阶段计算得到的变更应用于实际的DOM,从而实现页面的视觉更新。

需要注意的是,渲染阶段和提交阶段并不严格分割,它们可能交替执行。React采用异步渲染和调度策略来提高性能和用户体验,尽可能将工作分散到多个帧中执行,避免阻塞主线程。

结论

引入Fiber架构后,React的性能和响应性得到了显著改进。通过将任务切分为较小的片段并实现异步处理,React能够处理复杂的组件树,而不会导致显著的延迟或界面冻结。调度器、协调器和渲染器协同工作,使React能够高效地更新虚拟DOM并将变更应用于实际DOM。

react的双缓存机制

在 React 中,双缓存机制是指在更新组件时使用两个 Fiber 节点来交替存储组件树的状态和布局信息,以实现高效的更新和渲染过程。

React 的双缓存机制基于 Fiber 架构实现,它的基本工作流程如下:

  1. 初始渲染:首次渲染组件时,创建两个 Fiber 节点,分别称为当前 Fiber 和工作 In-Progress Fiber。当前 Fiber 存储当前显示的组件树的状态和布局信息,而工作 In-Progress Fiber 用于计算和处理下一次更新的组件树。
  2. 组件更新:当发生组件状态的更新时,React 使用协调过程(Reconciliation)生成一个新的组件树。这个协调过程在工作 In-Progress Fiber 上进行,通过对比前后两棵树的差异,确定需要进行哪些更新操作。
  3. 构建更新任务:在协调过程中,React 根据组件更新的优先级和调度策略,构建更新任务队列。这些任务描述了需要进行的具体更新操作,包括更新组件的状态、生成新的 Fiber 节点等。
  4. 提交更新:当所有的更新任务都构建完成后,React 开始执行提交阶段。在这个阶段,React 会将更新任务应用到当前 Fiber 中,更新组件的状态和布局信息。
  5. 交替切换:当提交阶段完成后,当前 Fiber 和工作 In-Progress Fiber 会发生交替切换。原先的当前 Fiber 成为下一次更新的工作 In-Progress Fiber,而原先的工作 In-Progress Fiber 则成为下一次更新时的当前 Fiber。

第二章 前置知识

调度器、协调器和渲染器

在React中,存在三个关键角色:调度器(Scheduler)、协调器(Reconciler)和渲染器(Renderer)。

  1. 调度器(Scheduler):负责调度任务的优先级,高优先级任务优先进入协调器。它确定任务的执行顺序,确保高优先级任务能够得到及时处理。
  2. 协调器(Reconciler):负责找出发生变化的组件。协调器通过对比前后状态的差异,确定哪些组件需要进行更新操作。
  3. 渲染器(Renderer):负责将发生变化的组件渲染到页面上。渲染器根据协调器提供的变更信息,将变化应用于实际的DOM,完成页面的更新。

react目录结构和关键文件夹

在React的目录结构中,以下文件夹值得关注:

  1. react:包含了React的核心代码和所有全局API。
  2. scheduler:实现了调度器的相关代码。
  3. shared:包含了一些公用方法和全局变量。
  4. render:包含了与渲染相关的代码,如react-art和react-dom。
  5. 辅助包:包括了一些辅助工具库,如react-is等。
  6. react-reconciler:它是连接调度器和不同平台渲染器的桥梁,构成了整个React 16的架构体系。

JSX和Fiber节点的关系

JSX和Fiber节点不是同一个东西。

JSX在编译时通过Babel转译为一个对象,用于描述当前组件内容的数据结构。它不包含组件调度、协调和渲染所需的相关信息。例如,JSX中不包含组件在更新中的优先级、组件的状态以及用于渲染器的标记等信息。而这些内容都包含在Fiber节点中。

在组件挂载时,协调器根据JSX描述的组件内容生成对应的Fiber节点。因此,Fiber节点包含了JSX所具有的信息,但JSX本身并不包含Fiber节点所具有的一些信息。

React组件、React元素和JSX的关系

在React中,JSX运行时的返回结果都是React元素(React Element)。JSX运行时会调用createElement方法,该方法返回一个React元素对象。React元素对象是一个包含各种描述组件及其属性的字段的对象。其中,$$typeof属性的值为REACT_ELEMENT_TYPE,用于标识它是一个React元素。

React组件(React Component)对应的元素的type字段为组件自身。例如,如果React组件的名称是AppClass,那么对

应的元素的type就是AppClass

以上是关于调度器、协调器、渲染器以及JSX和Fiber节点的相关知识。

第三章 Render阶段

流程

Render阶段采用先递后归、深度优先遍历的方式进行。

function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

在Render阶段的执行顺序如下:

  1. rootFiber执行beginWork。
  2. App Fiber执行beginWork。
  3. div Fiber执行beginWork。
  4. "i am" Fiber执行beginWork。
  5. "i am" Fiber执行completeWork。
  6. span Fiber执行beginWork。
  7. span Fiber执行completeWork。
  8. div Fiber执行completeWork。
  9. App Fiber执行completeWork。
  10. rootFiber执行completeWork。

之所以没有 "KaSong" Fiber 的beginWork/completeWork,是因为针对只有单一文本子节点的Fiber,React会进行特殊处理,作为性能优化的手段。

beginWork

beginWork是在递阶段执行的函数,它有三个参数。我们先讨论前两个参数:

  1. current:上一次更新时与当前组件对应的Fiber节点,即workInProgress.alternate
  2. workInProgress:当前组件对应的Fiber节点。

beginWork函数首先判断current是否为空。如果为空,则执行mount操作。执行mountChildFibers函数。

如果current不为空,则执行update操作。它会判断之前的current节点是否可以复用。如果可以复用,判断子树是否需要更新。如果需要更新,返回新的子树;如果不需要更新,则复用原子树。

如果current节点不可复用,则执行reconcileChildren函数,进行差异比较的算法。然后生成新的子Fiber节点,并将其赋值给workInProgress.child,作为本次beginWork的返回值,并作为下次performUnitOfWork执行时workInProgress的参数。

值得注意的是,mountChildFibersreconcileChildFibers这两个函数的逻辑基本相同,唯一的区别是:reconcileChildFibers会为生成的Fiber节点添加effectTag属性,而mountChildFibers则不会。

effectTag是用于标记当前节点对应的DOM操作的标记。为什么mountChildFibers函数不添加effectTag呢?这主要是为了节约资源。如果mountChildFibers也添加了effectTag,那么可以预见在mount阶段,整个Fiber树的所有节点都会有Placement类型的effectTag。这样,在commit阶段执行DOM操作时,每个节点都会执行一次插入操作,效率非常低下。

completeWork

completeWork函数是根据不同的fiber.tag调用不同的处理逻辑。

我们重点关注的是与页面渲染所必需的HostComponent(即原生DOM组件)对应

的Fiber节点。

根据current === null判断是mount还是update操作。

在update操作时,会考虑workInProgress.stateNode !== null,即该Fiber节点是否存在对应的DOM节点。

然后处理props,例如处理onClickonChange等回调函数的注册,处理style属性,处理DANGEROUSLY_SET_INNER_HTML属性,处理children属性。处理完的props会被赋值给workInProgress.updateQueue,最终会在commit阶段渲染到页面上。

在mount操作时,会生成Fiber节点对应的DOM节点,并将子孙DOM节点插入到新生成的DOM节点中。与update逻辑中的updateHostComponent类似地处理props过程。

在mount阶段,只有rootFiber存在Placement类型的effectTag。这样,在commit阶段中通过一次插入DOM操作(对应一个Placement类型的effectTag)将整个DOM树插入到页面中。

在commit阶段,会找到所有有effectTag的节点进行渲染。

completeUnitOfWork函数的上层函数completeWork中,每个执行完completeWork且存在effectTag的Fiber节点都会被保存在一个称为effectList的单向链表中。

effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个Fiber节点保存在fiber.lastEffect

在归阶段,所有有effectTag的Fiber节点都会被追加到effectList中,最终形成以rootFiber.firstEffect为起点的单向链表。

然后,Render阶段完成。

performSyncWorkOnRoot函数中,将fiberRootNode传递给commitRoot方法,启动commit阶段的工作流程。

第四章 Commit阶段

Commit阶段是将DOM渲染到页面的阶段。

大体分为三个阶段:

  1. before mutation阶段(执行DOM操作之前)
  2. mutation阶段(执行DOM操作)
  3. layout阶段(执行DOM操作之后)

在before mutation阶段,主要进行变量赋值和状态重置的工作。然后遍历effectList,依次调用commitBeforeMutationEffects函数进行处理。

effectList是由链表实现的,用于存储组件中的副作用。每个副作用表示为一个节点,包含以下属性:

  • tag:标识副作用的类型,如 "Hook"、"Placement"等。
  • create:一个函数,用于创建副作用。在组件实例化或更新时调用。
  • destroy:一个函数,用于清除副作用。在组件卸载时调用。
  • deps:依赖项数组,用于控制副作用的触发时机。

在before mutation阶段,遍历effectList,依次执行以下操作:

  • 处理DOM节点

渲染或删除后的autoFocusblur逻辑

  • 调用getSnapshotBeforeUpdate生命周期钩子
  • 调度useEffect

getSnapshotBeforeUpdate方法用于在更新之前获取组件的某些信息,例如滚动位置、DOM元素的尺寸或其他与DOM相关的数据。可以在getSnapshotBeforeUpdate方法中进行计算或查询,以便在更新后能够恢复到相应的状态。

在mutation阶段,遍历effectList,依次执行commitMutationEffects方法。该方法的主要工作是根据effectTag调用不同的处理函数处理Fiber节点。

在React中,effectTag是一个标记,用于表示组件副作用的类型。每个组件实例都有一个effectTag字段,它指示React在更新时应该执行哪些副作用操作。

常见的effectTag类型有:

  1. Placement:表示需要将组件添加到DOM中。在组件实例化或更新时,如果需要将其插入到DOM中,React会使用Placement标记。
  2. Update:表示需要更新组件。当组件的属性或状态发生变化时,React会使用Update标记,表示需要更新组件的内容。
  3. Deletion:表示需要从DOM中删除组件。在组件卸载时,React会使用Deletion标记,表示需要将组件从DOM中移除。
  4. Snapshot:表示需要获取组件更新前的快照。在组件更新时,如果需要获取更新前的某些信息(如滚动位置、DOM尺寸等),React会使用Snapshot标记,以便在更新后能够恢复到相应的状态。
  5. Passive:表示副作用是被动的,不会触发任何更改。这通常用于一些被动的副作用,如订阅事件或监听滚动等,不会导致组件内容的变化。

这些effectTag类型在React内部用于标识和处理组件的不同副作用,以便在适当的时机执行相应的操作。通常不需要直接操作这些标记,而是由React底层管理和处理。可以使用React提供的Hook(如useEffectuseLayoutEffect)来定义和管理组件的副作用,React会根据effectTag的类型来调度和执行相应的副作用操作。

在layout阶段,遍历effectList,依次执行commitLayoutEffects方法。该方法的主要工作是根据effectTag调用不同的处理函数处理Fiber节点并更新ref

第五章 Diff算法

单节点Diff

在进行Diff算法时,首先对单个节点进行比较。

  1. 首先判断节点的key,然后再判断type。如果keytype都没有变化,就更新子节点。

多节点Diff

多节点Diff可以分为三种情况:

  1. 节点更新
  2. 新增或删除节点
  3. 节点位置变化

在日常开发中,相对于新增和删除,更新组件的频率更高。因此,Diff算法会首先判断当前节点是否需要更新。

Diff算法的整体逻辑包括两轮遍历:

第一轮遍历:处理需要更新的节点。 第二轮遍历:处理剩余的不需要更新的节点。

第一轮遍历newChildren,将newChildren[i]oldFiber进行比较,判断是否可以复用DOM节点。 如果可以复用,则直接使用。 如果无法复用,有两种情况:

  • key不同导致无法复用,立即跳出整个遍历,第一轮遍历结束。
  • key相同但type不同导致无法复用,将oldFiber标记为DELETION,并继续遍历。

如果newChildren遍历完(即i === newChildren.length - 1),或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。

第二轮遍历:(第二轮遍历可能在第一轮中途跳出)

  1. newChildrenoldFiber同时遍历完时,无需进一步处理,直接结束。
  2. newChildren未遍历完但oldFiber遍历完时,表示存在新增节点,插入新节点,并依次遍历剩余的newChildren,为生成的workInProgress fiber节点依次标记为Placement
  3. newChildren遍历完但oldFiber未遍历完时,遍历剩余的oldFiber,依次标记为Deletion
  4. newChildrenoldFiber都未遍历完时,涉及到节点位置的处理。需要找到移动的节点,此时我们需要参照最后一个可复用节点在oldFiber中的位置索引。

在寻找位置时,如果oldIndex >= lastPlacedIndex,表示该节点可以复用,无需移动。 如果oldIndex < lastPlacedIndex,表示oldIndex小于最后一个可复用节点的索引,在新列表中,该可复用节点需要向后移动。

我们可能认为从 "abcd" 变为 "dabc",只需要将"d"移动到前面。

但实际上,React保持"d"不变,将"abc"

分别移动到"d"的后面。

从这点可以看出,在考虑性能的情况下,我们应尽量减少将节点从后面移动到前面的操作。

第六章 状态更新

Update

在 React 中,每次状态更新都会创建一个保存更新状态相关内容的对象,我们称之为 "Update"。这个 Update 对象包含了组件状态的改变信息以及与之关联的其他属性。

Update 对象的主要属性包括:

  1. State:表示组件状态的改变,即更新后的状态值。它可以是基本类型(如数字、字符串)或复杂类型(如对象、数组)。
  2. Callback:表示在状态更新完成后需要执行的回调函数。通常用于处理状态更新的副作用或执行其他逻辑操作。
  3. Next:表示下一个 Update 对象,形成一个链表结构。当组件有多个连续的状态更新时,每个 Update 对象都会指向下一个 Update 对象,以便按顺序依次处理更新操作。
  4. Expiration Time:表示 Update 对象的过期时间。React 使用时间片(time slice)和优先级调度来控制更新的执行顺序,Expiration Time 用于确定更新的优先级,以便在合适的时间执行。
  5. Tag:表示 Update 的类型,可以是同步更新、批量更新、回调更新等。不同类型的 Update 会在协调过程中采取不同的处理方式。

当组件发生状态更新时,React 会根据新的状态值和其他相关信息创建一个 Update 对象,并将它添加到组件的更新队列中。在协调过程中,React 会遍历更新队列,并根据 Update 对象的内容执行相应的更新操作,如更新组件状态、生成新的 Fiber 节点等。

优先级

React 任务的优先级顺序如下:

  1. 高优先级任务(High Priority):包括用户交互、动画效果等需要立即响应的任务。这些任务被分配到较高优先级的 Lanes 中。
  2. 普通优先级任务(Normal Priority):包括一般的状态更新和渲染任务。这些任务被分配到中等优先级的 Lanes 中。
  3. 低优先级任务(Low Priority):包括一些不紧急的任务,如预加载、缓存更新等。这些任务被分配到较低优先级的 Lanes 中。

第七章 Hooks

function App() {
  const [num, updateNum] = useState(0);

  return <p onClick={() => updateNum(num => num + 1)}>{num}</p>;
}

// 这个例子,当点击p标签时,组件会产生更新,更新会造成组件render。
// 组件render时useState返回的num为更新后的结果。

更新的数据结构是这样,
const update = {
  // 更新执行的函数
  action,
  // 与同一个Hook的其他更新形成链表
  next: null
}

多个update会形成环状单向链表。 update对象会保存在queue中。 ClassComponent可以保存queue。 FunctionComponent不存储数据,其queue保存在 FunctionComponent对应的fiber中。 Hook与update类似,都通过链表连接。不过Hook是无环的单向链表。在React中,update和hook之间存在一种特殊的所属关系。每个hook对象都可以有一个关联的update队列,用于存储对该hook的更新操作。

Hooks结构

hook与FunctionComponent fiber都存在memoizedState属性,他们的不同是什么

总结:

  • hookmemoizedState属性用于存储Hook对象的状态值。当我们调用Hook函数(例如useState)时,React会为每个Hook创建一个对应的hook对象,并将初始状态值存储在memoizedState中。在后续的组件渲染过程中,memoizedState会被更新为最新的状态值,并在组件重新渲染时提供给组件使用。
  • FunctionComponent Fiber的memoizedState属性用于存储组件实例对象或其他相关信息,以便在组件的多次渲染之间保持上下文。

在App内部,调用updateNum会触发一次更新。如果不对这种情况下触发的更新作出限制,那么这次更新会开启一次新的render阶段,最终会无限循环更新。

function useState(initialState) {
  let hook;

  // 判断是否首次渲染
  if (isMount) {
    // 创建新的hook对象
    hook = {
      queue: { pending: null },
      memoizedState: initialState,
      next: null
    };

    // 将hook对象添加到链表中
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook;
    } else {
      workInProgressHook.next = hook;
    }
    workInProgressHook = hook;
  } else {
    // 非首次渲染时,获取当前的hook对象并更新workInProgressHook
    hook = workInProgressHook;
    workInProgressHook = workInProgressHook.next;
  }

  let baseState = hook.memoizedState;

  // 处理队列中的更新操作
  if (hook.queue.pending) {
    let firstUpdate = hook.queue.pending.next;

    // 遍历队列中的更新操作
    do {
      const action = firstUpdate.action;
      baseState = action(baseState);
      firstUpdate = firstUpdate.next;
   

 } while (firstUpdate !== hook.queue.pending.next)

    // 将pending置为null表示更新操作已处理完
    hook.queue.pending = null;
  }

  // 更新memoizedState为最新的状态值
  hook.memoizedState = baseState;

  // 返回状态值和dispatchAction函数
  return [baseState, dispatchAction.bind(null, hook.queue)];
}

useEffect 工作流程

组件渲染时,遇到 useEffect Hook。React 会记录该 Hook 的存在,并将其添加到组件的副作用列表中。

组件完成首次渲染后,React 会执行所有记录的副作用函数。这包括首次渲染时的副作用函数和之后可能触发的更新时的副作用函数。

在执行副作用函数之前,React 会执行一个额外的步骤,称为“清理阶段”。这个阶段的目的是处理上一次渲染的副作用函数。

清理阶段首先会检查是否存在上一次渲染的副作用函数。如果存在,则执行清理操作,将其移除。这是为了确保每次渲染都只执行最新的副作用函数。

执行完清理阶段后,React 开始执行当前渲染的副作用函数。在这个函数中,你可以执行一些具有副作用的操作,例如订阅事件、发送网络请求、操作 DOM 等。

当组件准备进行下一次渲染时,React 会再次执行清理阶段,以便处理上一次渲染的副作用函数。

useRef

useRef仅仅是返回一个包含current属性的对象。

ref的工作流程可以分为两部分:

  • Render阶段:为含有ref属性的fiber添加Ref effectTag
  • Commit阶段:对于包含Ref effectTag的fiber执行对应操作。

ref的工作流程。

对于FunctionComponentuseRef负责创建并返回对应的ref

对于赋值了ref属性的HostComponentClassComponent,会在Render阶段经历赋值Ref effectTag,在Commit阶段执行对应ref操作。

useMemo与useCallback实现

useMemo与useCallback在mount与update两种情况下分别执行不同的实现函数。 这两个hook实现的逻辑是:是在组件的初始渲染/更新阶段创建 memoized 值和 memoized 回调函数,并在之后的渲染中复用它们。通过将值和依赖保存在 hook.memoizedState 中,可以在后续的渲染中比较依赖是否发生变化,从而决定是否重新计算值或创建新的回调函数。这样可以有效地优化组件的性能。

第八章 Concurrent Mode

Suspense

在 React 的 Suspense 组件内部,子组件树相对于组件树的其他部分具有较低的优先级。这意味着当组件树中的某个 Suspense 组件进入等待状态(例如等待异步数据加载)时,React 可以优先渲染其他不依赖于该 Suspense 组件的部分,以保持用户界面的响应性能。 当某个 Suspense 组件进入等待状态时,React 可以中断其子组件的渲染,转而渲染 Suspense 组件外的其他内容。这样可以确保用户界面仍然能够响应用户的交互,而不会出现长时间的卡顿或无响应的情况。一旦 Suspense 组件内部的异步操作完成,React 将重新开始渲染被中断的子组件。

这种方式使得应用程序能够更好地处理异步操作或网络请求,同时保持用户界面的流畅性和可交互性。通过将较低优先级的任务延迟处理,React 可以更好地管理渲染过程,提高整体的用户体验。

Scheduler的原理与实现

Scheduler 是 React 内部用于任务调度和执行的模块,采用协作式调度的方式。它使用时间片划分任务执行的时间段,每个时间片有固定时长。任务被分配到优先级级别的 Lane 中,Lanes 可根据任务类型和优先级分类。Scheduler 根据任务的优先级和时间片进行调度,维护多个任务队列,每个队列对应一个 Lane。在时间片结束时,中断当前任务,检查是否有更高优先级任务。中断和恢复过程保证任务的优先级处理和切换。Scheduler 在每个时间片内,按优先级选择下一个任务执行。执行任务后,根据时间片和优先级决定是否继续执行下一个任务。通过调度和中断恢复,Scheduler实现任务的优先级处理和时间片控制,平衡不同优先级任务,提供良好的用户体验和性能表现。

任务被中断后是如何重新启动?

shouldYieldtrue,意味着当前的渲染任务已经超过了时间片的限制,需要让出执行权以保持界面的响应性。

在 React 内部,使用了调度器(Scheduler)来管理任务的执行。当任务被中断时,调度器会安排下一个任务的执行,并将未完成的工作保存在调度器的任务队列中。

当再次分配时间片给 React 执行任务时,调度器会从任务队列中取出上次中断的工作单元(UnitOfWork),然后调用 performUnitOfWork 继续执行这个工作单元。

lane模型

可以将 Lane 模型看作是 React 的任务优先级的表示方式,而 Scheduler 则是具体实现了任务调度和执行的算法和机制。Lane 模型提供了一种灵活的优先级分类方式,Scheduler 则利用 Lane 模型来进行任务调度和处理,以保证 React 的更新过程能够高效、有序地进行。

综上所述,Lane 模型是 React 的优先级模型中的一部分,而 Scheduler 是 React 内部的调度器,负责根据 Lane 模型来进行任务的优先级调度和执行。