浅聊事件循环:宏任务和微任务

842 阅读3分钟

事件循环:宏任务和微任务

咱们先从js引擎开始。js是单线程的,也就是说所有的代码是一行接着一行运行的。此外js提供了宏任务和微任务这两个概念。先说宏任务。宏任务是一个外部脚本文件,一个用户交互触发的事件或一个setTimeout调用的回调函数。为了实现单线程这个概念,js有一个宏任务队列(先进先出),宏任务不断地创建出来塞到队尾,js引擎不断地从队首取任务出来执行。

分割大任务

现我们来模拟一个耗时很长的任务。

let i = 0;
let start = Date.now();
function count() {
  //耗时长的任务
  for (let j = 0; j < 1e9; j++) {
    i++;
  }
  console.log("Done in " + (Date.now() - start) + 'ms');
}
count();

在这个任务执行的过程,用户对页面的任何操作都将塞到这个任务的后面,因此造成一段时间没有响应,用户体验非常不好。 如果我们理解了以上的概念。我们就可以对代码进行改进。我们就大任务拆分成小任务。

let i = 0;
let start = Date.now();
function count() {
  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    console.log("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count);//注意!setTimeout会创建一个宏任务,塞到队尾。
  }
}
count();

之前的体验是
|        任务       |用户操作
现在是
| 任务 |用户操作| 任务 |用户操作| 任务 |
因为在处理小任务这个宏任务时,用户的操作事件也被塞进了队尾。这样就可以在处理小任务的间隙处理用户操作。

进程指示

OK!我们现在补充一个原理: 在宏任务执行的时候,浏览器不做任何的渲染,不管这个宏任务执行的时间的长短。
有个这个理解后,我们在说说我们还可以做点什么?有时候我们想把一个任务的执行过程显示出来。怎么做呢?

<div id="progress"></div>

<script>
  function count() {
    for (let i = 0; i < 1e6; i++) {
      i++;
      progress.innerHTML = i;
    }
  }
  count();
</script>

希望读者可以自己跑一下,你会发现你只能看到最后的结果999999. 而如果我们这样做了后。

<div id="progress"></div>

<script>
  let i = 0;
  function count() {
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e7) {
      setTimeout(count);
    }
  }
  count();
</script>

你会看到一个数字不断地加1000地上升。

微任务与宏任务

讲完了宏任务,来讲讲微任务。微任务是由Promise创建出来的且js中有一个专门的微任务队列来存储微任务。微任务的机制是:当执行完一个任务后,只要有微任务就先执行微任务。宏任务和渲染通通排到后面。

我们来看一段代码。

setTimeout(() => console.log("timeout"));

Promise.resolve()
  .then(() => console.log("promise"));

console.log("code");

假设这是一个脚本文件,首先将setTimeout的回调塞入到宏任务队列中,但是因为这个脚本文件还没执行完,所以它不会立刻得到执行。然后将then中的函数塞到微任务队列中,然后输出了'code',这个脚本文件宏任务执行完了。因为微任务的存在,js引擎先执行微任务,所以先输出'promise',这样最后输出'timeout'。

所以最后的输出顺序是 code promise timeout.

结尾

我相信到这,你应该明白了事件循环,宏任务与微任务。

参考链接