react fiber

在 react 16 之前,更新虚拟 DOM 为 stack reconciler,这是一个递归的过程,在树很深的时候,单次 diff 时间过长会造成 JS 线程持续被占用,用户交互响应迟滞,页面渲染会出现明显的卡顿。为了解决这种问题,react 团队使用 fiber 架构重构了 React,使其能够将任务分片,划分优先级,同时能够实现类似于操作系统中对线程的抢占式调度

写在前面

浏览器每一帧都需要完成哪些工作? 浏览器一帧内的工作.png

通过上图可看到,一帧内需要完成如下六个步骤的任务:

  1. 处理用户交互
  2. 解析 js 脚本
  3. Begin frame。resize、scroll 等事件的处理
  4. rAF
  5. 布局
  6. 绘制

页面是一帧一帧绘制出来的,当 FPS(每秒绘制的帧数)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。也就是说浏览器 1s 绘制 60 帧,每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们应该力求不让一帧的工作量超过 16ms。

由此我们知道,浏览器是一帧一帧执行的,在两个执行帧之间,主线程通常会有一小段空闲时间

浏览器一帧工作的空余.png

requestIdleCallback 可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务

Fiber reconciler

Fiber 协调核心:可中断可恢复优先级

新特性如下:

  • 增量式渲染。把渲染任务拆分成块,匀到多帧
  • 能够暂停、终止和复用渲染任务
  • 给不同类型的更新赋予优先级
  • 并行渲染(还在开发中,值得期待)

将以前的 stack reconciler 拆分成两个阶段:rendercommit。render 阶段是可打断的,被拆分成一个个的小任务(fiber),在每一侦的渲染空闲期执行。然后是 commit 阶段,这个阶段是不拆分且不能打断的,将最终的 effectList 一口气更新到页面上。这两个阶段后续会详细介绍

  • 每个更新任务都会赋予一个优先级
  • 当更新任务抵达调度器时,高优先级的更新任务(A)会更快地被调度进入 Reconciler —— 优先级
  • 此时有新的更新任务(B),调度器会检查它优先级,若高于当前任务(A),处于当前 Reconciler 层的 A 任务会被中断,调度器将 B 任务推入 Reconciler 层 —— 可中断
  • 当 B 任务完成渲染后,新一轮调度开始,之前被中断的 A 任务将会被重新推入 Reconciler 层,继续它的渲染 —— 可恢复

fiber 对象

React Fiber 把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

维护每一个分片的数据结构,就是 Fiber。

有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。让线程处理别的事情

fiber调度.png

一个 react 节点对应着一个 fiber 节点

type Fiber = {
  // 标识不同的组件类型
  // FunctionComponent: 0; ClassComponent: 1...
  tag: WorkTag,

  key: null | string,

  // react 元素类型
  // 可以为ClassComponent、FunctionComponent、Symbol、HostComponent...
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  type: any,

  // 该fiber节点对应的ReactElement对象
  stateNode: any,

  // fiber的父级
  return: Fiber | null,

  // 单链表树状结构
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,

  ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,

  // 新 props
  pendingProps: any,
  // 旧 props
  memoizedProps: any,

  // 更新队列
  updateQueue: UpdateQueue<any> | null,

  // 旧 state
  memoizedState: any,

  // DOM diff相关
  effectTag: SideEffectTag,

  // 副作用链表
  nextEffect: Fiber | null,
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,

  // 标识该组件应该在未来的某个时刻完成更新
  // expirationTime 五种类型:
  // NoWork(默认0)、Never(1)、Interactive(100 - 150)、Async(250 - 5000)、Sync(Number.MAXINTEGER)
  expirationTime: ExpirationTime,

  // 快速确定子fiber树有没有挂起的更改
  childExpirationTime: ExpirationTime,

  // fiber 的镜像节点
  alternate: Fiber | null,
};

fiber 树示意图如下:

fiber树结构.png

演示:

创建fiber.gif

双链表

