最近在优化之前的练习代码时想到了半年前的一个小插曲。
当时我在掘金发了第二篇文章 -- 《不懂递归?读完这篇保证你懂》。有位仁兄觉得我在炫技,和我杠上了。由于原文已经删除了,我复述下对话吧。有精简,无扭曲。
网友A:你写这么难指望谁能看懂?说得不好听就是炫耀技术了。
我:能让你有机会理解你还不懂的东西,你应该感谢才对。
网友A:就你牛逼,这么牛逼,给你出个题:写个死循环,还不影响页面性能。不是牛逼么,不要说你写不出来啊。
我:刚好我上一篇文章就写了个死循环,服不服?
……
上面是同行交流反面案例,大家不要跟着学。
我说的那个死循环很多人都看过了,长这样:
const starks = [
"Eddard Stark",
"Catelyn Stark",
"Rickard Stark",
"Brandon Stark",
"Rob Stark",
"Sansa Stark",
"Arya Stark",
"Bran Stark",
"Rickon Stark",
"Lyanna Stark"
];
function* repeatedArr(arr) {
let i = 0;
while (true) {
yield arr[i++ % arr.length];
}
}
const infiniteNameList = repeatedArr(starks);
const wait = ms =>
new Promise(resolve => {
setTimeout(resolve, ms);
});
(async () => {
for (const name of infiniteNameList) {
await wait(1000);
console.log(name);
}
})();
复制代码
为了证明这个死循环不影响页面性能,我写了个 codepen,在循环开始后,输入框还能正常输入。
由于 codepen 会限制死循环,当wait
时间小于 1000 ms 时,codepen 会终止程序。不过你可以把代码保存到本地跑,把 wait 时间改成 0 都没问题。
之所以这样写没让页面卡死,是因为 setTimeout 和 JavaScript 的事件循环机制。当 event loop 遇到 timeout 事件时,会将此任务推到 task queue 排队,event loop 继续处理调用栈,直到调用栈空了再来处理 task queue。
将上面的代码简化,依然利用 setTimeout 来实现死循环的功能:
let i = 0;
let timer = 0;
function start() {
p.innerText = starks[i++ % starks.length];
timer = setTimeout(start);
}
复制代码
这个无限递归不会爆栈,也不会影响页面性能。输入框照常能输入。见 codepen
既然都是异步事件,用 promise 可以实现 setTimeout 的这个效果吗?这就涉及到 task 和 micro task 的区别了。来试试:
let i = 0
function andThen(){
p.innerText = starks[i++ % starks.length];
Promise.resolve().then(andThen)
}
function start(){
Promise.resolve().then(andThen)
}
复制代码
效果见这个 codepen。点击开始后,页面会卡死。
promise 属于 micro task,当运行时处理完每个 task 之后,都会检查 micro task queue,如果不为空,则将其依次执行完。上面无限递归生成无限个 micro task,事件循环一直执行 micro tasks,在处理完之前不响应其它事件,所以页面会卡死。
本文开头提到的优化历史代码,优化前 (codepen):
async function run(pause) {
for (tasks of chunkedTasks) {
await asyncPipe(...tasks)();
await wait(pause);
}
return run(pause);
}
run(1000);
复制代码
优化后 (codepen):
async function run(pause) {
for (const tasks of chunkedTasks) {
await asyncPipe(...tasks)();
await wait(pause);
}
setTimeout(run, 0, pause);
}
run(1000);
复制代码