React源码04 - Fiber Scheduler (调度器)

创建更新之后,找到 Root 然后进入调度,同步和异步操作完全不同,实现更新分片的性能优化。

主流的浏览器刷新频率为 60Hz,即每(1000ms / 60Hz)16.6ms 浏览器刷新一次。JS可以操作 DOM,JS线程GUI渲染线程 是互斥的。所以 **JS脚本执行 **和 **浏览器布局、绘制 **不能同时执行。
在每16.6ms时间内,需要完成如下工作:

JS脚本执行 -----  样式布局 ----- 样式绘制

既然以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器在每一帧 16.6ms 中执行完自己的 GUI 渲染线程后,还有剩余时间的话能通知我们执行 react 的异步更新任务,react 执行时会自己计时,如果时间到了,而 react 依然没有执行完,则会挂起自己,并把控制权还给浏览器,以便浏览器执行更高优先级的任务。然后 react 在下次浏览器空闲时恢复执行。而如果是同步任务,则不会中断,会一直占用浏览器直到页面渲染完毕。

其实部分浏览器已经实现了这个API,这就是 requestIdleCallback(字面意思:请求空闲回调)。但是由于以下因素,React 放弃使用:

  • 浏览器兼容性
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的 requestIdleCallback 触发的频率会变得很低。

React 实现了功能更完备的 requestIdleCallback polyfill(使用window.requestAnimationFrame() 和 JavaScript 任务队列进行模拟),这就是 Scheduler,除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置。

当 Scheduler 将任务交给 Reconciler 后,Reconciler 会为变化的虚拟 DOM 打上代表增/删/更新的标记,类似这样:

// 这种二进制存储数据:
// 设置:集合 | 目标
// 查询:集合 & 目标
// 取消:集合 & ~目标

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

整个 Scheduler 与 Reconciler 的工作都在内存中进行。只有当所有组件都完成 Reconciler 的工作后,才会统一交给 Renderer。

Renderer 根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。


image.png
image.png

其中红框中的步骤随时可能由于以下原因被中断:

  • 有其他更高优任务需要先更新
  • 当前帧没有剩余时间

由于红框中的工作都在内存中进行,不会更新页面上的DOM,即使反复中断用户也不会看见更新不完全的DOM。
因此可以说 Scheduler 和 Reconciler 是和平台无关的,而和平台相关的是 Renderer。

大致更新调度的流程:

  • 首先通过 ReactDOM.render/setState/forceUpdate 产生更新。
  • 找到产生更新的节点所对应的 FiberRoot,将其加入到调度器中(多次调用 ReactDOM.render 就会有多个 root)。
  • 根据 expirationTime 判断是同步更新,还是异步更新。具体就是在上一篇提到的,在 computeExpirationForFiber() 计算过期时间时,根据 fiber.mode & ConcurrentMode 模式是否开启,来计算同步的过期时间,和异步的过期时间,也就是 同步更新任务异步更新任务 。同步更新任务没有 deadline(用于 react 执行的分片时间),会立即执行,不会被中断。而异步更新任务有 deadline,需要等待调度,并可能会中断。在此过程中,同步任务和异步任务最终会汇聚到一起,根据是否有 deadline,会进入同步循环条件或异步循环条件,而这个循环就是指遍历整棵 Fiber 树的每个节点进行更新的操作。同步任务遍历完更新完就完了,而异步任务在更新节点时会受到分片调度的控制。
  • 异步更新任务被加入到 Scheduler 的 callbackList 中等待调度。调度时,会检查是否有任务已经过期(expirationTime),会先把所有已经超时的任务执行掉,直到遇到非超时的任务时,如果当前时间分片 deadline 还没到点,则继续执行,如果已经到点了,则控制权交给浏览器。
  • 采用了 deadline 分片保证异步更新任务不会阻塞浏览器 GUI 的渲染、如动画等能在 30FPS 以上。

TODO:Scheduler 图,可能要自己手绘

2. scheduleWork

