你不知道的 requestIdleCallback

时间:2022-09-20 19:24:03

你不知道的 requestIdleCallback

本文副标题是 Request Schedule 源码解析一。在本章中会介绍 requestIdleCallback 的用法以及其缺陷, 接着对 React 团队对该 api 的 hack 部分的源码进行剖析。在下一篇中会结合优先级对 React 的调度算法进行宏观的解释, 欢迎关注个人博客

React 调度算法requestIdleCallback 这个 api 息息相关。requestIdleCallback 的作用是是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务, 其用法如下:

var tasksNum = 10000

requestIdleCallback(unImportWork)

function unImportWork(deadline) {
while (deadline.timeRemaining() && tasksNum > 0) {
console.log(`执行了${10000 - tasksNum + 1}个任务`)
tasksNum--
} if (tasksNum > 0) { // 在未来的帧中继续执行
requestIdleCallback(unImportWork)
}
}

deadline 有两个参数

  • timeRemaining(): 当前帧还剩下多少时间
  • didTimeout: 是否超时

另外 requestIdleCallback 后如果跟上第二个参数 {timeout: ...} 则会强制浏览器在当前帧执行完后执行。

requestIdleCallback 的缺陷

requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work。—— from Releasing Suspense

也就是说 requestIdleCallback 的 FPS 只有 20, 这远远低于页面流畅度的要求!(一般 FPS 为 60 时对用户来说是感觉流程的, 即一帧时间为 16.7 ms), 这也是 React 需要自己实现 requestIdleCallback 的原因。

源码解析之 requestIdleCallback

非 DOM 环境

在不能操作 DOM 的环境下, 可以借助 setTimeout 来模拟 requestIdleCallback 的实现。

requestIdleCallback = (callback) => {
setTimeout(callback({
timeRemaining() {
return Infinity
}
}))
}

下面将 React 源码中关于服务端的实现也呈现出来:

let _callback = null;
const _flushCallback = function (didTimeout) {
if (_callback !== null) {
try {
_callback(didTimeout);
} finally {
_callback = null;
}
}
};
requestHostCallback = function (cb) {
if (_callback !== null) {
// 如果 _callback 不为空, 则将 requestHostCallback 放到下一个事件队列中再次执行
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0, false);
}
};
cancelHostCallback = function () {
_callback = null;
};
shouldYieldToHost = function () {
return false;
};

DOM 环境

在浏览器端的环境下, 介绍一个与 requestIdleCallback 功能相近的 api —— requestAnimationFrame(callback), 其会在下次重绘前执行指定的回调函数,因此这个 api 在动效领域得到了广泛的使用。下面通过一个简单的 demo 来认识它:

let frame
let n = 5
function callback(timeStamp) {
console.log(timeStamp) // 开始执行回调的时间戳
// 如果想要产生循环动画的效果, 需在回调函数中再次调用 requestAnimationFrame()
while (n > 0) {
requestAnimationFrame(callback)
console.log('测试执行顺序')
n--
}
} frame = requestAnimationFrame(callback) // 在下次重绘之前调用回调 // 如果想要销毁该回调, 可以执行 cancelAnimationFrame(frame)

执行上述代码, 控制台(chrome)打印如下数据:

先输出 5 次 '测试执行顺序'
1795953.649
1795970.318
1795986.987
1796003.656
1796020.325
...

可以看到在浏览器上一帧的时间大致为 16ms。同时可以看到 requestAnimation(callback) 中的 callback 也是异步的(只不过它是基于帧与帧间的异步), 所以上述打印结果是先打印出 5 次 '测试执行顺序' 后再依次打印出 5 个时间戳。

requestHostCallback(也就是 requestIdleCallback) 这部分源码的实现比较复杂, 可以将其分解为以下几个重要的步骤(有一些细节点可以看注释):

步骤一: 如果有优先级更高的任务, 则通过 postMessage 触发步骤四, 否则如果 requestAnimationFrame 在当前帧没有安排任务, 则开始一个帧的流程;

步骤二: 在一个帧的流程中调用 requestAnimationFrameWithTimeout 函数, 该函数调用了 requestAnimationFrame, 并对执行时间超过 100ms 的任务用 setTimeout 放到下一个事件队列中处理;

步骤三: 执行 requestAnimationFrame 中的回调函数 animationTick, 在该回调函数中得到当前帧的截止时间 frameDeadline, 并通过 postMessage 触发步骤四;

步骤四: 通过 onmessage 接受 postMessage 指令, 触发消息事件的执行。在 onmessage 函数中根据 frameDeadline - currentTime <= 0 判断任务是否可以在当前帧执行,如果可以的话执行该任务, 否则进入下一帧的调用。

