最近在优化以前的练习代码时想到了半年前的一个小插曲。promise
当时我在掘金发了第二篇文章 -- 《不懂递归?读完这篇保证你懂》。有位仁兄以为我在炫技,和我杠上了。因为原文已经删除了,我复述下对话吧。有精简,无扭曲。异步
网友A:你写这么难期望谁能看懂?说得很差听就是炫耀技术了。async
我:能让你有机会理解你还不懂的东西,你应该感谢才对。oop
网友A:就你牛逼,这么牛逼,给你出个题:写个死循环,还不影响页面性能。不是牛逼么,不要说你写不出来啊。性能
我:恰好我上一篇文章就写了个死循环,服不服?优化
……ui
上面是同行交流反面案例,你们不要跟着学。spa
我说的那个死循环不少人都看过了,长这样:code
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);
复制代码