Skip to content

Latest commit

 

History

History
201 lines (173 loc) · 11.4 KB

30. react 时间调度.md

File metadata and controls

201 lines (173 loc) · 11.4 KB

前言

继续前面两篇 react 的分析。提到 react 16,除了 fiber 机制之外,还有其调度机制。这里就不得不提 requestIdleCallback 了,react 采用了 requestIdleCallback 的思想来实现调度,为什么说思想呢,因为 requestIdleCallback 是新出的 api,兼容性差,很多现代浏览器都不支持。于是 react 团队就写了一个 ployfill。

requestIdleCallback

关于 requestIdleCallback,请先看看 MDN 上面的介绍。requestIdleCallback 的出现意使得浏览器可以在空闲的时候执行 callback,这对单线程的 V8 而言就相当又用了。假设你需要大量涉及到 DOM 的操作的计算,在运算时,浏览器可能就会出现明显的卡顿行为,甚至不能进行任何操作,因为是单线程,就算用

在输入处理,给定帧渲染和合成之后,用户的主线程就会变得空闲,直到下一帧的开始,或者有优先的待处理任务,或者是存在输入。用户所看到的页面都是一帧一帧渲染出来的,下图中可以看到一帧的过程:

虽然上面是介绍 requestAnimationFrame 时用到的图片,但是也很详细的介绍到一帧里面浏览器都做了些什么,一帧里面除了上面干的活外就是空余时间 idle 了。可以看看 W3C requestidlecallback 的介绍:

Layout 和 paint 就是我们常见的重排和重绘,也就是 render 的部分,而 Task 就包含了各种任务,包括输入处理,js 处理等等。完成这些事情还有多少时间剩余呢?一般是按照 60fps 来处理的。至于为什么是 60fps 呢,大多数浏览器遵循 W3C 所建议的刷新频率也就是 60fps 了,requestAnimationFrame 里面就是按照频率来的。60 fps 就已经能够保证看到的动画不会一卡一卡了。所以在一帧 16.66ms 的时间里面空闲的时间就是 requestIdleCallback 调用处理的阶段了。

requestIdleCallback 参数

MDN 里面介绍到语法,这里再提一下:

var handle = window.requestIdleCallback(callback[, options]);

callback 是要执行的回调函数,会传入 deadline 对象作为参数,deadline 包含:

  1. timeRemaining:剩余时间,单位 ms,指的是该帧剩余时间。
  2. didTimeout:布尔型,true 表示该帧里面没有执行回调,超时了。

options 里面有个重要参数 timeout,如果给定 timeout,那到了时间,不管有没有剩余时间,都会立刻执行回调 callback。

React 中的实现

先看两张 amazing 的图: 首先是 React 16 之前版本的

在之前的版本里面,若 React 要开始更新的时候,就会处于深度调用的状态,程序会一直处理更新,而不会接受处理外部的输入。如果更新的层级多而深则会导致更新时间长的问题。到了 React 16 fiber 的阶段呢,如下所示;

可以明显的看到每次处理完一部分之后,react 都会从非常深的调用栈上看看有没有其他优先要做的事情,有则开始做其他事情,如输入事件等等,结束后再回过头来继续之前的事情。是不是很神奇!这种时间丝滑般的设计,对于大量数据的渲染很有帮助。看看 react 中设计的 requestIdleCallback ployfill。

const localRequestAnimationFrame = requestAnimationFrame;
// 链表头部与尾部
let  headOfPendingCallbacksLinkedList = null;
let  tailOfPendingCallbacksLinkedList = null;
// frameDeadlineObject 为传入callback的参数 deadline
const frameDeadlineObject = {
  didTimeout: false,
  timeRemaining() {
    // 通过 frameDeadline 来判断,该帧剩余时间
    const remaining = frameDeadline - now();
    return remaining > 0 ? remaining : 0;
  },
};
// export 对外函数,也就是 requestIdleCallback ployfill
scheduleWork = function(callback, options) {
  const timeoutTime = now() + options.timeout;
  const scheduledCallbackConfig: CallbackConfigType = {
    scheduledCallback: callback,
    timeoutTime,
    prev: null,
    next: null,
  };
  // 省略将scheduledCallbackConfig插入到链表里面过程
  if (!isAnimationFrameScheduled) {
    isAnimationFrameScheduled = true;
    localRequestAnimationFrame(animationTick);
  }
}
// requestAnimationFrame 调用函数
const animationTick = function(rafTime) {
  isAnimationFrameScheduled = false;
  // 更新 frameDeadline
  frameDeadline = rafTime + activeFrameTime;
  if (!isIdleScheduled) {
    isIdleScheduled = true;
    window.postMessage(messageKey, '*');
  }
}
// 省略消息监听处理部分

// 执行 callback,与传参 deadline
const callUnsafely = function(callbackConfig, arg) {
  const callback = callbackConfig.scheduledCallback;
  callback(arg);
  // 总是会删除调用过的 callbackConfig
  cancelScheduledWork(callbackConfig);
}

cancelScheduledWork = function(callbackConfig) {
  // 在链表中删除对应节点,并维护好pre以及next关系
}

可以看出这里是采用 requestAnimationFrame 来代替 requestIdleCallback,这也很好理解。关键地方在于 deadline 参数的传递。这里用了 frameDeadlineObject 来表示,每次 requestAnimationFrame 的时候都会更新 frameDeadlineObject 对象里面的 frameDeadline 基线。frameDeadline 正如其名,就是每次 requestAnimationFrame 开始的时间以及该帧时长之和。只要 now() 的时间大于它,自然是表示没有空闲时间。