在上一篇 “React 中的更新” 提到,ReactDOM.render/setState/forceUpdate 最终都会进入 scheduleWork 即调度工作。

  • 找到更新所对应的 FiberRoot 节点。
  • 如果符合条件则重置 stack
  • 如果符合条件就请求工作调度

TODO: Fiber 树图
**
点击 button,是 List 组件实例调用了 setState,创建 update 后开始调度时,是将 List 所在的 RootFiber 这个 fiber 根节点加入到调度队列中(而并不是直接把 List Fiber 节点加入调度中),正如每次更新时也是从 RootFiber 开始更新。

一些全局变量:

  • isWorking: 用来标志是否当前有更新正在进行,不区分阶段,包含了 commit 和 render 阶段
  • isCommitting: 是否处于 commit 阶段

React在16版本之后处理任务分为两个阶段

  • render 阶段: 判断哪些变更需要被处理成 DOM,也就是比较上一次渲染的结果和新的更新,打标记。
  • commit 阶段: 处理从 js 对象到 DOM 的更新,不会被打断,并且会调用 componentDidMount 和 componentDidUpdate 这些生命周期方法。
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) {
    return;
  }

  // 异步任务由于时间片不够被中断,执行权了交给浏览器,
  // 此时产生了更新任务,其优先级更高,则会打断老的任务。
  if (
    !isWorking &&
    nextRenderExpirationTime !== NoWork &&
    expirationTime < nextRenderExpirationTime
  ) {
    // This is an interruption. (Used for performance tracking.)
    interruptedBy = fiber; // 给开发工具用的,用来展示被哪个节点打断了异步任务
    resetStack(); // 重置之前的中断的任务已经产生的部分节点更新
  }
  // 暂时先忽略
  markPendingPriorityLevel(root, expirationTime);
  
  // 没有正在进行的工作,或者是上一次render阶段已经结束(更新结束),已经到了commit阶段,
  // 那么可以继续请求一次调度工作。
  if (
    // If we're in the render phase, we don't need to schedule this root
    // for an update, because we'll do it before we exit...
    !isWorking ||
    isCommitting ||
    // ...unless this is a different root than the one we're rendering.
    nextRoot !== root // 单个FiberRoot入口时nextRoot永远等于root,所以基本不需要考虑这个条件,因为基本都是单入口应用
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime); // 请看下一节
  }
  
  // 检测死循环更新
  if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
    // Reset this back to zero so subsequent updates don't throw.
    nestedUpdateCount = 0;
    invariant(
      false,
      'Maximum update depth exceeded. This can happen when a ' +
      'component repeatedly calls setState inside ' +
      'componentWillUpdate or componentDidUpdate. React limits ' +
      'the number of nested updates to prevent infinite loops.',
    );
  }
}

function resetStack() {
  // 用于记录render阶段Fiber树遍历过程中下一个需要执行的节点,
  // 不为null说明之前存在更新任务,只是时间片不够了,被中断了
  // 于是就要向上遍历父节点,将已经更新了的状态回退到更新之前,
  // 回退是为了避免状态混乱,不能把搞了一半的摊子直接丢给新的更新任务,
  // 因为新的高优先级任务也要从根节点RootFiber开始更新。
  if (nextUnitOfWork !== null) {
    let interruptedWork = nextUnitOfWork.return;
    while (interruptedWork !== null) {
      unwindInterruptedWork(interruptedWork);
      interruptedWork = interruptedWork.return;
    }
  }

  if (__DEV__) {
    ReactStrictModeWarnings.discardPendingWarnings();
    checkThatStackIsEmpty();
  }

  // 回退一些全局变量
  nextRoot = null;
  nextRenderExpirationTime = NoWork;
  nextLatestAbsoluteTimeoutMs = -1;
  nextRenderDidError = false;
  nextUnitOfWork = null;
}

scheduleWorkToRoot() 根据传入的 Fiber 节点,找到对应的 FiberRoot(也就是最初调用 ReactDOM.render 时创建的 FiberRoot)。对于 ReactDOM.render 产生的调用 scheduleWork() ,其传入的是 RootFiber 节点, RootFiber.stateNode 就找到了 FiberRoot;而 setState/forceUpdate 传入的是自身组件所对应的 Fiber 子节点,会复杂一些:

function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
  // 记录调度时间,
  recordScheduleUpdate();

  // 如果尚未更新过,没有存储过期时间,或者最新计算出的过期时间更短,意味着优先级更高,
  // 那么就更新目标Fiber上的过期时间 (即产生更新的那个Fiber,如刚才的List组件对应的Fiber)
  // Update the source fiber's expiration time
  if (
    fiber.expirationTime === NoWork ||
    fiber.expirationTime > expirationTime
  ) {
    fiber.expirationTime = expirationTime;
  }
  let alternate = fiber.alternate;
  // 同理如果alternate fiber(之前说过的双缓存、双buff机制)存在,也要尝试更新过期时间
  if (
    alternate !== null &&
    (alternate.expirationTime === NoWork ||
      alternate.expirationTime > expirationTime)
  ) {
    alternate.expirationTime = expirationTime;
  }
  
  
  // Walk the parent path to the root and update the child expiration time.
  let node = fiber.return; // fiber.return指向的就是父fiber节点。
  let root = null;
  // 只有RootFiber.return会是null,说明传入的Fiber就是RootFiber,其tag就是3,也就是HostRoot 
  if (node === null && fiber.tag === HostRoot) { 
    root = fiber.stateNode; // 找到了 FiberRoot
  } else {
    // 向上遍历,寻找FiberRoot
    while (node !== null) {
      alternate = node.alternate;
      if (
        node.childExpirationTime === NoWork ||
        node.childExpirationTime > expirationTime
      ) {
        // childExpirationTime: 父节点记录了子树中优先级最高的过期时间,即最先的过期时间
        // 如果当前传入节点的过期时间优先级更高,则更新父节点的childExpirationTime,
        // 同理还要更新父节点的alternate节点。
        node.childExpirationTime = expirationTime;
        if (
          alternate !== null &&
          (alternate.childExpirationTime === NoWork ||
            alternate.childExpirationTime > expirationTime)
        ) {
          alternate.childExpirationTime = expirationTime;
        }
      // node.childExpirationTime不需要更新,node.alternate.childExpirationTime也要尝试更新
      } else if (
        alternate !== null &&
        (alternate.childExpirationTime === NoWork ||
          alternate.childExpirationTime > expirationTime)
      ) {
        alternate.childExpirationTime = expirationTime;
      }
      // 找到了 FiberRoot 就跳出
      if (node.return === null && node.tag === HostRoot) {
        root = node.stateNode;
        break;
      }
      // 没找到则指针上溯,继续下次while循环
      node = node.return;
    }
  }

  // 提醒当前传入的fiber没有找到FiberRoot节点
  if (root === null) {
    if (__DEV__ && fiber.tag === ClassComponent) {
      warnAboutUpdateOnUnmounted(fiber);
    }
    return null;
  }

  // 跟踪应用更新的相关代码,精力和水平有限,汪洋大海不再深究。
  if (enableSchedulerTracing) {
    const interactions = __interactionsRef.current;
    if (interactions.size > 0) {
      const pendingInteractionMap = root.pendingInteractionMap;
      const pendingInteractions = pendingInteractionMap.get(expirationTime);
      if (pendingInteractions != null) {
        interactions.forEach(interaction => {
          if (!pendingInteractions.has(interaction)) {
            // Update the pending async work count for previously unscheduled interaction.
            interaction.__count++;
          }

          pendingInteractions.add(interaction);
        });
      } else {
        pendingInteractionMap.set(expirationTime, new Set(interactions));

        // Update the pending async work count for the current interactions.
        interactions.forEach(interaction => {
          interaction.__count++;
        });
      }

      const subscriber = __subscriberRef.current;
      if (subscriber !== null) {
        const threadID = computeThreadID(
          expirationTime,
          root.interactionThreadID,
        );
        subscriber.onWorkScheduled(interactions, threadID);
      }
    }
  }

  return root;
}

3. requestWork

  • 加入到 root 调度队列
  • 判断是否批量更新
  • 根据 expirationTime 判断调度类型