在 react 中始终存在 workInprogressTree(future vdom)oldTree(current vdom)两个链表,两个链表相互引用。

fiber双链表

双链表的好处有:

  • 复用内部对象(fiber),可以节省内存分配、GC 的时间开销
  • 获取旧状态
  • 当 workInprogressTree 生成报错时,这时也不会导致页面渲染崩溃,而只是更新失败,页面仍然还在

三个阶段

Scheduler

该阶段主要是给任务分配优先级,统筹任务调度

Scheduler 的源码解析可以参考:https://juejin.cn/post/6889314677528985614

react 16 基于 requestIdleCallback 的 polyfill 方案来实现任务调度,用法如下:

window.requestIdleCallback(callback[, options])
let handle = window.requestIdleCallback((idleDeadline) => {
    const {didTimeout, timeRemaining} = idleDeadline;
    console.log(`是否超时?${didTimeout}`);
    console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`);
    // do sth
    const now = Date.now(), timespent = 10;
    while (Date.now() < now + timespent);
    console.log(`花了${timespent}ms`);
    console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`);
}, {timeout: 1000});
// 是否超时?false
// 可用时间剩余45ms
// 花了12ms
// 可用时间剩余32ms

由于 requestIdleCallback 有兼容问题,react 团队采用的是它的 polyfill 方案,可参考

Render

遍历 Fiber 树,得出需要更新的节点信息。可以被打断,让位于优先级更高的操作,比如用户点击等

  1. Fiber Root开始遍历,构建一个新的 Fiber 树。performSyncWorkOnRoot(root)—>renderRootSync
  2. 更新每个 fiber。workLoopSync—>performUnitOfWork—>beginWork
function workLoopSync() {
  // workInProgress:当前正在处理的节点
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  // ...
  // NOTE
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  // ...
  if (next === null) {
    // 如果没有子节点,则完成当前工作
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}

beginWork 主要做了以下事情:

  1. 判断 fiber 节点是否可以复用
  2. 根据不同的 Tag(标记不同的组件类型:纯组件、函数组件、类组件),生成不同的 fiber 节点赋值给workInprogress.child
function beginWork(current, workInProgress, renderLanes) {
  // ...
  switch (workInProgress.tag) {
    // ...
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);

      // NOTE
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      // 执行 render()、生命周期钩子函数等 -> reconcileChildren
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
       // ReactDOM.render(<App/>) -> reconcileChildren
      return updateHostRoot(current, workInProgress, renderLanes);
    // ...
}

updateFunctionComponent为例,主要干了几件事:

  1. 生成新的 hooks 链表并挂载在 fiber 节点上
  2. 调用 reconcileChildren
  3. 返回 workInprogress.child
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes
) {
  // ...
  {
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes
    );
  }
  // ...
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}
// ...

reconcileChildren主要是处理子 fiber 节点,在这个阶段做 diff,并在变更的节点上标记副作用类型

