React Fiber 源码解析

图片做者:Artem Sapegin,来源:unsplash.com/photos/b18T…javascript

本文做者:刘鹏前端

前言

在 React v16.13 版本中,正式推出了实验性的 Concurrent Mode,尤为是提供一种新的机制 Suspense,很是天然地解决了一直以来存在的异步反作用问题。结合前面 v16.8 推出的 Hooks,v16.0 底层架构 Fiber,React 给开发者体验上带来了极大提高以及必定程度上更佳的用户体验。因此,对 React 17,你会有什么期待?java

Stack Reconciler 和  Fiber Reconciler

咱们知道,Stack Reconciler 是 React v15 及以前版本使用的协调算法。而 React Fiber 则是从 v16 版本开始对 Stack Reconciler 进行的重写,是 v16 版本的核心算法实现。 Stack Reconciler 的实现使用了同步递归模型,该模型依赖于内置堆栈来遍历。React 团队 Andrew 以前有提到:node

若是只依赖内置调用堆栈,那么它将一直工做,直到堆栈为空,若是咱们能够随意中断调用堆栈并手动操做堆栈帧,这不是很好吗? 这就是 React Fiber 的目标。Fiber 是内置堆栈的从新实现,专门用于 React 组件,能够将一个 fiber 看做是一个虚拟堆栈帧。react

正是因为其内置 Stack Reconciler 天生带来的局限性,使得 DOM 更新过程是同步的。也就是说,在虚拟 DOM 的比对过程当中,若是发现一个元素实例有更新,则会当即同步执行操做,提交到真实 DOM 的更改。这在动画、布局以及手势等领域,可能会带来很是糟糕的用户体验。所以,为了解决这个问题,React 实现了一个虚拟堆栈帧。实际上,这个所谓的虚拟堆栈帧本质上是创建了多个包含节点和指针的链表数据结构。每个节点就是一个 fiber 基本单元,这个对象存储了必定的组件相关的数据域信息。而指针的指向,则是串联起整个 fibers 树。从新自定义堆栈带来显而易见的优势是,能够将堆栈保留在内存中,在须要执行的时候执行它们,这使得暂停遍历和中止堆栈递归成为可能。git

Fiber 的主要目标是实现虚拟 DOM 的增量渲染,可以将渲染工做拆分红块并将其分散到多个帧的能力。在新的更新到来时,可以暂停、停止和复用工做,能为不一样类型的更新分配优先级顺序的能力。理解 React 运行机制对咱们更好理解它的设计思想以及后续版本新增特性,好比 v17 版本可能带来的异步渲染能力,相信会有很好的帮助。本文基于 React v16.8.6 版本源码,输出一些浅见,但愿对你也有帮助,若有不对,还望指正。github

基础概念

在了解 React Fiber 架构的实现机制以前,有必要先把几个主要的基础概念抛出来,以便于咱们更好地理解。算法

Work

在 React Reconciliation 过程当中出现的各类必须执行计算的活动,好比 state update,props update 或 refs update 等,这些活动咱们能够统一称之为 work。安全

Fiber 对象

文件位置:packages/react-reconciler/src/ReactFiber.jsmarkdown

每个 React 元素对应一个 fiber 对象,一个 fiber 对象一般是表征 work 的一个基本单元。fiber 对象有几个属性,这些属性指向其余 fiber 对象。

  • child: 对应于父 fiber 节点的子 fiber
  • sibling: 对应于 fiber 节点的同类兄弟节点
  • return: 对应于子 fiber 节点的父节点

所以 fibers 能够理解为是一个包含 React 元素上下文信息的数据域节点,以及由 child, sibling 和 return 等指针域构成的链表结构。

fiber 对象主要的属性以下所示:

Fiber = {
    // 标识 fiber 类型的标签,详情参看下述 WorkTag
    tag: WorkTag,

    // 指向父节点
    return: Fiber | null,

    // 指向子节点
    child: Fiber | null,

    // 指向兄弟节点
    sibling: Fiber | null,

    // 在开始执行时设置 props 值
    pendingProps: any,

    // 在结束时设置的 props 值
    memoizedProps: any,

    // 当前 state
    memoizedState: any,

    // Effect 类型,详情查看如下 effectTag
    effectTag: SideEffectTag,

    // effect 节点指针,指向下一个 effect
    nextEffect: Fiber | null,

    // effect list 是单向链表,第一个 effect
    firstEffect: Fiber | null,

    // effect list 是单向链表,最后一个 effect
    lastEffect: Fiber | null,

    // work 的过时时间,可用于标识一个 work 优先级顺序
    expirationTime: ExpirationTime,
};
复制代码