export let requestHostCallback;
export let cancelHostCallback;
export let shouldYieldToHost;
export let getCurrentTime; const ANIMATION_FRAME_TIMEOUT = 100;
let rAFID;
let rAFTimeoutID;
// ② 调用 requestAnimationFrame, 并对执行时间超过 100 ms 的任务用 setTimeout 进行处理
const requestAnimationFrameWithTimeout = function (callback) {
rAFID = requestAnimationFrame(function (timestamp) {
clearTimeout(rAFTimeoutID);
callback(timestamp); // 一帧中任务调用的核心流程的实现, 接着看第 ③ 步
});
// 如果在一帧中某个任务执行时间超过 100 ms 则终止该帧的执行并将该任务放入下一个事件队列中
rAFTimeoutID = setTimeout(function () {
cancelAnimationFrame(rAFID);
callback(getCurrentTime());
}, ANIMATION_FRAME_TIMEOUT);
}; getCurrentTime = function () {
return performance.now();
}; let scheduledHostCallback = null; // 调度器回调函数
let isMessageEventScheduled = false; // 消息事件是否执行
let timeoutTime = -1; let isAnimationFrameScheduled = false; let isFlushingHostCallback = false; let frameDeadline = 0; // 当前帧的截止时间 // 假设最开始的 FPS(feet per seconds) 为 30, 但这个值会随着动画帧调用的频率而动态变化
let previousFrameTime = 33; // 一帧的时间: 1000 / 30 ≈ 33
let activeFrameTime = 33; shouldYieldToHost = function () {
return frameDeadline <= getCurrentTime();
}; const channel = new MessageChannel();
const port = channel.port2;
// ④ 接受 `postMessage` 指令, 触发消息事件的执行。在其中判断任务是否在当前帧执行,如果在的话执行该任务
channel.port1.onmessage = function (event) {
isMessageEventScheduled = false; const prevScheduledCallback = scheduledHostCallback;
const prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1; const currentTime = getCurrentTime(); let didTimeout = false; // 是否超时
// 如果当前帧已经没有时间剩余, 检查是否有 timeout 参数,如果有的话是否已经超过这个时间
if (frameDeadline - currentTime <= 0) {
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// didTimeout 为 true 后, 在当前帧中执行(针对优先级较高的任务)
didTimeout = true;
} else {
// 在下一帧中执行
if (!isAnimationFrameScheduled) {
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
scheduledHostCallback = prevScheduledCallback;
timeoutTime = prevTimeoutTime;
return;
}
} if (prevScheduledCallback !== null) {
isFlushingHostCallback = true;
try {
prevScheduledCallback(didTimeout);
} finally {
isFlushingHostCallback = false;
}
}
}; // ③ requestAnimationFrame 的回调函数。传入的 rafTime 为执行该帧的时间戳。
const animationTick = function (rafTime) {
// 如果存在调度器回调函数则在一帧的开头急切地安排下一帧的动画回调(急切是因为如果在帧的后半段安排动画回调的话, 就会增大下一帧超过 100ms 的几率, 从而会浪费一个帧的利用, 可以结合步骤②来理解这句话), 如果不存在调度器回调函数否则立马终止执行。
if (scheduledHostCallback !== null) {
requestAnimationFrameWithTimeout(animationTick);
} else {
isAnimationFrameScheduled = false;
return;
} let nextFrameTime = rafTime - frameDeadline + activeFrameTime; // 当前帧开始调用动画的时间 - 上一帧调用动画的截止时间 + 当前帧执行的时间,这里的 nextFrameTime 仅仅是临时变量
// 如果连续两帧的时间都小于当前帧的时间, 则说明得调高 FPS
if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
// 将 activeFrameTime 的值减小相当于调高 FPS。同时取 nextFrameTime 与 previousFrameTime 中较大的一个以让前后两帧都不出问题。
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime; // 当前帧的截止时间(上面几行代码的目的是得到该 frameDeadline 值, 该值在 postMessage 会用来判断)
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined); // 最后进入第④步, 通过 postMessage 触发消息事件。
}
}; // DOM 环境下 requestIdleCallback 的实现, 这里第二个参数在最新的 requestIdleCallback 中因为对象类型
requestHostCallback = function (callback, absoluteTimeout) {
scheduledHostCallback = callback; // 这里的 callback 为调度器回调函数
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0) {
// 针对优先级较高的任务不等下一个帧,在当前帧通过 postMessage 尽快执行
port.postMessage(undefined);
} else if (!isAnimationFrameScheduled) {
// ① 如果 rAF 在当前帧没有安排任务, 则开始一个帧的流程
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
}; cancelHostCallback = function () {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};

相关资料

