抬杠:写个死循环,还要让页面正常跑

最近在优化以前的练习代码时想到了半年前的一个小插曲。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);
复制代码
相关文章
相关标签/搜索