看过开光大大的 @zxg_神说要有光 的帖子 Admin.net/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 3D渲染完毕,这时就能称之为 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();
}
// 宏任务在这里运作
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,2个埋点汇报就需要 100ms * 2,这怎么忍?
好消息是,资询了一波,很多人都认为埋点的实用性并不重要,那我们有方法让 collectData 没有在首屏实行吗?大家改下编码:
function firstScreen() {
task1();
nextTick(collectData);
task2();
nextTick(collectData);
task3();
}
换句话说,大家让 collectData 在下一个宏任务内实行。 再看一下 performance。
提升前:
提升后:
哈,collectData 被成功移到了首屏后!所以只要 10 min解决。
一种特殊的生产调度方法 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();
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:之上都为我日常工作汇报,热烈欢迎加我好友,一起玩啊