Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • 调和(Reconciliation)

  • 调度器(Scheduler)

    • 开始上手
    • scheduleCallback与调度任务
    • schedulerHostConfig
      • 目录
      • 非 DOM 环境
      • requestHostCallback:请求主线程回调
      • cancelHostCallback
      • requestHostTimeout:请求主线程延时回调
      • cancelHostTimeout:取消主线程延迟回调
    • scheduler 顶层 API
  • 更新器(Updater)

  • 渲染器(Render)

  • hooks原理

  • 总结

  • React源码漂流记

  • react
  • 调度器(Scheduler)
jonsam
2022-04-14
目录

schedulerHostConfig

# 目录

  • 目录
  • 非 DOM 环境
  • requestHostCallback:请求主线程回调
    • postMessage:发送执行消息
    • performWorkUntilDeadline: 执行回调函数
    • onAnimationFrame
  • cancelHostCallback
  • requestHostTimeout:请求主线程延时回调
  • cancelHostTimeout:取消主线程延迟回调

# 非 DOM 环境

标签: 了解

在非 DOM 环境中,不存在 rAF Api,因此采用了原生的 setTimeout 来模拟。这里主要用于 node 环境。

if (
  // If Scheduler runs in a non-DOM environment, it falls back to a naive
  // implementation using setTimeout.
  // 在非 DOM 环境中,回退到 setTimeout 的原生实现,因为 非 DOM 环境没有 rAF Api。
  typeof window === 'undefined' ||
  // Check if MessageChannel is supported, too.
  // DOM 环境要用到 MessageChannel API
  // 如果 window 和MessageChannel 都不存在,就是用原生 setTimeout 模拟
  typeof MessageChannel !== 'function'
) {
  // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
  // fallback to a naive implementation.
  let _callback = null;
  let _timeoutID = null;
  // 如果有 callback 就执行,执行失败就尝试空闲时段重新执行
  const _flushCallback = function() {
    if (_callback !== null) {
      try {
        const currentTime = getCurrentTime();
        const hasRemainingTime = true;
        _callback(hasRemainingTime, currentTime);
        _callback = null;
      } catch (e) {
        setTimeout(_flushCallback, 0);
        throw e;
      }
    }
  };
  // Scheduler 初始化的时间
  const initialTime = Date.now();
  // 距离初始化的时间差
  getCurrentTime = function() {
    return Date.now() - initialTime;
  };
  // 请求主线程回调
  requestHostCallback = function(cb) {
    if (_callback !== null) {
      // Protect against re-entrancy.
      // 这里将 requestHostCallback 作为定时器的回调传入,延迟 0 毫秒表示回调将在空闲时间立即执行,
      // 执行时将 cb 作为参数传入
      // 如果 callback 还未 flush,就尝试在空闲时间重新请求回调(依次循环)
      setTimeout(requestHostCallback, 0, cb);
    } else {
      // callback 为空时挂载 cb,并且在空闲时间执行 callback
      _callback = cb;
      setTimeout(_flushCallback, 0);
    }
  };
  // 取消主线程当前的回调,当前即时回调 由 _callback 管理
  cancelHostCallback = function() {
    _callback = null;
  };
  // 请求主线程延时回调,直接调用 setTimeout
  requestHostTimeout = function(cb, ms) {
    _timeoutID = setTimeout(cb, ms);
  };
  // 取消主线程当前延时回调,当前延时回调由 _timeoutID 管理
  cancelHostTimeout = function() {
    clearTimeout(_timeoutID);
  };
  shouldYieldToHost = function() {
    return false;
  };
  // 请求重绘
  requestPaint = forceFrameRate = function() {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

在 DOM 环境中,将会以 rAF 和 setTimeout 模拟。

# requestHostCallback:请求主线程回调

标签: 重要
// 请求主线程即时回调
requestHostCallback = function (callback) {
  // 保存下当前的 callback
  scheduledHostCallback = callback;
  if (enableMessageLoopImplementation) {
    // 如果当前还没有打开消息循环,说明没有其他消息回调在处理,可以打开消息循环(加锁),并发出消息
    if (!isMessageLoopRunning) {
      isMessageLoopRunning = true;
      port.postMessage(null);
    }
  } else {
    // 如果enableMessageLoopImplementation为 false,即没有打开消息循环的特性(不使用 messageChannel Api),就直接用 RAF 循环实现
    if (!isRAFLoopRunning) {
      // Start a rAF loop.
      isRAFLoopRunning = true;
      // rAFTime 指的是 performance.now() 的时间
      requestAnimationFrame(rAFTime => {
        onAnimationFrame(rAFTime);
      });
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这个函数的作用是请求主线程即时回调。这里根据 enableMessageLoopImplementation 消息循环机制的实现是否开启分成了两种方式。我们主要来看消息循环实现的这种。

# postMessage:发送执行消息

// see: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
const channel = new MessageChannel();
const port = channel.port2;
// 当 port 上发消息时,会执行 performWorkUntilDeadline
channel.port1.onmessage = performWorkUntilDeadline;
1
2
3
4
5

这里使用了 MessageChannel API,兼容性很好。能够在 port1 和 post2 之间发送和接受消息。这里 port1 监听到 port 发消息就会执行 performWorkUntilDeadline。

# performWorkUntilDeadline: 执行回调函数

performWorkUntilDeadline 函数是调度任务的执行者,

// 执行回调任务
const performWorkUntilDeadline = () => {
  if (enableMessageLoopImplementation) {
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      // Yield after `frameLength` ms, regardless of where we are in the vsync
      // cycle(硬件设备的频率). This means there's always time remaining at the beginning of
      // the message event.
      // 在 frameLength 之后 yield,因此在消息时间开始时有剩余的时间
      frameDeadline = currentTime + frameLength;
      const hasTimeRemaining = true;
      // 调用回调函数,回调函数返回布尔值表示是否中断
      try {
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        // 没有更多任务,清空 scheduledHostCallback
        // 结合到 react 逻辑,如果没有中断,本次回调结束
        if (!hasMoreWork) {
          isMessageLoopRunning = false;
          scheduledHostCallback = null;
        } else {
          // If there's more work, schedule the next message event at the end
          // of the preceding one.
          // 如果有更多的任务,继续发起下一个事件回调
          // 结合 react 逻辑,如果有中断,在发一次消息,排除一个执行者
          port.postMessage(null);
        }
      } catch (error) {
        // If a scheduler task throws, exit the current browser task so the
        // error can be observed.
        // 如果当前回调调用失败,继续发起下一个事件消息
        port.postMessage(null);
        throw error;
      }
    } else {
      // 没有回调就把消息锁打开
      isMessageLoopRunning = false;
    }
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    needsPaint = false;
  } else {
    // 如果没有打开 MessageLoop 特性,就 不会自动发起下一个事件回调,由上层函数每帧检查进行回调
    if (scheduledHostCallback !== null) {
      const currentTime = getCurrentTime();
      const hasTimeRemaining = frameDeadline - currentTime > 0;
      try {
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        if (!hasMoreWork) {
          scheduledHostCallback = null;
        }
      } catch (error) {
        // If a scheduler task throws, exit the current browser task so the
        // error can be observed, and post a new task as soon as possible
        // so we can continue where we left off.
        port.postMessage(null);
        throw error;
      }
    }
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    needsPaint = false;
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

# onAnimationFrame

const onAnimationFrame = rAFTime => {
  // 没有回调任务
  if (scheduledHostCallback === null) {
    // No scheduled work. Exit.
    prevRAFTime = -1;
    prevRAFInterval = -1;
    isRAFLoopRunning = false;
    return;
  }

  // Eagerly schedule the next animation callback at the beginning of the
  // frame. If the scheduler queue is not empty at the end of the frame, it
  // will continue flushing inside that callback. If the queue *is* empty,
  // then it will exit immediately. Posting the callback at the start of the
  // frame ensures it's fired within the earliest possible frame. If we
  // waited until the end of the frame to post the callback, we risk the
  // browser skipping a frame and not firing the callback until the frame
  // after that.
  // 在帧首提前调度下一个动画回调。如果在帧尾调度队列非空,将会在此次回调时继续执行其他的回调。
  // 如果调度队列为空,则立即退出。在帧首调节这个回调以保证他会在尽可能早的帧里被触发。
  // 如果等到帧尾在提交回调,可能会导致浏览器有跳帧和没有在帧尾触发回调的风险。
  isRAFLoopRunning = true;
  requestAnimationFrame(nextRAFTime => {\=
    // 在下一帧中清除定时器并再次执行onAnimationFrame
    clearTimeout(rAFTimeoutID);
    onAnimationFrame(nextRAFTime);
  });

  // requestAnimationFrame is throttled when the tab is backgrounded. We
  // don't want to stop working entirely. So we'll fallback to a timeout loop.
  // TODO: Need a better heuristic for backgrounded work.
  // 当 tab 页在后台时,rAF 将会被节流。我们并不想完全停止,所以这时回退到 setTimeout。
  const onTimeout = () => {
    frameDeadline = getCurrentTime() + frameLength / 2;
    performWorkUntilDeadline();
    rAFTimeoutID = setTimeout(onTimeout, frameLength * 3);
  };
  rAFTimeoutID = setTimeout(onTimeout, frameLength * 3);

  if (
    prevRAFTime !== -1 &&
    // Make sure this rAF time is different from the previous one. This check
    // could fail if two rAFs fire in the same frame.
    // 保证新的 rAF 并非原来的 rAF。这里意思是 rAFTime 和 prevRAFTime 不在同一帧。
    rAFTime - prevRAFTime > 0.1
  ) {
    const rAFInterval = rAFTime - prevRAFTime;
    if (!fpsLocked && prevRAFInterval !== -1) {
      // We've observed two consecutive frame intervals. We'll use this to
      // dynamically adjust the frame rate.

      // If one frame goes long, then the next one can be short to catch up.
      // If two frames are short in a row, then that's an indication that we
      // actually have a higher frame rate than what we're currently
      // optimizing. For example, if we're running on 120hz display or 90hz VR
      // display. Take the max of the two in case one of them was an anomaly
      // due to missed frame deadlines.
      // 观察到两个连续的帧间隔,将以此动态调整帧率
      // 如果两次的帧时间间隔都小于 frameLength,说明浏览器 CPU 资源富足(帧率较高),将降低 yield 的间隔 frameLength。
      // 根据连续两帧之间的间隔时间动态调整 frameLength
      if (rAFInterval < frameLength && prevRAFInterval < frameLength) {
        // 调整为两者中较大值
        frameLength =
          rAFInterval < prevRAFInterval ? prevRAFInterval : rAFInterval;
        // 最小只能降低到 8.33
        if (frameLength < 8.33) {
          // Defensive coding. We don't support higher frame rates than 120hz.
          // If the calculated frame length gets lower than 8, it is probably
          // a bug.
          // 最多只能支持 120 赫兹的帧率,
          frameLength = 8.33;
        }
      }
    }
    prevRAFInterval = rAFInterval;
  }
  // 保存上一次的rAFTime并更新frameDeadline
  prevRAFTime = rAFTime;
  frameDeadline = rAFTime + frameLength;

  // We use the postMessage trick to defer idle work until after the repaint.
  // 发出回调消息
  // 发消息只是检查有无回调任务,有则执行。
  port.postMessage(null);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

# cancelHostCallback

cancelHostCallback = function() {
  scheduledHostCallback = null;
};
1
2
3

# requestHostTimeout:请求主线程延时回调

// 请求主线程延时回调。这里直接用了 setTimeout。
requestHostTimeout = function(callback, ms) {
  taskTimeoutID = setTimeout(() => {
    callback(getCurrentTime());
  }, ms);
};
1
2
3
4
5
6

# cancelHostTimeout:取消主线程延迟回调

cancelHostTimeout = function() {
  clearTimeout(taskTimeoutID);
  taskTimeoutID = -1;
};
1
2
3
4
编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56
scheduleCallback与调度任务
scheduler 顶层 API

← scheduleCallback与调度任务 scheduler 顶层 API→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式