本文副标题是 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. [No00002E]关于大数据,你不知道的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. Ruby Enumerator的各种迭代

    Enumerator迭代 Mix-in Enumerator获得的迭代方法: each_cons: each_slice: each_with_index: with_index: each_with ...

  2. 脚本检测Kafka和Zookeeper

    Java测试环境中Kafka最近总是自动停止,所有写了一个简单的脚本来监听Kafka和Zookeeper,其中Kafka监听端口为9092,Zookeeper监听端口为2181,脚本如下: #!/bi ...

  3. CAN总线学习记录之三:总线中主动错误和被动错误的通俗解释

    首先建议把广泛使用的"主动错误"和"被动错误"概念换成"主动报错"和"被动报错". 1. 主动报错站点 只要检查到错误, ...

  4. Java开发笔记(六十六)映射:HashMap和TreeMap

    前面介绍了两种集合的用法,它们的共性为每个元素都是唯一的,区别在于一个无序一个有序.虽说往集合里面保存数据还算容易,但要从集合中取出数据就没那么方便了,因为集合居然不提供get方法,没有get方法怎么 ...

  5. nginx系列2:搭建nginx环境

    我们选择编译安装nginx. 1,下载nginx 进入nginx的官网下载页面:http://nginx.org/en/download.html 找到稳定版本Stable version的下载入口开 ...

  6. Web前端:博客美化:一、模板美化

    1.选用模板simplememory 2.写css放在 这些会覆盖掉原来的css样式 我是在网上找的css代码二次加工的 : ) /*1.针对simplememory的修改*/ #google_ad_ ...

  7. 控制台程序(C#)不弹出认证窗口连接到Dynamics CRM Online的Web API

    摘要: 本人微信和易信公众号: 微软动态CRM专家罗勇 ,回复271或者20180602可方便获取本文,同时可以在第一间得到我发布的最新的博文信息,follow me!我的网站是 www.luoyon ...

  8. Bitmap上下合成图片

    合成两张图片,上下叠加的效果: /** * 把两个位图覆盖合成为一个位图,以底层位图的长宽为基准 * * @param backBitmap 在底部的位图 * @param frontBitmap 盖 ...

  9. Easyui 修改|新增jquery-easyui icon图标

    修改|新增jquery-easyui icon图标 by:授客 QQ:1033553122 测试环境 jquery-easyui-1.5.3 修改配置文件 打开jquery-easyui-1.5.3\ ...

  10. whistle

    whistle介绍 whistle是基于Node的跨平台web调试代理工具, 主要查看, 修改HTTP, HTTPS, Websocket的请求,响应, 也可以作为HTTP代理服务器使用 (文档)[h ...