// 处理子 fiber 节点
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}
// ...
function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    const isUnkeyedTopLevelFragment = ...;
    // Handle object types
    const isObject = typeof newChild === 'object' && newChild !== null;
    if (isObject) {
      // 根据不同的类型,处理不同的节点对比
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        ...
    }

    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }
    // 多节点数组
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    ...
}
...
// reconcileChildFibers 会判断节点变更的类型,赋值flags
// 在commit阶段,根据flags更新dom
// placeSingleChild 会先判断是否是第一次渲染,是的话会增加Placement副作用
function placeSingleChild(newFiber: Fiber): Fiber {
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags = Placement;
    }
    return newFiber;
}
...
// 对于 ReactElement,会调用 reconcileSingleElement
function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    expirationTime: ExpirationTime,
  ): Fiber {
    // 新 key
    const key = element.key;
    // 旧的第一个子节点
    let child = currentFirstChild;

    while (child !== null) {
      // 新旧key相同
      if (child.key === key) {
        // 节点类型未改变
        if (
          child.tag === Fragment
            ? element.type === REACT_FRAGMENT_TYPE
            : child.elementType === element.type ||
              (__DEV__
                ? isCompatibleFamilyForHotReloading(child, element)
                : false)
        ) {
          // 复用当前 child,先删除它的兄弟节点
          deleteRemainingChildren(returnFiber, child.sibling);
          // 复制 fiber 节点,重置 index 和 sibling
          const existing = useFiber(
            child,
            element.type === REACT_FRAGMENT_TYPE
              ? element.props.children
              : element.props,
            expirationTime,
          );
          // ...
          // 设置父节点
          existing.return = returnFiber;
          return existing;
        } else {
          // 节点类型不同,要删除旧节点
          deleteRemainingChildren(returnFiber, child);
          break;
        }
      } else {
        // 新旧 key 不相同,直接删除
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    // child 为 null,需要建立子节点
    if (element.type === REACT_FRAGMENT_TYPE) {
      // 创建 Fragment 类型的 fiber 节点
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        expirationTime,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      // 创建 Element 类型的 fiber 节点
      const created = createFiberFromElement(
        element,
        returnFiber.mode,
        expirationTime,
      );
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
}

...
// 副作用类型
import {
  NoEffect,
  PerformedWork,
  Placement,            // 挂载,didMount
  Update,               // 更新, didUpdate
  Snapshot,             // getSnapshotBeforeUpdate,更新之前设置快照
  PlacementAndUpdate,
  Deletion,             // 卸载,willUnmount
  ContentReset,
  Callback,
  DidCapture,
  Ref,
  Incomplete,
  HostEffectMask,
  Passive,
} from'shared/ReactSideEffectTags';
  1. fiber 节点更新收尾,串联 fiber 节点,收集 effect(如果有副作用)。completeUnitOfWorkcompleteWork
function completeUnitOfWork(unitOfWork: Fiber) {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    if ((completedWork.flags & Incomplete) === NoFlags) {
      let next;
      // 处理 fiber 的 props、创建 dom 对象、绑定事件等
      next = completeWork(current, completedWork, subtreeRenderLanes);
      // ...
      // 收集 effect,把子节点 side Effect 加到父节点的 side Effect 上
      if (
        returnFiber !== null &&
        (returnFiber.flags & Incomplete) === NoFlags
      ) {
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          returnFiber.lastEffect = completedWork.lastEffect;
        }

        const flags = completedWork.flags;
        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      // ...
    }
    // 存在兄弟节点,将 workInProgress 指向兄弟节点,并return,执行兄弟节点的beginWork -> Fiber Node
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    // 没有兄弟节点,返回父节点
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
  // ...
}

收集 effect 时,会创建 effectList,如下图所示,橙色节点上有 effect

commit

执行到 commitRoot 时,意味着 render 阶段结束,进入 commit 阶段。该阶段主要是处理 FiberRoot 上收集的 effectList,只针对变化的节点做工作,并执行生命周期钩子函数

先看下几个生命周期的执行时机:

// 第1阶段 render/reconciliation
componentWillMount;
componentWillReceiveProps;
shouldComponentUpdate;
componentWillUpdate;

// 第2阶段 commit
componentDidMount;
componentDidUpdate;
componentWillUnmount;

第 1 阶段的生命周期函数可能会被多次调用,默认以低优先级执行,被高优先级任务打断后,稍后会重新执行

commitRoot 主要是执行commitRootImpl这个函数

function commitRoot(root) {
  // ...
  commitRootImpl.bind(null, root, renderPriorityLevel)
  // ...
}
// ...
function commitRootImpl() {
  do {
    // NOTE 调用 flushPassiveEffects 执行完所有 effect
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
  // ...
  firstEffect = finishedWork.firstEffect;
  if (firseEffect !== null) {
    nextEffect = firstEffect;
    // 第一阶段,before mutation
    do {
      commitBeforeMutationEffects();
    } while(nextEffect !== null)
    // ...
    // 重置 nextEffect 为 firstEffect,接着进行第二阶段
    nextEffect = firstEffect;
    // 第二阶段 mutation
    do {
      commitMutationEffects(root, renderPriorityLevel);
    } while(nextEffect !== null)

    // 将当前的 workInProgress树 作为 current 树
    root.current = finishedWork;

    // ...
  	// 第三阶段 layout
    do {
      commitLayoutEffects(root, expirationTime);
    } while(nextEffect)
    // ...
    // 确保 root 上所有的 work 都被调度完
    ensureRootIsScheduled(root);
    // 检测在 useLayoutEffect 中是否做了布局修改等,刷新布局,如果在 layoutEffect 中调用了 setState 也会在该函数中检测中并开启新的一轮调度
    flushSyncCallbackQueue();
  }
  ...
}

commit 阶段可以细分为以下三个阶段:

  1. before mutation:读取组件变更前的状态,对于类组件,会调用生命周期函数getSnapshotBeforeUpdate,在 DOM 变更前可以获取到组件实例相关的信息;对于函数组件,会异步调度 useEffect
function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const effectTag = nextEffect.effectTag;
    // 对于使用 getSnapShowBeforeUpdate 的组件 fiber.effectTag |= SnapShot
    if ((effectTag & Snapshot) !== NoEffect) {
      // ...
      const current = nextEffect.alternate;
      // 执行 getSnapShotBeforeUpdate 生命周期
      commitBeforeMutationEffectOnFiber(current, nextEffect);
      // ...
    }
    // 对于使用 useEffect 的组件,其 Fiber.effectTag = UpdateEffect | PassiveEffect
    if ((effectTag & Passive) !== NoEffect) {
      // If there are passive effects, schedule a callback to flush at
      // the earliest opportunity.
      if (!rootDoesHavePassiveEffects) {
        rootDoesHavePassiveEffects = true;
        // NOTE
        scheduleCallback(NormalPriority, () => {
          flushPassiveEffects();
          return null;
        });
      }
    }
    nextEffect = nextEffect.nextEffect;
  }
}
  1. mutation:根据 effectTag 执行对应的 dom 操作。对于类组件,还会调用 componentWillUnmount;对于函数组件,还会执行 useLayoutEffect 的销毁函数
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  while (nextEffct) {
    // ...
    const effectTag = nextEffect.effectTag;
    let primaryEffectTag =
      effectTag & (Placement | Update | Deletion | Hydrating);
    // ...
    switch (primaryEffectTag) {
      // 挂载 DOM
      case Placement: {
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        return;
      }
      // 更新组件及DOM
      case PlacementAndUpdate: {
        commitPlacement(nextEffect);
        nextEffect.effectTag &= ~Placement;
        // 刷新 layoutEffect.destroy
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 更新组件
      case Update: {
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // 卸载
      case Deletion: {
        commitDeletion(root, nextEffect, renderPriorityLevel);
        break;
      }
      // ...
    }
    nextEffect = nextEffect.nextEffect;
  }
}
  1. layout:在 DOM 操作完成后,读取组件的状态,对于类组件,会调用生命周期componentDidMountcomponentDidUpdate;对于函数式组件,主要执行 commitHookEffectListMount
function commitLayoutEffects(root, committedExpirationTime) {
  while (nextEffect !== null) {
    // ...
    const effectTag = nextEffect.effectTag;
    if (effectTag & (Update | Callback)) {
      recordEffect();
      const current = nextEffect.alternate;
      // didmount
      commitLayoutEffectOnFiber(
        root,
        current,
        nextEffect,
        committedExpirationTime
      );
    }
    // ...
  }
}

function commitLayoutEffectOnFiber(
  finishedRoot,
  current,
  finishedWork,
  committedExpirationTime
) {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: {
      commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
      // ...
      return;
    }
    case ClassComponent: {
      // didMount
    }
  }
}