requestWork:

// requestWork is called by the scheduler whenever a root receives an update.
// It's up to the renderer to call renderRoot at some point in the future.
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }
    // 关于批处理,下一小节来探讨
  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, true);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) { 
    // 同步调用js代码直到结束为止,不会被打断。
    performSyncWork();
  } else { 
    // 否则进行异步调度,进入到了独立的scheduler包中, 即react的requestIdleCallback polyfill
    // 等待有在deadline时间片内才能得到执行,deadline到点后则控制权交给浏览器,等待下一个时间片。
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

addRootToSchedule:

  • 检查 root 是否已参与调度,没有则加入到 root 调度队列,实际就是单向链表添加节点的操作。当然绝大多数时候,我们的应用只有一个 root。
  • 如果 root 已经参与调度,但可能需要提升优先级,使用 expirationTime 保存最高优先级的任务。
function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
  // Add the root to the schedule.
  // Check if this root is already part of the schedule.
  if (root.nextScheduledRoot === null) {
    // This root is not already scheduled. Add it.
    root.expirationTime = expirationTime;
    if (lastScheduledRoot === null) {
      firstScheduledRoot = lastScheduledRoot = root;
      root.nextScheduledRoot = root;
    } else {
      lastScheduledRoot.nextScheduledRoot = root;
      lastScheduledRoot = root;
      lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
    }
  } else {
    // This root is already scheduled, but its priority may have increased.
    const remainingExpirationTime = root.expirationTime;
    if (
      remainingExpirationTime === NoWork ||
      expirationTime < remainingExpirationTime
    ) {
      // Update the priority.
      root.expirationTime = expirationTime;
    }
  }
}

4. batchUpdates

顾名思义,批量更新,可以避免短期内的多次渲染,攒为一次性更新。

在后面提供的 demo 中的 handleClick 中有三种方式调用 this.countNumber()
**
第1种:
批量更新,会打印 0 0 0,然后按钮文本显示为3。每次 setState 虽然都会经过 enqueueUpdate (创建update 并加入队列)-> scheduleWork(寻找对应的 FiberRoot 节点)-> requestWork (把 FiberRoot 加入到调度队列),可惜上下文变量 isBatchingUpdates 在外部某个地方被标记为了 true ,因此本次 setState 一路走来,尚未到达接下来的 performSyncWork 或者 scheduleCakkbackWithExpirationTime 就开始一路 return 出栈:

image

isBatchingUpdates 变量在早前的调用栈中(我们为 onClick 绑定的事件处理函数会被 react 包裹多层),被标记为了 true ,然后 fn(a, b) 内部经过了3次 setState 系列操作,然后 finally 中 isBatchingUpdates 恢复为之前的 false,此时执行同步更新工作 performSyncWork

image

第2种:
handleClick 中使用 setTimeoutthis.countNumber 包裹了一层 setTimeout(() => { this.countNumber()}, 0) ,同样要调用 handleClick 也是先经过 interactiveUpdates$1 上下文,也会执行 setTimeout ,然后 fn(a, b) 就执行完了,因为最终是浏览器来调用 setTimeout 的回调 然后执行里面的 this.countNumber ,而对于 interactiveUpdates$1 来说继续把自己的 performSyncWork 执行完,就算结束了。显然不管 performSyncWork 做了什么同步更新,我们的 setState 目前为止都还没得到执行。然后等到 setTimeout 的回调函数等到空闲被执行的时候,才会执行 setState ,此时没有了批量更新之上下文,所以每个 setState 都会单独执行一遍 requestWork 中的 performSyncWork 直到渲染结束,且不会被打断,3次 setState 就会整个更新渲染 3 遍(这样性能不好,所以一般不会这样写 react)。
什么叫不会被打断的同步更新渲染?看一下 demo 中的输出,每次都同步打印出了最新的 button dom 的 innerText

第3种:
已经可以猜到,无非就是因为使用 setTimeout 而“错过了”第一次的批量更新上下文,那等到 setTimeout 的回调执行的时候,专门再创建一个批量更新上下文即可:

image.png
image.png