从 React 元素建立一个 fiber 对象

文件位置:react-reconciler/src/ReactFiber.js

export function createFiberFromElement( element: ReactElement, mode: TypeOfMode, expirationTime: ExpirationTime ): Fiber {
    const fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, expirationTime);
    return fiber;
}
复制代码

workTag

文件位置:shared/ReactWorkTags.js

上述 fiber 对象的 tag 属性值,称做 workTag,用于标识一个 React 元素的类型,以下所示:

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedSuspenseComponent = 18;
export const EventComponent = 19;
export const EventTarget = 20;
export const SuspenseListComponent = 21;
复制代码

EffectTag

文件位置:shared/ReactSideEffectTags.js

上述 fiber 对象的 effectTag 属性值,每个 fiber 节点都有一个和它相关联的 effectTag 值。
咱们把不能在 render 阶段完成的一些 work 称之为反作用,React 罗列了可能存在的各种反作用,以下所示:

export const NoEffect = /* */ 0b000000000000;
export const PerformedWork = /* */ 0b000000000001;

export const Placement = /* */ 0b000000000010;
export const Update = /* */ 0b000000000100;
export const PlacementAndUpdate = /* */ 0b000000000110;
export const Deletion = /* */ 0b000000001000;
export const ContentReset = /* */ 0b000000010000;
export const Callback = /* */ 0b000000100000;
export const DidCapture = /* */ 0b000001000000;
export const Ref = /* */ 0b000010000000;
export const Snapshot = /* */ 0b000100000000;
export const Passive = /* */ 0b001000000000;

export const LifecycleEffectMask = /* */ 0b001110100100;
export const HostEffectMask = /* */ 0b001111111111;

export const Incomplete = /* */ 0b010000000000;
export const ShouldCapture = /* */ 0b100000000000;
复制代码

Reconciliation 和 Scheduling

协调(Reconciliation):
简而言之,根据 diff 算法来比较虚拟 DOM,从而能够确认哪些部分的 React 元素须要更改。

调度(Scheduling):
能够简单理解为是一个肯定在何时执行 work 的过程。

Render 阶段和 Commit 阶段

相信不少同窗都看过这张图,这是 React 团队做者 Dan Abramov 画的一张生命周期阶段图,详情点击查看。他把 React 的生命周期主要分为两个阶段:render 阶段和 commit 阶段。其中 commit 阶段又能够细分为 pre-commit 阶段和 commit 阶段,以下图所示:

image.png

从 v16.3 版本开始,在 render 阶段,如下几个生命周期被认为是不安全的,它们将在将来的版本中被移除,能够看到这些生命周期在上图中未被包括进去,以下所示:

  • [UNSAFE_]componentWillMount (deprecated)
  • [UNSAFE_]componentWillReceiveProps (deprecated)
  • [UNSAFE_]componentWillUpdate (deprecated)

在 React 官网中明确提到了废弃的缘由,这些被标记为不安全的生命周期因为经常被开发者错误理解甚至被滥用,好比一些开发人员会倾向于将带有请求数据等反作用的逻辑放在这些生命周期方法中,认为能带来更好的性能,而实际上真正带来的收益几乎能够忽略。在将来, React 逐步推崇异步渲染模式下,这颇有可能会由于不兼容而带来不少问题。

在 render 阶段,React 能够根据当前可用的时间片处理一个或多个 fiber 节点,而且得益于 fiber 对象中存储的元素上下文信息以及指针域构成的链表结构,使其可以将执行到一半的工做保存在内存的链表中。当 React 中止并完成保存的工做后,让出时间片去处理一些其余优先级更高的事情。以后,在从新获取到可用的时间片后,它可以根据以前保存在内存的上下文信息经过快速遍历的方式找到中止的 fiber 节点并继续工做。因为在此阶段执行的工做并不会致使任何用户可见的更改,由于并无被提交到真实的 DOM。因此,咱们说是 fiber 让调度可以实现暂停、停止以及从新开始等增量渲染的能力。相反,在 commit 阶段,work 执行老是同步的,这是由于在此阶段执行的工做将致使用户可见的更改。这就是为何在 commit 阶段, React 须要一次性提交并完成这些工做的缘由。

Current 树和 WorkInProgress 树

