JS宏任务微任务和事件循环(js的执行机制)

先猜一下下面代码是输出顺序是什么?

console.log('---------- start --------------');
setTimeout(() => {
    console.log('setTimeout');
}, 0)

new Promise((resolve, reject) => {
    for(var i = 0; i < 5; i++) {
        console.log(i);
    }
    resolve(); 
    console.log('333')
}).then(() => {
    console.log('promise实例成功回调执行!');
}).then(()=>{
    console.log('promise2')
})
console.log('------------- end ----------------');



//输出结果
/*
---------- start --------------
0
1
2
3
4
------------- end ----------------
promise实例成功回调执行
promise2
setTimeout
*/

js是一门单线程语言,所有任务都需要排队执行,如果上一个任务要消耗很长的时间,下一个任务都不得不等待很长时间,这时设计者就把任务分为了同步任务和异步任务。同步任务在主线程上一个个执行,异步任务则会加入到任务队列,只有所有同步任务执行完,才会执行异步任务。

异步任务有:setTimeout,setInterval,dom事件,promise, ajax

按照这样的话,上面例子的setTimeout和promise.then是异步任务,加入到任务队列然后依次执行,但并不是这样,异步任务又分为宏任务和微任务

宏任务和微任务:宏任务优先于微任务执行, 宏任务包括整体代码script,setTimeout,setInterval,dom事件; 微任务包括promise,process.nextTick,MutationObserver(监听dom变化,具体用法可见《如何实现一个控制台删不掉的水印》)

所以上面的例子中script整体为宏任务,依次执行同步任务,先打印start--->然后遇到setTimeout,setTimeout属于异步任务也属于宏任务,放到task(宏任务队列)里面--->然后是promise,创建promise实例的代码属于同步任务,直接执行(执行后会resolve会生成promise.then ,微任务,放到microtask(微任务队列)里面)--->然后执行到最后打印end;此时主线程js stack被清空。

主线程执行完毕之后就会去清空微任务,所以接下来promise.then会被加入到主线程中去执行,执行完毕后(又有一个新的promise.then),主线程又被清空了

此时微任务队列里还有刚刚产生的promise.then,又被加入到主线程,打印出promise2

此时微任务和主线程都被清空,接着从宏任务队列中取出一个宏任务加入到主线程中执行,打印出setTimeout

此时主线程,微任务队列,宏任务队列都被清空了。

如果宏任务队列和微任务队列还没清空,就会:主栈全部执行完毕后-->清空微任务-->会取出一个宏任务 --> 执行完毕后-->清空微任务 -> 无线循环,这就是我们所说的事件环(Event Loop),也就是javascript的执行机制。

看下面的例子

<div class="outer">
  <div class="inner"></div>
</div>
<script>
// 获取dom节点
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

//监听element属性变化
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
</script>

点击inner会打印什么?

  1. 执行script标签
  2. dom事件是宏任务,加入到macrotask,此时macrotask内有两个onclick函数
  3. 点击inner,执行第一个onclick,
  4. 先打印click->setTimeout为宏任务,加到macrotask->promise.then和mutationObserver属于微任务,加入microtask,此时onclick函数体执行完成
  5. 执行微任务,依次打印promise和mutate,微任务队列清空
  6. 从macrotask中取出宏任务第二个onlick并执行
  7. 重复步骤4,打印click
  8. 重复步骤5,打印promise和mutate,微任务队列清空
  9. 此时宏任务队列还有两个setTimeout
  10. 依次执行Timeout

所以打印顺序为click-promise-mutate-click-promise-mutate-timeout-timeout