**
所以,setState 是同步还是异步?
**
setState 方法本身是被同步调用,但并不代表 react 的 state 就会被立马同步地更新,而是要根据当前执行上下文来判断。
如果处于批量更新的情况下,state 不会立马被更新,而是批量更新。
如果非批量更新的情况下,那么就“有可能”是立马同步更新的。为什么不是“一定”?因为如果 React 开启了 Concurrent Mode,非批量更新会进入之前介绍过的异步调度中(时间分片)。

批量更新演示 demo:

import React from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'

export default class BatchedDemo extends React.Component {
  state = {
    number: 0,
  }

  handleClick = () => {
    // 事件处理函数自带batchedUpdates
    // this.countNumber()

    // setTimeout中没有batchedUpdates
    setTimeout(() => {
      this.countNumber()
    }, 0)

    // 主动batchedUpdates
    // setTimeout(() => {
    //   batchedUpdates(() => this.countNumber())
    // }, 0)
  }

  countNumber() {
    const button = document.getElementById('myButton')
    const num = this.state.number
    this.setState({
      number: num + 1,
    })
    console.log(this.state.number)
    console.log(button.innerText)
    this.setState({
      number: num + 2,
    })
    console.log(this.state.number)
    console.log(button.innerText)
    this.setState({
      number: num + 3,
    })
    console.log(this.state.number)
    console.log(button.innerText)
  }

  render() {
    return <button id="myButton" onClick={this.handleClick}>Num: {this.state.number}</button>
  }
}

5. Scheduler 调度器

  • 维护时间片
  • 模拟 requestIdleCallback
  • 调度列表和超时判断

如果1秒30帧,那么需要是平均的30帧,而不是前0.5秒1帧,后0.5秒29帧,这样也会感觉卡顿的。
Scheduler 目的就是保证 React 执行更新的时间,在浏览器的每一帧里不超过一定值。
不要过多占用浏览器用来渲染动画或者响应用户输入的处理时间。

image

image

**
继续之前的源码,requestWork 的最后,如果不是同步的更新任务,那么就要参与 Scheduler 时间分片调度了:

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }

scheduleCallbackWithExpirationTime:

function scheduleCallbackWithExpirationTime(
  root: FiberRoot,
  expirationTime: ExpirationTime,
) {
  
  // 如果已经有在调度的任务,那么调度操作本身就是在循环遍历任务,等待即可。
  if (callbackExpirationTime !== NoWork) {
    // A callback is already scheduled. Check its expiration time (timeout).
    // 因此,如果传入的任务比已经在调度的任务优先级低,则返回
    if (expirationTime > callbackExpirationTime) {
      // Existing callback has sufficient timeout. Exit.
      return;
    } else {
      // 但是!如果传入的任务优先级更高,则要打断已经在调度的任务
      if (callbackID !== null) {
        // Existing callback has insufficient timeout. Cancel and schedule a
        // new one.
        cancelDeferredCallback(callbackID);
      }
    }
    // The request callback timer is already running. Don't start a new one.
  } else {
    startRequestCallbackTimer(); // 涉及到开发工具和polyfill,略过
  }
    
  // 如果是取消了老的调度任务,或者是尚未有调度任务,则接下来会安排调度
  callbackExpirationTime = expirationTime;
  // 计算出任务的timeout,也就是距离此刻还有多久过期
  const currentMs = now() - originalStartTimeMs; // originalStartTimeMs 代表react应用最初被加载的那一刻
  const expirationTimeMs = expirationTimeToMs(expirationTime);
  const timeout = expirationTimeMs - currentMs;
  // 类似于 setTimeout 返回的 ID,可以用来延期回调
  callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}

6. unstable_scheduleCallback

前面都还在 packages/react-reconciler/ReactFiberScheduler.js 中,下面就要跟着刚才的 **scheduleDeferredCallback **辗转进入到单独的 packages/scheduler 包中:

  • 根据不同优先级等级计算不同的 callbackNode 上的过期时间。
  • 存储以过期时间为优先级的环形链表,用时可借助首节点 firstCallbackNode 可对链表进行遍历读取。
  • firstCallbackNode 变了后要调用 ensureHostCallbackIsScheduled 重新遍历链表进行调度。