首次渲染以后,React 会生成一个对应于 UI 渲染的 fiber 树,称之为 current 树。实际上,React 在调用生命周期钩子函数时就是经过判断是否存在 current 来区分什么时候执行 componentDidMount 和 componentDidUpdate。当 React 遍历 current 树时,它会为每个存在的 fiber 节点建立了一个替代节点,这些节点构成一个 workInProgress 树。后续全部发生 work 的地方都是在 workInProgress 树中执行,若是该树还未建立,则会建立一个 current 树的副本,做为 workInProgress 树。当 workInProgress 树被提交后将会在 commit 阶段的某一子阶段被替换成为 current 树。

这里增长两个树的主要缘由是为了不更新的丢失。好比,若是咱们只增长更新到 workInProgress 树,当 workInProgress 树经过从 current 树中克隆而从新开始时,一些更新可能会丢失。一样的,若是咱们只增长更新到 current 树,当 workInProgress 树被提交后会被替换为 current 树,更新也会被丢失。经过在两个队列都保持更新,能够确保更新始终是下一个 workInProgress 树的一部分。而且,由于 workInProgress 树被提交成为 current 树,并不会出现相同的更新而被重复应用两次的状况。

Effects list

effect list 能够理解为是一个存储 effectTag 反作用列表容器。它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect,和最后一个节点 lastEffect。以下图所示:

image.png

React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每个有反作用的 fiber 筛选出来,最后构建生成一个只带反作用的 effect list 链表。
在 commit 阶段,React 拿到 effect list 数据后,经过遍历 effect list,并根据每个 effect 节点的 effectTag 类型,从而对相应的 DOM 树执行更改。

更多 effect list 构建演示流程,能够点击查看动画 《Effect List —— 又一个 Fiber 链表的构建过程》

Render 阶段

在本文中,咱们以类组件为例,假设已经开始调用了一个 setState 方法。

enqueueSetState

每一个 React 组件都有一个相关联的 updater,做为组件层和核心库之间的桥梁。
react.Component 本质上就是一个函数,在它的原型对象上挂载了 setState 方法

文件位置:react/src/ReactBaseClasses.js

// Component函数
function Component(props, context, updater) {
    this.props = props;
    this.context = context;
    this.updater = updater || ReactNoopUpdateQueue;
}

// Component原型对象挂载 setState
Component.prototype.setState = function (partialState, callback) {
    this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码

React 给 work 大体分红如下几种优先级类型,其中 immediate 比较特殊,它的优先级最高,能够理解为是同步调度,调度过程当中不会被中断。

export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
复制代码

React 有一套计算逻辑,根据不一样的优先级类型为不一样的 work 计算出一个过时时间 expirationTime,其实就是一个时间戳。所谓的 React 在新的更新到来时,能为不一样类型的更新分配优先级顺序的能力,本质上是根据过时时间 expirationTime 的大小来肯定优先级顺序,expirationTime 数值越小,则优先级越高。在相差必定时间范围内的 work,React 会认为它们是同一个批次(batch)的,所以这一批次的 work 会在一次更新中完成。

文件位置:react-reconciler/src/ReactFiberClassComponent.js

const classComponentUpdater = {
    enqueueSetState(inst, payload, callback) {
        // 获取 fiber 对象
        const fiber = getInstance(inst);
        const currentTime = requestCurrentTime();

        // 计算到期时间 expirationTime
        const expirationTime = computeExpirationForFiber(currentTime, fiber, suspenseConfig);

        const update = createUpdate(expirationTime, suspenseConfig);
        // 插入 update 到队列
        enqueueUpdate(fiber, update);
        // 调度 work 方法
        scheduleWork(fiber, expirationTime);
    },
};
复制代码

renderRoot

文件位置:react-reconciler/src/ReactFiberWorkLoop.js

协调过程老是 renderRoot 开始,方法调用栈:scheduleWork -->  scheduleCallbackForRoot  --> renderRoot

代码以下:

function renderRoot( root: FiberRoot, expirationTime: ExpirationTime, isSync: boolean, ) | null {
  do {
    // 优先级最高,走同步分支
    if (isSync) {
      workLoopSync();
    } else {
      workLoop();
    }
  } while (true);
}

// 全部的fiber节点都在workLoop 中被处理
function workLoop() {
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}
复制代码

performUnitOfWork

全部的 fiber 节点都在 workLoop 方法处理。协调过程老是从最顶层的 hostRoot 节点开始进行 workInProgress 树的遍历。可是,React 会跳过已经处理过的 fiber 节点,直到找到还未完成工做的节点。例如,若是在组件树的深处调用 setState,React 将从顶部开始,但会快速跳过父节点,直到到达调用了 setState 方法的组件。整个过程采用的是深度优先搜索算法,处理完当前 fiber 节点后,workInProgress 将包含对树中下一个 fiber 节点的引用,若是下一个节点为 null 不存在,则认为执行结束退出 workLoop 循环并准备进行一次提交更改。

方法调用栈以下: performUnitOfWork  -->  beginWork -->  updateClassComponent --> finishedComponent --> completeUnitOfWork

代码以下所示:

文件位置:react-reconciler/src/ReactFiberWorkLoop.js

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
    const current = unitOfWork.alternate;

    let next;
    next = beginWork(current, unitOfWork, renderExpirationTime);

    // 若是没有新的 work,则认为已完成当前工做
    if (next === null) {
        next = completeUnitOfWork(unitOfWork);
    }

    return next;
}
复制代码