你不知道的 requestIdleCallback的更多相关文章

  1. MySQL 系列(三)你不知道的 视图、触发器、存储过程、函数、事务、索引、语句

    第一篇:MySQL 系列(一) 生产标准线上环境安装配置案例及棘手问题解决 第二篇:MySQL 系列(二) 你不知道的数据库操作 第三篇:MySQL 系列(三)你不知道的 视图.触发器.存储过程.函数 ...

  2. MySQL 系列(二) 你不知道的数据库操作

    第一篇:MySQL 系列(一) 生产标准线上环境安装配置案例及棘手问题解决 第二篇:MySQL 系列(二) 你不知道的数据库操作 本章内容: 查看\创建\使用\删除 数据库 用户管理及授权实战 局域网 ...

  3. 《你不知道的JavaScript》整理(二)——this

    最近在读一本进阶的JavaScript的书<你不知道的JavaScript(上卷)>,这次研究了一下“this”. 当一个函数被调用时,会创建一个活动记录(执行上下文). 这个记录会包含函 ...

  4. 《你不知道的JavaScript》整理(一)——作用域、提升与闭包

    最近在读一本进阶的JavaScript的书<你不知道的JavaScript(上卷)>,里面分析了很多基础性的概念. 可以更全面深入的理解JavaScript深层面的知识点. 一.函数作用域 ...

  5. 你不知道的Javascript(上卷)读书笔记之一 ---- 作用域

    你不知道的Javascript(上卷)这本书在我看来是一本还不错的书籍,这本书用比较简洁的语言来描述Js的那些"坑",在这里写一些博客记录一下笔记以便消化吸收. 1 编译原理 在此 ...

  6. &lbrack;No00002E&rsqb;关于大数据&comma;你不知道的6个迷思

    还是那个观点:计算机,编程语言,互联网,大数据等等都只是工具! 导语:看过美剧<纸牌屋>没?知道这部"白宫甄嬛传"为什么会火吗?靠的是大!数!据! 过去两年,在 Net ...

  7. 开源 VS 商业,消息中间件你不知道的那些事

    11月23日,新炬网络中间件技术专家刘拓老师在DBA+社群中间件用户组进行了一次主题为“开源 VS 商业,消息中间件你不知道的那些事”的线上分享.小编特别整理出其中精华内容,供大家学习交流. 嘉宾简介 ...

  8. 另5个你不知道的HTML5接口API

    原文地址: 5 More HTML5 APIs You Didn’t Know Existed 演示地址: 全屏API Demo 原文日期: 2012年11月08日 翻译日期: 2013年08月13日 ...

  9. 屌炸天实战 MySQL 系列教程(二) 史上最屌、你不知道的数据库操作

    此篇写MySQL中最基础,也是最重要的操作! 第一篇:屌炸天实战 MySQL 系列教程(一) 生产标准线上环境安装配置案例及棘手问题解决 第二篇:屌炸天实战 MySQL 系列教程(二) 史上最屌.你不 ...

随机推荐

  1. 8 TIPS TO BECOME A MICROSOFT TECHNICAL EVANGELIST

    8 TIPS TO BECOME A MICROSOFT TECHNICAL EVANGELIST By Thomas Lewis February, 28 2012 I have had a pai ...

  2. 9&period; Linux远程登录

    1. 检查网络是否通畅 C:\Users\cfm>ping 192.168.232.131 正在 Ping 192.168.232.131 具有 32 字节的数据:来自 192.168.232. ...

  3. 一个Java对象到底占用多大内存?

    最近在读<深入理解Java虚拟机>,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存? 在网上搜到了一篇博客讲的非常好 ...

  4. Android-Activity生命周期从onStart直接到onStop

    一般应用场景中,onStart执行后都是要执行onResume,但是如果在onStart中调用了finish,会直接执行onStop.

  5. IO库 8&period;3

    题目:什么情况下,下面的while循环会终止? while(cin >> i) /* ... */ 解答:当读取发生错误时上述while循环会终止.比如i是整形,却输入非整形的数:输入文件 ...

  6. 【flex弹性盒布局】------这个强大的功能

    你知道flex弹性布局么? 我们先来了解它的概念:Flex是Flexible Box的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性. 任何一个容器都可以指定为Flex布局 ...

  7. 【STM32H7教程】第13章 STM32H7启动过程详解

    完整教程下载地址:http://forum.armfly.com/forum.php?mod=viewthread&tid=86980 第13章       STM32H7启动过程详解 本章教 ...

  8. linux-Centos7安装python3并与python2共存

    1.查看是否已经安装Python CentOS 7.2 默认安装了python2.7.5 因为一些命令要用它比如yum 它使用的是python2.7.5. 使用 python -V 命令查看一下是否安 ...

  9. docker使用技巧小记

    1.在使用docker的时候有很多人习惯使用官方镜像.有的人喜欢自己制作镜像,有的时候都是使用默认的配置启动的服务,或者自己在制作镜像的时候直接将配置文件打包到镜像里面了.有的时候会碰到要修改配置文件 ...

  10. 基于jquery实现页面loading加载效果

    实现loading 加载提示 ······ 透明遮罩 居中效果 具体代码如下: CSS样式部分: <style type="text/css"> .background ...