导论
先看题目:
question:试着写出下面程序的输出结果:
console.log(1);
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
console.log(3);
resolve();
}).then(res => {
console.log(4);
})
console.log(5);
}, 0);
new Promise((resolve) => {
console.log(6);
resolve();
}).then(res => {
console.log(7);
})
setTimeout(() => {
console.log(8);
new Promise((resolve) => {
console.log(9);
resolve();
}).then(res => {
console.log(10);
}, 0)
});
console.log(11);
answer:1 -> 6 -> 11 -> 7 -> 2 -> 3 -> 5 -> 4 -> 8 -> 9 -> 10
宏任务与微任务
在javascript事件循环中,==异步**任务是通过队列(queue
)来存储的。流程如下:
而异步任务一共分为两种:微任务与宏任务。它们的执行顺序如下:
javascript宏任务与微任务执行顺序需要注意的是,微任务与宏任务是先注册,再执行。而不是读取到就立即执行。
常见的宏任务(按优先级排列):整体代码script > setImmediate
> setTimeout
/setInterval
。
常见的微任务(按优先级排列):process.nextTick
(Nodejs
中的内容) > 原生Promise
> MutationObserver
。
回到刚才的那个题目:
// 主代码块
console.log(1);
// 注册了一个宏任务
setTimeout(() => {
// ...
}, 0);
// {↓标记↓}
new Promise((resolve) => {
// 立即执行部分,其实是同步任务
console.log(6);
resolve();
}).then(res => {
// 这才是微任务
console.log(7);
})
// 注册了一个宏任务
setTimeout(() => {
// ...
});
// 主代码块
console.log(11);
所以一开始先执行主代码块,输出1、6、11,请注意,Promise
构造函数的参数中的代码是同步任务,与主代码块同步进行。
执行完主代码块后,会寻找微任务队列中的微任务并执行,此时的微任务只有标记的Promise.then()
(其他Promsie
所在的代码块并未被执行,因此尚未被注册),输出7。
微任务执行完后,寻找下一个宏任务 — 第一个SetTimeOut
。
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
console.log(3);
resolve();
}).then(res => {
console.log(4);
})
console.log(5);
}, 0);
同理可得:将输出2、3、5,并注册一个微任务(Promise.then()
)。在执行完后执行微任务,输出4。
然后再是最后一个宏任务:
setTimeout(() => {
console.log(8);
new Promise((resolve) => {
console.log(9);
resolve();
}).then(res => {
console.log(10);
}, 0)
});
8、9、10,没什么问题了吧,别问,问就同理可得。
西江月·证明
即得易见平凡,仿照上例显然。留作习题答案略,读者自证不难。
反之亦然同理,推论自然成立,略去过程Q.E.D ,由上可知证毕。
其实这个题还差了点意思,换做我就这样出:
console.log(1);
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
console.log(3);
resolve();
}).then(res => {
console.log(4);
})
console.log(5);
}, 0);
new Promise((resolve) => {
console.log(6);
resolve();
}).then(res => {
console.log(7)
setTimeout(() => {
console.log(8);
});
})
console.log(9);
猜一下答案输出顺序是什么?
...
...
...
...
...
...
...
...
...
答案是 1 -> 6 -> 9 -> 7 -> 2 -> 3 -> 5 -> 4 -> 8。
拓展
常见的宏任务除了上面提到的那些之外,还有一个不曾提到但非常常见的:IO
(输入输出流)。你可以简单理解为事件监听(最常见的表现就是事件监听,不过不仅仅包括事件监听)。
上代码:
/* css */
div {
border: 1px solid black;
}
#outer {
padding: 25px;
width: 50px;
background-color: aqua;
}
#inner {
width: 50px;
height: 50px;
background-color: green;
}
<!--html-->
<div id="outer">
<div id="inner"></div>
</div>
// js
let $outer = document.getElementById('outer');
let $inner = document.getElementById('inner');
function handler() {
console.log('click') // 直接输出
// 注册微任务
Promise.resolve().then(_ => console.log('promise'))
// 注册宏任务
setTimeout(_ => console.log('timeout'))
// 注册宏任务
requestAnimationFrame(_ => console.log('animationFrame'));
// DOM属性修改,触发微任务
$outer.setAttribute('data-random', Math.random());
}
// 微任务
new MutationObserver(_ => {
console.log('observer')
}).observe($outer, {
attributes: true
})
$inner.addEventListener('click', handler);
$outer.addEventListener('click', handler);
效果图
点击div#inner
,输出结果: 'click' -> 'promise' -> 'observer' -> 'click' -> 'promise' -> 'observer' -> 'animationFrame' -> 'animationFrame' -> 'timeout' -> 'timeout'。
让我们来捋一下:
点击时通过事件冒泡触发了宏任务$inner.click()
,输出'click'。该任务触发了冒泡事件并注册了宏任务$outer.click()
。并在事件处理函数handler
中注册了两个宏任务(requestAnimationFrame
和setTimeout
)和一个微任务(Promise
),并触发了一个微任务MutationObserver
。
该宏任务执行完后,微任务队列中有两个微任务,按优先级先执行Promise
,所以依次输出'promise','observer'。
下一个宏任务,即$outer.click()
开始执行,重复了$inner.click()
的事件,区别是它没有冒泡并注册新任务了。依次输出'click','promise','observer'。
至此,只剩下几个宏任务对线了。
值得注意的是,因为requestAnimationFrame
会触发页面重绘(不是优先级哦),进而会导致setTimeout
重置,所以前者会比后者先输出。
后记
祝你学习愉快。
网友评论