image

unstable_scheduleCallback:

function unstable_scheduleCallback(callback, deprecated_options) {
  var startTime =
    currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

  var expirationTime;
  if (
    typeof deprecated_options === 'object' &&
    deprecated_options !== null &&
    typeof deprecated_options.timeout === 'number'
  ) {
    // FIXME: Remove this branch once we lift expiration times out of React.
    expirationTime = startTime + deprecated_options.timeout;
  } else {
    switch (currentPriorityLevel) {
      case ImmediatePriority:
        expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
        break;
      case UserBlockingPriority:
        expirationTime = startTime + USER_BLOCKING_PRIORITY;
        break;
      case IdlePriority:
        expirationTime = startTime + IDLE_PRIORITY;
        break;
      case NormalPriority:
      default:
        expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
    }
  }

  var newNode = {
    callback,
    priorityLevel: currentPriorityLevel,
    expirationTime,
    next: null,
    previous: null,
  };

  // Insert the new callback into the list, ordered first by expiration, then
  // by insertion. So the new callback is inserted any other callback with
  // equal expiration.
  if (firstCallbackNode === null) {
    // This is the first callback in the list.
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    ensureHostCallbackIsScheduled();
  } else {
    var next = null;
    var node = firstCallbackNode;
    do {
      if (node.expirationTime > expirationTime) {
        // The new callback expires before this one.
        next = node;
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode);

    if (next === null) {
      // No callback with a later expiration was found, which means the new
      // callback has the latest expiration in the list.
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      // The new callback has the earliest expiration in the entire list.
      firstCallbackNode = newNode;
      ensureHostCallbackIsScheduled();
    }

    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}

7. ensureHostCallbackIsScheduled

  • 该方法名字就说明了目的是保证 callback 会被调度,故若已经有 callbackNode 在被调度,自会自动循环。
  • 从头结点,也就是最先过期的 callbackNode 开始请求调用,顺表如果有已存在的调用要取消。这就是之前说过的参与调用的任务有两种被打断的可能:1. 时间片到点了,2. 有更高优先级的任务参与了调度
function ensureHostCallbackIsScheduled() {
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest expiration in the list.
  var expirationTime = firstCallbackNode.expirationTime;
  if (!isHostCallbackScheduled) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    cancelHostCallback();
  }
  requestHostCallback(flushWork, expirationTime);
}
requestHostCallback = function(callback, absoluteTimeout) {
  scheduledHostCallback = callback;
  timeoutTime = absoluteTimeout;
  
  // 超时了要立即安排调用
  if (isFlushingHostCallback || absoluteTimeout < 0) {
    // Don't wait for the next frame. Continue working ASAP, in a new event.
    window.postMessage(messageKey, '*');
  } else if (!isAnimationFrameScheduled) {
    // 没有超时,就常规安排,等待时间片
    isAnimationFrameScheduled = true;
    requestAnimationFrameWithTimeout(animationTick);
  }
};

// 取消之前安排的任务回调,就是重置一些变量
cancelHostCallback = function() {
  scheduledHostCallback = null;
  isMessageEventScheduled = false;
  timeoutTime = -1;
};

为了模拟 requestIdleCallback API:
传给 window.requestanimationframe 的回调函数会在浏览器下一次重绘之前执行,也就是执行该回调后浏览器下面会立即进入重绘。使用 window.postMessage 技巧将空闲工作推迟到重新绘制之后。
具体太过复杂,就大概听个响吧,若要深究则深究:

  • animationTick
  • idleTick
// 仅供示意
requestAnimationFrameWithTimeout(animationTick);
var animationTick = function(rafTime) {
  requestAnimationFrameWithTimeout(animationTick);
}
window.addEventListener('message', idleTick, false);
window.postMessage(messageKey, '*');

react 这里还能统计判断出平台刷新频率,来动态减少 react 自身运行所占用的时间片,支持的上限是 120hz 的刷新率,即每帧总共的时间不能低于 8ms。
此间如果一帧的时间在执行 react js 之前就已经被浏览器用完,那么对于非过期任务,等待下次时间片;而对于过期任务,会强制执行。

8. flushWork

ensureHostCallbackIsScheduled 中的 requestHostCallback(flushWork, expirationTime) 参与时间片调度:
flushWork:

  • 即使当前时间片已超时,也要把 callbackNode 链表中所有已经过期的任务先强制执行掉
  • 若当前帧还有时间片,则常规处理任务
function flushWork(didTimeout) {
  isExecutingCallback = true;
  deadlineObject.didTimeout = didTimeout;
  try {
    // 把callbackNode链表中所有已经过期的任务先强制执行掉
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      while (firstCallbackNode !== null) {
        // Read the current time. Flush all the callbacks that expire at or
        // earlier than that time. Then read the current time again and repeat.
        // This optimizes for as few performance.now calls as possible.
        var currentTime = getCurrentTime();
        if (firstCallbackNode.expirationTime <= currentTime) {
          do {
            flushFirstCallback();
          } while (
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime
          );
          continue;
        }
        break;
      }
    } else {
      // 当前帧还有时间片,则继续处理任务
      // Keep flushing callbacks until we run out of time in the frame.
      if (firstCallbackNode !== null) {
        do {
          flushFirstCallback();
        } while (
          firstCallbackNode !== null &&
          getFrameDeadline() - getCurrentTime() > 0
        );
      }
    }
  } finally {
    isExecutingCallback = false;
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      ensureHostCallbackIsScheduled();
    } else {
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    flushImmediateWork();
  }
}

