一个栗子,搞懂 JS 事件循环

272 阅读3分钟

话不多说,先上代码

<div id="app" class="app">
  hello
</div>

<script>
  // 定义 MutationObserver
  var targetNode = document.getElementById('app');
  var config = { attributes: true, childList: true, subtree: true };
  var callback = function(mutationsList) {
    for(var mutation of mutationsList) {
      if (mutation.type == 'childList') {
        console.log('Mutaition childlist');
      } else if (mutation.type == 'attributes') {
        console.log('Mutaition attributes');
      }
    }
  };
  var observer = new MutationObserver(callback);
  observer.observe(targetNode, config);

  // 定义 async函数
  async function async1 () {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
  }
  async function async2 () {
    console.log('async2 start');
  }

  /********* 开始输出 **********/
  console.log('start');

  // 更新dom,进入Mutation 回调函数中
  targetNode.innerHTML = 'world';

  // 执行async1函数
  async1();

  // 执行promise函数
  new Promise(function (resolve) {
    console.log('promise');
    resolve();
  }).then(function () {
    console.log('promise then');
  });

  // 再次更新dom
  targetNode.className = 'abc';

  // 执行setTimeout函数
  setTimeout(function () {
    console.log('setTimeout');
  },0);

  /********* 结束输出 **********/
  console.log('end');

</script>

下面是输出结果,看看和大家想的是否一样。

// start
// async1 start
// async2 start
// promise
// end
// Mutaition childlist
// Mutaition attributes
// async1 end
// promise then
// setTimeout

1. 宏任务和微任务

JS分为同步异步,异步又分为宏任务微任务

简单理解执行过程:一条主线程,两个异步栈(宏任务栈,微任务栈)。主线程(同步任务)开始执行,遇到异步任务,放到对应的异步栈中,等主线程执行完毕,先清空微任务栈,再清空宏任务栈,都是先进先出原则。如果执行过程中再遇见异步任务,就重复这个流程,直到全部执行完毕。

清楚了JS执行过程,再去看执行结果,就容易了,我们只需要搞清楚一件事就行了,哪些是宏任务,哪些是微任务。

2. 梳理一下上面的各种异步方案。

  1. MutationObserver - 微任务,创建一个对DOM变化的监听对象
  2. setTimeout - 宏任务
  3. Promise - 微任务
  4. async 也相当于对Promise的一种封装,使异步代码可以不用嵌套。他本身是同步代码,但是遇到await就相当于开启了一个Promise,变成了一个微任务。

现在清楚了各种方法的分类,对上面的输出结果应该大部分都没有疑问了,唯一一点是 Mutaition childlist、Mutaition attributes 这两个输出,第二个修改属性的动作明明是在Promise之后的,为什么却跑到了它前面输出了呢?之后我把第一个修改DOM的操作移除,再执行一次

// start
// async1 start
// async2 start
// promise
// end
// async1 end
// promise then
// Mutaition attributes
// setTimeout

结果就完全跟我们想的一样了,所以这里可以理解成,在微任务栈中,MutationObserver 的callback 会放到一起存储,当执行到这里时,就会依次优先执行。

总结整个流程:

  1. 开始执行 打印start
  2. 更新DOM,MutationObserver callback放入微任务栈
  3. 执行async1(),打印async1 start,再执行async2(),打印async2 start
  4. console.log('async1 end');相当于在then的回调里,放入微任务栈
  5. 执行Promise,打印promise
  6. console.log('promise then');放入微任务栈
  7. 再次更新DOM,MutationObserver callback再次放入微任务栈,并插入到上一个callback后
  8. 执行setTimeout,回调放入宏任务栈
  9. 打印end,主线程执行完成
  10. 开始依次执行微任务栈,打印Mutaition childlist,Mutaition attributes,async1 end,promise then
  11. 开始依次执行宏任务栈,打印setTimeout,全部执行完成

参考文章
developer.mozilla.org/zh-CN/docs/… xieyufei.com/2019/12/30/…