了解树的深度优先搜索算法,可点击参考该示例 《js-ntqfill》

completeUnitOfWork

文件位置:react-reconciler/src/completeUnitOfWork.js

在 completeUnitOfWork 方法中构建 effect-list 链表,该 effect list 在下一个 commit 阶段很是重要,关于 effect list 上述有介绍。

以下所示:

function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
    // 深度优先搜索算法
    workInProgress = unitOfWork;
    do {
        const current = workInProgress.alternate;
        const returnFiber = workInProgress.return;

        /* 构建 effect-list部分 */
        if (returnFiber.firstEffect === null) {
            returnFiber.firstEffect = workInProgress.firstEffect;
        }
        if (workInProgress.lastEffect !== null) {
            if (returnFiber.lastEffect !== null) {
                returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
            }
            returnFiber.lastEffect = workInProgress.lastEffect;
        }

        if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
        } else {
            returnFiber.firstEffect = workInProgress;
        }
        returnFiber.lastEffect = workInProgress;

        const siblingFiber = workInProgress.sibling;
        if (siblingFiber !== null) {
            // If there is more work to do in this returnFiber, do that next.
            return siblingFiber;
        }
        // Otherwise, return to the parent
        workInProgress = returnFiber;
    } while (workInProgress !== null);
}
复制代码

至此,一个 render 阶段大概流程结束。

Commit 阶段

commit 阶段是 React 更新真实 DOM 并调用 pre-commit phase 和 commit phase 生命周期方法的地方。与 render 阶段不一样,commit 阶段的执行始终是同步的,它将依赖上一个 render 阶段构建的 effect list 链表来完成。

commitRootImpl

commit 阶段实质上被分为以下三个子阶段:

  • before mutation
  • mutation phase
  • layout phase

mutation 阶段主要作的事情是遍历 effect-list 列表,拿到每个 effect 存储的信息,根据反作用类型 effectTag 执行相应的处理并提交更新到真正的 DOM。全部的 mutation effects 都会在 layout phase 阶段以前被处理。当该阶段执行结束时,workInProgress 树会被替换成 current 树。所以在 mutation phase 阶段以前的子阶段 before mutation,是调用 getSnapshotBeforeUpdate 生命周期的地方。在 before mutation 这个阶段,真正的 DOM 尚未被变动。最后一个子阶段是 layout phase,在这个阶段生命周期 componentDidMount/Update 被执行。

文件位置:react-reconciler/src/ReactFiberWorkLoop.js

以下所示:

function commitRootImpl(root) {
    if (firstEffect !== null) {
        // before mutation 阶段,遍历 effect list
        do {
            try {
                commitBeforeMutationEffects();
            } catch (error) {
                nextEffect = nextEffect.nextEffect;
            }
        } while (nextEffect !== null);

        // the mutation phase 阶段,遍历 effect list
        nextEffect = firstEffect;
        do {
            try {
                commitMutationEffects();
            } catch (error) {
                nextEffect = nextEffect.nextEffect;
            }
        } while (nextEffect !== null);

        // 将 work-in-progress 树替换为 current 树
        root.current = finishedWork;

        // layout phase 阶段,遍历 effect list
        nextEffect = firstEffect;
        do {
            try {
                commitLayoutEffects(root, expirationTime);
            } catch (error) {
                captureCommitPhaseError(nextEffect, error);
                nextEffect = nextEffect.nextEffect;
            }
        } while (nextEffect !== null);

        nextEffect = null;
    } else {
        // No effects.
        root.current = finishedWork;
    }
}
复制代码

commitBeforeMutationEffects