比如在一个帧里面,可能第一个 callback 运行时间过长,frameDeadline - now() 不为正数,则不会无法继续执行。程序会在下一帧开始的时候执行 input 事件什么的,若有空闲时间才执行 idle callback。

上文中通过链表的结构,每次都将传入的 callback 和 timeoutTime 保存起来,以 pre/next 的形式来维系。并在 callUnsafely 里面调用完之后就删除掉。回顾一下上面处理流程,通过 scheduleWork 传入 callback,用 requestAnimationFrame 方式第一次启用 animationTick,并用事件的方式 window.postMessage 消息的方式来调用。其中省略的消息监听的处理如下:

// messageKey 为特点字符串
const idleTick = function(event) {
  if (event.source !== window || event.data !== messageKey) {
    return;
  }
  isIdleScheduled = false;
  callTimedOutCallbacks();
  let currentTime = now();
  // 空闲时间判断
  while (
    frameDeadline - currentTime > 0 &&
    headOfPendingCallbacksLinkedList !== null
  ) {
    const latestCallbackConfig = headOfPendingCallbacksLinkedList;
    frameDeadlineObject.didTimeout = false;
    callUnsafely(latestCallbackConfig, frameDeadlineObject);
    currentTime = now();
  }
  // 继续下一个节点,调用requestAnimationFrame
  if (
    !isAnimationFrameScheduled &&
    headOfPendingCallbacksLinkedList !== null
  ) {
    isAnimationFrameScheduled = true;
    localRequestAnimationFrame(animationTick);
  }
}
window.addEventListener('message', idleTick, false);
// 如果设置了 timeoutTime 的话,自然是无脑执行到底的,而不会把时间让渡予下一帧
const callTimedOutCallbacks = function() {
  const currentTime = now();
  const timedOutCallbacks = [];
  let currentCallbackConfig = headOfPendingCallbacksLinkedList;
  while (currentCallbackConfig !== null) {
    if (timeoutTime !== -1 && timeoutTime <= currentTime) {
      timedOutCallbacks.push(currentCallbackConfig);
    }
  }
  // 存在 timeoutTime 的事件,并且发生超时了,那就执行,不考虑帧的问题了
  if (timedOutCallbacks.length > 0) {
    frameDeadlineObject.didTimeout = true;
    for (let i = 0, len = timedOutCallbacks.length; i < len; i++) {
      callUnsafely(timedOutCallbacks[i], frameDeadlineObject);
    }
  }
}

animationTick 结合 idleTick 形成消息传递事件的发送方和接收方,同时也分别是 requestAnimationFrame 回调函数和触发函数。通过 messageKey 来识别是否是通知的自己。idleTick 里面的循环判断和 timeRemaining 相同,判断是否有空闲时间,有才进行 callUnsafely,执行 callback。

fiber 与 requestIdleCallback

在上面的代码好像都没有看到 timeRemaining 使用的地方哦,其实在 workLoop 里面才会有判断

function workLoop(isYieldy) {
  if(isYield) {
    while (nextUnitOfWork !== null && !shouldYield()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}
function shouldYield() {
  if (deadline === null || deadlineDidExpire) {
    return false;
  }
  if (deadline.timeRemaining() > 1) {
    return false;
  }
  deadlineDidExpire = true;
  return true;
}

对于异步更新的每次执行 performUnitOfWork 前都会判断一次是否有空余时间,有才会继续。通过这个地方判断是否有空闲时间。

前者 idleTick 里面是在初始化的时候判断,能否立马执行 performWork 函数,以及在该帧里面能够执行链表的下一个 performWork。 后则 workLoop 是在 render/reconcilation 阶段的 workLoop 循环里面判断空闲时间,有就继续。当然在第二个阶段 commit 是没有检查空闲时间过程的。从而实现了之前版本没有现实的方式。当一次组件更新时间较长的时候,仍然运行 input 等操作,同时,更重要的是不会发生局部更新。事件能打断的只是 render/reconcilation 阶段,这个阶段不会发生任何的真实 DOM 的变化。这也是调度里面最神奇的地方,空闲时间的检查仅仅发生在 render/reconcilation 阶段。

只是该阶段还是会执行 ComponentWillUpdate 这些生命钩子,well,react 团队表示着没有关系。

于是到了timeRemaining之后,render/reconcilation 阶段就会被打断,继续处理浏览器的其他输入事件。并在输入后执行

expirationTime 与优先级别

之前我们计算 expirationTime,都是按照同步来算的,也就是值为 1(SYNC)。既然是同步自然就不需要调度来控制任务系统了。当 expirationTime !== SYNC 的时候,才进入 requestIdleCallback 的任务调度

function requestWork(root, expirationTime) {
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    // 进入调度
    scheduleCallbackWithExpirationTime(expirationTime);
  }
}

这里的 expirationTime 是 root.expirationTime。也就是说 React 当前是处于同步还是调度模式,是由 root 的 expirationTime 决定的。这也就是说明了其模式分两种,一种是同步,一种是调度。而调度的优先级将取决于 expirationTime。

##总结 本文更多的只是结合 requestIdleCallback 来介绍异步相关过程,但是更多的内容还是没有介绍到,将放在下篇文章 react 开启异步渲染与优先级 介绍到。

参考

  1. W3C requestidlecallback
  2. requestAnimationFrame Scheduling For Nerds