关于react.js:React-Fiber-源码解析

35次阅读

共计 14344 个字符,预计需要花费 36 分钟才能阅读完成。

图片作者:Artem Sapegin,起源:https://unsplash.com/photos/b…

本文作者:刘鹏

前言

在 React v16.13 版本中,正式推出了实验性的 Concurrent Mode,尤其是提供一种新的机制 Suspense,十分天然地解决了始终以来存在的异步副作用问题。联合后面 v16.8 推出的 Hooks,v16.0 底层架构 Fiber,React 给开发者体验上带来了极大晋升以及肯定水平上更佳的用户体验。所以,对 React 17,你会有什么期待?

Stack Reconciler 和  Fiber Reconciler

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

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

正是因为其内置 Stack Reconciler 天生带来的局限性,使得 DOM 更新过程是同步的。也就是说,在虚构 DOM 的比对过程中,如果发现一个元素实例有更新,则会立刻同步执行操作,提交到实在 DOM 的更改。这在动画、布局以及手势等畛域,可能会带来十分蹩脚的用户体验。因而,为了解决这个问题,React 实现了一个虚构堆栈帧。实际上,这个所谓的虚构堆栈帧实质上是建设了多个蕴含节点和指针的链表数据结构。每一个节点就是一个 fiber 根本单元,这个对象存储了肯定的组件相干的数据域信息。而指针的指向,则是串联起整个 fibers 树。从新自定义堆栈带来不言而喻的长处是,能够将堆栈保留在内存中,在须要执行的时候执行它们,这使得暂停遍历和进行堆栈递归成为可能。

Fiber 的次要指标是实现虚构 DOM 的增量渲染,可能将渲染工作拆分成块并将其扩散到多个帧的能力。在新的更新到来时,可能暂停、停止和复用工作,能为不同类型的更新调配优先级程序的能力。了解 React 运行机制对咱们更好了解它的设计思维以及后续版本新增个性,比方 v17 版本可能带来的异步渲染能力,置信会有很好的帮忙。本文基于 React v16.8.6 版本源码,输入一些浅见,心愿对你也有帮忙,如有不对,还望斧正。

根底概念

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

Work

在 React Reconciliation 过程中呈现的各种必须执行计算的流动,比方 state update,props update 或 refs update 等,这些流动咱们能够对立称之为 work。

Fiber 对象

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

每一个 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 阶段,如下图所示:

从 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。如下图所示:

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 的参考。源码调试过程能够找到对应的函数办法打断点,以理解理论运行的过程,便于更好梳理出各个逻辑办法之间的关系。

requestIdleCallback

之前有文章在总结 React Fiber 的调度原理时提到,客户端线程执行工作时会以帧的模式划分,在两个执行帧之间,主线程通常会有一小段闲暇工夫,在这个闲暇期触发 requestIdleCallback 办法,可能执行一些优先级较低的 work。

据说在晚期的 React 版本上的确是这么做的,但应用 requestIdleCallback 实际上有一些限度,执行频次有余,以致于无奈实现晦涩的 UI 渲染,扩展性差。因而 React 团队放弃了 requestIdleCallback 用法,实现了自定义的版本。比方,在公布 v16.10 版本中,推出实验性的 Scheduler,尝试应用 postMessage 来代替 requestAnimationFrame。更多理解能够查看 React 源码 packages/scheduler 局部。

小结

Fiber 由来已久,能够说是 React 设计思维的一个典型体现。相比业界其余风行库更多采纳当新数据达到时再计算模式,React 保持拉取模式,即可能把计算资源提早到必要时候再用,并且它晓得,什么时候更适宜执行,什么时候不执行。看起来尽管只是渺小的区别,却意义很大。随着后续异步渲染能力等新个性的推出,咱们有理由置信,在将来,React 将会在人机交互的利用中给咱们带来更多的惊喜。

参考

  • react-fiber-architecture
  • In-depth explanation of state and props update in react
  • in-depth overview of the new reconciliation algorithm in react
  • The how and why on React’s usage of linked list in Fiber to walk the component’s tree
  • Effect List —— 又一个 Fiber 链表的构建过程
  • js-ntqfill

本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0