flushFirstCallback 负责处理链表节点,然后执行 flushedNode.callback

9. performWork

  • 是否有 deadline 的区分
  • 循环渲染 Root 的条件
  • 超过时间片的处理

performSyncWork 不会传 deadline。
没有deadline时,会循环执行 root 上的同步任务,或者任务过期了,也会立马执行任务。

performAsyncWork:

function performAsyncWork(dl) {
  if (dl.didTimeout) { // 是否过期
    if (firstScheduledRoot !== null) {
      recomputeCurrentRendererTime();
      let root: FiberRoot = firstScheduledRoot;
      do {
        didExpireAtExpirationTime(root, currentRendererTime);
        // The root schedule is circular, so this is never null.
        root = (root.nextScheduledRoot: any);
      } while (root !== firstScheduledRoot);
    }
  }
  performWork(NoWork, dl);
}

performSyncWork:

function performSyncWork() {
  performWork(Sync, null);
}

performWork:

function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
  deadline = dl;

  // Keep working on roots until there's no more work, or until we reach
  // the deadline.
  findHighestPriorityRoot();

  if (deadline !== null) {
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;

    if (enableUserTimingAPI) {
      const didExpire = nextFlushedExpirationTime < currentRendererTime;
      const timeout = expirationTimeToMs(nextFlushedExpirationTime);
      stopRequestCallbackTimer(didExpire, timeout);
    }

    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime) &&
      (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
    ) {
      performWorkOnRoot(
        nextFlushedRoot,
        nextFlushedExpirationTime,
        currentRendererTime >= nextFlushedExpirationTime,
      );
      findHighestPriorityRoot();
      recomputeCurrentRendererTime();
      currentSchedulerTime = currentRendererTime;
    }
  } else {
    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime)
    ) {
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
      findHighestPriorityRoot();
    }
  }

  // We're done flushing work. Either we ran out of time in this callback,
  // or there's no more work left with sufficient priority.

  // If we're inside a callback, set this to false since we just completed it.
  if (deadline !== null) {
    callbackExpirationTime = NoWork;
    callbackID = null;
  }
  // If there's work left over, schedule a new callback.
  if (nextFlushedExpirationTime !== NoWork) {
    scheduleCallbackWithExpirationTime(
      ((nextFlushedRoot: any): FiberRoot),
      nextFlushedExpirationTime,
    );
  }

  // Clean-up.
  deadline = null;
  deadlineDidExpire = false;

  finishRendering();
}

