你不知道的 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的更多相关文章
- MySQL 系列(三)你不知道的 视图、触发器、存储过程、函数、事务、索引、语句
第一篇:MySQL 系列(一) 生产标准线上环境安装配置案例及棘手问题解决 第二篇:MySQL 系列(二) 你不知道的数据库操作 第三篇:MySQL 系列(三)你不知道的 视图.触发器.存储过程.函数 ...
- MySQL 系列(二) 你不知道的数据库操作
第一篇:MySQL 系列(一) 生产标准线上环境安装配置案例及棘手问题解决 第二篇:MySQL 系列(二) 你不知道的数据库操作 本章内容: 查看\创建\使用\删除 数据库 用户管理及授权实战 局域网 ...
- 《你不知道的JavaScript》整理(二)——this
最近在读一本进阶的JavaScript的书<你不知道的JavaScript(上卷)>,这次研究了一下“this”. 当一个函数被调用时,会创建一个活动记录(执行上下文). 这个记录会包含函 ...
- 《你不知道的JavaScript》整理(一)——作用域、提升与闭包
最近在读一本进阶的JavaScript的书<你不知道的JavaScript(上卷)>,里面分析了很多基础性的概念. 可以更全面深入的理解JavaScript深层面的知识点. 一.函数作用域 ...
- 你不知道的Javascript(上卷)读书笔记之一 ---- 作用域
你不知道的Javascript(上卷)这本书在我看来是一本还不错的书籍,这本书用比较简洁的语言来描述Js的那些"坑",在这里写一些博客记录一下笔记以便消化吸收. 1 编译原理 在此 ...
- [No00002E]关于大数据,你不知道的6个迷思
还是那个观点:计算机,编程语言,互联网,大数据等等都只是工具! 导语:看过美剧<纸牌屋>没?知道这部"白宫甄嬛传"为什么会火吗?靠的是大!数!据! 过去两年,在 Net ...
- 开源 VS 商业,消息中间件你不知道的那些事
11月23日,新炬网络中间件技术专家刘拓老师在DBA+社群中间件用户组进行了一次主题为“开源 VS 商业,消息中间件你不知道的那些事”的线上分享.小编特别整理出其中精华内容,供大家学习交流. 嘉宾简介 ...
- 另5个你不知道的HTML5接口API
原文地址: 5 More HTML5 APIs You Didn’t Know Existed 演示地址: 全屏API Demo 原文日期: 2012年11月08日 翻译日期: 2013年08月13日 ...
- 屌炸天实战 MySQL 系列教程(二) 史上最屌、你不知道的数据库操作
此篇写MySQL中最基础,也是最重要的操作! 第一篇:屌炸天实战 MySQL 系列教程(一) 生产标准线上环境安装配置案例及棘手问题解决 第二篇:屌炸天实战 MySQL 系列教程(二) 史上最屌.你不 ...
随机推荐
- jstat命令查看jvm的GC情况 (以Linux为例)
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量.命令的格式如下: jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数] 注意!!!:使用的jdk版本是jdk8. ...
- 【Parallel】.Net 并行执行程序的使用心得
一.摘要 官方介绍:提供对并行循环和区域的支持. 命名空间:using System.Threading.Tasks 三个静态方法:Parallel.Invoke,Parallel.For,Paral ...
- java servlet的执行流程
1.先附上代码如下 Servlet1.java public class Servlet1 implements Servlet { @Override public void init(Servle ...
- 使用bfd监控静态路由,达到网络故障及时切换功能。
结论:通过BFD可以联动静态路由,从而监控整个网络上的网络情况,当出现故障时及时进行切换. 下面的例子,就是通过BFD监控上面的这个往返路由,当中间网络出现故障时,两端全部切换到下面的第二条路由进行通 ...
- Android 常驻广播和非常驻广播
一.知识准备 ①常驻广播接受者:使用AndroidManifest.xml注册,接受者不随Activity的销毁而销毁,也就是拥有独立的生命周期. ②非常驻广播接受者:使用registerReceiv ...
- linux下编译opencv
1.安装cmake:sudo apt-get install cmake 2.下载opencv-2.4.10.zip,解压 3.编译opencv cd /home/opencv-2.4.10 mkdi ...
- ffmpeg相关函数整理
1.av_read_frame() 该函数用于读取具体的音/视频帧数据,从流中读取数据帧到 AVPacket,AVPacket保存仍然是未解码的数据 int av_read_frame(AVForma ...
- Session session = connection.createSession(paramA,paramB);参数解析
Session session = connection.createSession(paramA,paramB); paramA是设置事务,paramB是设置acknowledgment mode ...
- mssql sqlserver 使用sql脚本 清空所有数据库表数据的方法分享
摘要: 下文讲述清空数据库中所有表信息的方法分享,如下所示: 实验环境:sql server 2008 实现思路: 1.禁用所有约束,外键 2.禁用所有触发器 3.删除表数据 4.开启触发器 5.开启 ...
- drf 教程
1, 序列化 Serialization 创建一个新环境 在做其他事之前,我们会用virtualenv创建一个新的虚拟环境.这将确保我们的包配置与我们正在工作的其他项目完全隔离. virtualenv ...