before mutation 调用链路:commitRootImpl -->  commitBeforeMutationEffects --> commitBeforeMutationLifeCycles

代码以下:

function commitBeforeMutationLifeCycles( current: Fiber | null, finishedWork: Fiber, ): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    ...
    // 属性 stateNode 表示对应组件的实例
    // 在这里 class 组件实例执行 instance.getSnapshotBeforeUpdate()
    case ClassComponent: {
      if (finishedWork.effectTag & Snapshot) {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );

          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
      }
      return;
    }
    case HostRoot:
    case HostComponent:
    case HostText:
    case HostPortal:
    case IncompleteClassComponent:
      ...
  }
}
复制代码

commitMutationEffects

文件位置:react-reconciler/src/ReactFiberWorkLoop.js

mutation phase 阶段调用链路: commitRootImpl -->  commitMutationEffects --> commitWork

代码以下:

function commitMutationEffects() {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;

    let primaryEffectTag = effectTag & (Placement | Update | Deletion);
    switch (primaryEffectTag) {
      case Placement:
        ...
      case PlacementAndUpdate:
        ...
      case Update: {
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      case Deletion: {
        commitDeletion(nextEffect);
        break;
      }
    }
  }
}
复制代码

commitLayoutEffects

文件位置:react-reconciler/src/ReactFiberCommitWork.js

layout phase 调用链路:commitRootImpl -->  commitLayoutEffects --> commitLifeCycles

代码以下:

function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedExpirationTime: ExpirationTime, ): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
      ...
    case ClassComponent: {
      // 属性 stateNode 表示对应组件的实例
      // 在这里 class 组件实例执行 componentDidMount/DidUpdate
      const instance = finishedWork.stateNode;
      if (finishedWork.effectTag & Update) {
        // 首次渲染时,尚未 current 树
        if (current === null) {
          instance.componentDidMount();
        } else {
          const prevProps =
            finishedWork.elementType === finishedWork.type
              ? current.memoizedProps
              : resolveDefaultProps(finishedWork.type, current.memoizedProps);
          const prevState = current.memoizedState;
          instance.componentDidUpdate(
            prevProps,
            prevState,
            instance.__reactInternalSnapshotBeforeUpdate,
          );
        }
      }
      const updateQueue = finishedWork.updateQueue;
      if (updateQueue !== null) {
        commitUpdateQueue(
          finishedWork,
          updateQueue,
          instance,
          committedExpirationTime,
        );
      }
      return;
    }
    case HostRoot:
    case HostComponent:
    case HostText:
    case HostPortal:
    case Profiler:
    case SuspenseComponent:
    case SuspenseListComponent:
      ...
  }
}
复制代码

扩展

如下是一些关于 Fiber 的扩展内容。

调用链路

以下图所示,根据 React 源码绘制的调用链路图,主要罗列了一些比较重要的函数方法,可做为你们了解 Fiber 的参考。源码调试过程能够找到对应的函数方法打断点,以了解实际运行的过程,便于更好梳理出各个逻辑方法之间的关系。

fiber调用链路.jpg

requestIdleCallback

以前有文章在总结 React Fiber 的调度原理时提到,客户端线程执行任务时会以帧的形式划分,在两个执行帧之间,主线程一般会有一小段空闲时间,在这个空闲期触发 requestIdleCallback 方法,可以执行一些优先级较低的 work。

听说在早期的 React 版本上确实是这么作的,但使用 requestIdleCallback 实际上有一些限制,执行频次不足,以至于没法实现流畅的 UI 渲染,扩展性差。所以 React 团队放弃了 requestIdleCallback 用法,实现了自定义的版本。好比,在发布 v16.10 版本中,推出实验性的 Scheduler,尝试使用 postMessage 来代替 requestAnimationFrame。更多了解能够查看 React 源码 packages/scheduler 部分。

小结

Fiber 由来已久,能够说是 React 设计思想的一个典型表现。相比业界其余流行库更多采用当新数据到达时再计算模式,React 坚持拉取模式,即可以把计算资源延迟到必要时候再用,而且它知道,何时更适合执行,何时不执行。看起来虽然只是微小的区别,却意义很大。随着后续异步渲染能力等新特性的推出,咱们有理由相信,在将来,React 将会在人机交互的应用中给咱们带来更多的惊喜。

参考

本文发布自 网易云音乐大前端团队,文章未经受权禁止任何形式的转载。咱们常年招收前端、iOS、Android,若是你准备换工做,又刚好喜欢云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!

相关文章
相关标签/搜索