performWorkOnRoot:

  • isRendering 标记现在开始渲染了
  • 判断 finishedWork:是:调用 completeRoot 进入下一章的 commit 阶段;否:调用 renderRoot 遍历 Fiber 树。
function performWorkOnRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isExpired: boolean,
) {
    
  isRendering = true;

  // Check if this is async work or sync/expired work.
  if (deadline === null || isExpired) {
    // Flush work without yielding.
    // TODO: Non-yieldy work does not necessarily imply expired work. A renderer
    // may want to perform some work without yielding, but also without
    // requiring the root to complete (by triggering placeholders).

    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // This root is already complete. We can commit it.
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      // If this root previously suspended, clear its existing timeout, since
      // we're about to try rendering again.
      const timeoutHandle = root.timeoutHandle;
      if (timeoutHandle !== noTimeout) {
        root.timeoutHandle = noTimeout;
        // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
        cancelTimeout(timeoutHandle);
      }
      const isYieldy = false;
      renderRoot(root, isYieldy, isExpired);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // We've completed the root. Commit it.
        completeRoot(root, finishedWork, expirationTime);
      }
    }
  } else {
    // Flush async work.
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // This root is already complete. We can commit it.
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      // If this root previously suspended, clear its existing timeout, since
      // we're about to try rendering again.
      const timeoutHandle = root.timeoutHandle;
      if (timeoutHandle !== noTimeout) {
        root.timeoutHandle = noTimeout;
        // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
        cancelTimeout(timeoutHandle);
      }
      const isYieldy = true;
      renderRoot(root, isYieldy, isExpired);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // We've completed the root. Check the deadline one more time
        // before committing.
        if (!shouldYield()) {
          // Still time left. Commit the root.
          completeRoot(root, finishedWork, expirationTime);
        } else {
          // There's no time left. Mark this root as complete. We'll come
          // back and commit it later.
          root.finishedWork = finishedWork;
        }
      }
    }
  }

  isRendering = false;
}

10. renderRoot

  • 调用 workLoop 进行循环单元更新
  • 捕获错误并进行处理
  • 走完流程之后善后

**renderRoot **流程:

  • 遍历 Fiber 树的每个节点。

根据 Fiber 上的 updateQueue 是否有内容,决定是否要更新那个 Fiber 节点,并且计算出新的 state,
对于异步任务,更新每个 Fiber 节点时都要判断时间片是否过期,如果一个 Fiber 更新时出错,则其子节点就不用再更新了。最终整个 Fiber 树遍历完之后,根据捕获到的问题不同,再进行相应处理。

  • createWorkInProgress:renderRoot 中,调用 createWorkInProgress 创建 “workInProgress” 树,在其上进行更新操作。在 renderRoot 开始之后,所有的操作都在 “workInProgress” 树上进行,而非直接操作 “current” 树。(双buff机制)
  • workLoop:开始更新一颗 Fiber 树上的每个节点, isYieldy 指示是否可以中断,对于 sync 任务和已经超时的任务都是不可中断的,于是 while 循环更新即可;对于可中断的,则每次 while 循环条件中还要判断是否时间片到点需先退出。
function workLoop(isYieldy) {
  if (!isYieldy) {
    // Flush work without yielding
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // Flush asynchronous work until the deadline runs out of time.
    while (nextUnitOfWork !== null && !shouldYield()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}
  • performUnitOfWork:更新子树:
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  const current = workInProgress.alternate;
  // See if beginning this work spawns more work.
  startWorkTimer(workInProgress);
  let next;
    if (enableProfilerTimer) {
    if (workInProgress.mode & ProfileMode) {
      startProfilerTimer(workInProgress);
    }

    next = beginWork(current, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;

    if (workInProgress.mode & ProfileMode) {
      // Record the render duration assuming we didn't bailout (or error).
      stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
    }
  } else {
    next = beginWork(current, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
  }
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
  }
  ReactCurrentOwner.current = null;
  return next;
}
  • beginWork:开始具体的节点更新,下一章再说。

Root 节点具体怎么遍历更新,以及不同类型组件的更新,将在下一篇探讨。


image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270