先猜一下下面代码是输出顺序是什么?
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会打印什么?
- 执行script标签
- dom事件是宏任务,加入到macrotask,此时macrotask内有两个onclick函数
- 点击inner,执行第一个onclick,
- 先打印click->setTimeout为宏任务,加到macrotask->promise.then和mutationObserver属于微任务,加入microtask,此时onclick函数体执行完成
- 执行微任务,依次打印promise和mutate,微任务队列清空
- 从macrotask中取出宏任务第二个onlick并执行
- 重复步骤4,打印click
- 重复步骤5,打印promise和mutate,微任务队列清空
- 此时宏任务队列还有两个setTimeout
- 依次执行Timeout
所以打印顺序为click-promise-mutate-click-promise-mutate-timeout-timeout