看了神光大大 @zxg_神说要有光 的文章 juejin.cn/post/715535… ,使用 Performance 工具深入理解事件循环。这真是个好办法,看 spec 看半天不如看一眼 performance 的 trace!但问题来了,我知道这玩意是这个顺序,面试题也背了一堆,但有什么用?
举个例子
我们有这么一段代码
const sleep = (ms) => {
let s = Date.now();
while (Date.now() - s < ms) {
}
};
const doit = () => {
sleep(1000);
Promise.resolve().then(() => {
sleep(1000);
});
setTimeout(() => {
sleep(1000);
});
requestAnimationFrame(() => {
sleep(1000);
});
};
使用 performance 工具查看运行过程,他长这样:
ok,那我们开始
微任务
新建一个微任务很简单:
Promise.resolve().then(() => {
// 微任务在此运行
});
从图中可以看出,“当前任务”运行结束,紧接微任务就会执行。
那么可以用来干嘛呢?
可见/可交互耗时统计。 lighthouse 只能给你一个大概的参考时间,无法用来做稳定的线上指标追踪。比如你想统计 TTI,那么数据回来后, React 渲染结束,此时就可以认为是 TTI。那么用微任务就很好,刚好是这个时间点,比如:
function main() {
const [ data, setData ] = useState([]);
useEffect(() => {
request().then((data) => {
setData(data);
Promise.resolve().then(() => {
reportTTI(performance.now());
});
})
});
return (
<ul>
{...data.map(e => <li>{e}</li>}
</ul>
);
}
坏消息是,这样 hack 了 react 的实现,这默认 setData 不会被分到多个任务中执行。但 react 不会天天升级,这样大体还是够用的。
屎山代码 bug 修复: 不再维护的屎山代码的 bug 修复,时序逻辑复杂得过了分,你需要保住自己的工作。
宏任务
// 宏任务在此运行
function nextTick(callback) {
setTimeout(() => {
callback();
});
}
// 另外一种新建方式
// 比 setTimeout 好,因为递归调用 setTimeout 会间隔越来越长。
function nextTick(callback) {
const { port1, port2 } = new MessageChannel();
port1.onmessage = callback;
port2.postMessage();
}
Note: 仅为了演示,省略了一堆细节。
这个比较常见,很多人会使用这个来做任务分片。比如 react-scheduler,让出主进程的时间,将 cpu 留给绘制,比如这篇:zhuanlan.zhihu.com/p/570318956
这里介绍一个更有意思的玩法,优化首屏。
举一个场景,你刚接手一个项目,发现首屏很慢,你发现埋点上报的耗时很长,举个例子:
function firstScreen() {
task1();
collectData();
task2();
collectData();
task3();
}
我们看一下 performance
哇,首屏一共 2.5s,两个埋点上报就要 100ms * 2,这怎么忍?
好消息是,咨询了一波,大家都认为埋点的实时性不重要,那么我们有办法让 collectData 不在首屏执行吗?我们改下代码:
function firstScreen() {
task1();
nextTick(collectData);
task2();
nextTick(collectData);
task3();
}
也就是说,我们让 collectData 在下一个宏任务内执行。 再看看 performance。
优化前:
优化后:
哈,collectData 被成功挪到了首屏后!而且只要 10 分钟搞定。
一种特殊的调度方式 requestAnimationFrame
其实不太推荐用这种方法调度,因为 requestAnimationFrame 有个致命缺点 --- 如果页面不可见,requestAnimationFrame 的回调函数也不会执行。 spec 是这么说的:paint 之前执行 requestAnimationFrame,这也意味着 requestAnimationFrame 什么时候触发全看浏览器实现。
如果你需要编排 requestAnimationFrame 和宏任务的时序,这意味着噩梦:
举个 bad case:
function howLoopWork(ms) {
const s = Date.now();
requestAnimationFrame(() => {
console.log(2);
});
while(Date.now() - s < ms) {};
setTimeout(() => {
console.log(1);
});
}
howLoopWork(10); // log: 1 2
// 等一会再执行
howLoopWork(100); // log: 2 1
你以为这就结束了?我们就变个位置:
function howLoopWork(ms) {
const s = Date.now();
while(Date.now() - s < ms) {};
setTimeout(() => {
console.log(1);
});
requestAnimationFrame(() => {
console.log(2);
});
}
howLoopWork(10); // log: 1 2
// 等一会再执行
howLoopWork(100); // log: 1 2
那 requestAnimationFrame 除了做动画和绘制前改 DOM 就没卵用了吗?
我们再举几个 requestAnimationFrame 的神秘表现:
function howLoopWork() {
requestAnimationFrame(() => console.log(1));
requestAnimationFrame(() => console.log(2));
setTimeout(() => {
const s = Date.now();
console.log(3);
while(Date.now() - s < 10) {};
});
}
howLoopWork(); // 1 2 3
这次恭喜你,结果必然是 1 2,在一个任务内执行多个 requestAnimationFrame,那么他会在收益 paint 时按序触发。
那么再改一下:
function howLoopWork() {
requestAnimationFrame(() => {
console.log(1);
requestAnimationFrame(() => console.log(2)); // 第 4 行
});
setTimeout(() => {
const s = Date.now();
while(Date.now() - s < 10) {};
});
}
howLoopWork(); // 1 3 2
哈哈,这次有点不同,为什么呢?
和微任务不一样,在 callback 内嵌套调用 requestAnimationFrame,也就是第 4 行,嵌套的 callback 会在下一次 paint 执行! 这么说有点抽象,我们看看 performance:
好吧,知道了又怎么样。可以监控首屏啊。
我们知道一次任务的流程大概是:
当前任务执行 -> requestAnimationFrame -> paint
那么无论是微任务还是 requestAnimationFrame 都无法监控到 paint 阶段的耗时,这么说嵌套 requestAnimationFrame 就可以,但问题也很明显:
- 页面不可见时凉凉,间隔会变得异常长
- 中间宏任务插入过多也凉,下次 paint 被延后许多
我们实践了一段时间后果断放弃了这种做法,乖乖使用微任务的方式,因为我们的 paint 耗时极短。
这个章节就权当给大家介绍了一个有趣的技术游戏。
再来一道面试题
function howLoopWork() {
let a = 0;
setTimeout(() => {
a = 1;
setTimeout(() => {
console.log('4', a);
});
});
Promise.resolve().then(() => {
console.log('1', a);
Promise.resolve().then(() => {
const s = Date.now();
while(Date.now() - s < 16) {};
console.log('2', a);
});
});
requestAnimationFrame(() => {
console.log('3', a);
requestAnimationFrame(() => {
console.log('5', a);
});
});
console.log('0', a);
}
howLoopWork();
答案:
你掌握了吗~
Note:以上均是我的日常工作总结,欢迎加我好友,一起玩啊