宏任务和微任务
[建议可先批阅 什么什么,竟然还有人没搞懂JavaScript的事件循环机制吧]
通过上文我们明白了事件循环机制和JavaScript的执行流程,我们知道有一个任务队列的容器,是一个队列的结构。
所有除了同步任务以外的代码,都会在工作线程中,然后按照他们的时间顺序依次有序地进入任务队列,事件循环机制会不断地从任务队列中读取进入放入执行栈中执行。
而任务队列中的异步任务又分为宏任务和微任务。
宏任务 (macro task)
宏任务就是JavaScript中最原始的异步任务,比如:setTimeout、setInterVal、AJAX等等,在代码执行的时候会进入到工作线程中挂起,然后按照任务的时间节点顺序,依次进入任务队列,然后通过事件循环机制依次进入函数执行栈中执行。
微任务 (micro task)
微任务是后面提出的新的异步任务,在执行每一个宏任务之前,程序都会先检测是否有当次事件循环还没有被执行的微任务,如果有,则清空本次的微任务后再去执行下一个宏任务。
每一个宏任务内部可以注册当次任务的微任务队列,在下一次宏任务执行前运行,微任务也按照进入队列的顺序执行。
举个🌰
十足知道吧?去十足买过东西吧?我去买东西,一群人也去买东西,我就是一个异步任务,一群人就是一群的异步任务。
我们在排队(任务队列),排到我了,后面的人要等我付完钱(执行完毕),但是,我在第一次付完钱后,又和收银员说我要一个叉烧包还要办十足会员卡(产生了微任务)。
然后收银员就又扣了我叉烧包的钱顺便办了会员卡(执行微任务)。
然后再下一个人进行付款(执行)。
所以,在JavaScript的运行环境中,代码执行流程如下所示:
- 首先同步代码按照顺序从上到下执行,运行过程中会注册本次的微任务和后续的宏任务。
- 当本次的同步代码执行完毕后,会检查一下当前的微任务队列是否有注册微任务,如果有,则优先执行微任务队列中注册的微任务。
- 如果当前注册的微任务中存在微任务和后续的宏任务,则会把宏任务和微任务注册到任务队列。
- 直到把下一个宏任务开始前的所有的微任务都执行完
- 首先执行最先进入队列的宏任务,谁先进入队列和之前的方式一样,看时间谁先到。执行宏任务的时候,注册本次代码产生的微任务和后续宏任务,在下一个宏任务开始前,把本次的微任务执行完。
总结
异步任务分类:宏任务和微任务。
当前代码产生的微任务,会在下一个宏任务执行前优先执行。当前同步任务->当前同步任务产生的微任务->下一个需要执行的宏任务.
下面来看下面的例子:
function task1() {
console.log("task1");
}
function task2() {
setTimeout(() => {
console.log("task2");
}, 1000);
}
function task3() {
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log("task3");
resolve("p2");
}, 0);
});
p2.then((res) => {
console.log("第一个then:", res);
});
}
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log("p1");
resolve("p1");
}, 1000);
});
p1.then((res) => {
console.log("第一个then:", res);
return res;
}).then((res) => {
console.log("第二个then:", res);
});
let p3 = new Promise((resolve, reject) => {
console.log("p3");
resolve("p3");
}).then((res) => {
console.log("第一个then:", res);
});
task1();
task2();
task3();
// p3 task1 第一个then:p3 task3 第一个then:p2 第一个then:p1 第二个then:p1 task2
上面代码从上往下执行,p1的Promise回调函数是异步的,所以放入了宏任务的工作线程等待。
p3的Promise的回调函数是同步的,所以优先打印了p3。然后这个promise.then产生了微任务,把微任务放到了微任务的工作线程。
然后代码继续往下,分别执行了task1 task2 task3函数,按照顺序执行。
task1是同步的,所以直接打印了task1。
task2是异步的,放入工作线程,代码继续执行。
task3是异步的,放入工作线程,至此,当前所有的同步任务执行完毕。
然后工作线程中会把当前代码产生的微任务依次放入微任务队列中,然后执行第一个.then的回调函数代码,继续执行第二个.then产生的微任务代码。
至此,当前代码的同步任务和产生的微任务都执行完毕。接下来看工作线程中哪个异步任务先到时间。
发现p2的Promise里的异步任务时间先到,然后打印task3
然后当前代码(p2的Promise里的异步任务内的同步代码)的同步任务执行完毕,发现产生了一个微任务,在下一个宏任务执行前优先执行当前代码产生的微任务,即打印第一个then:p2
p1的promise回调函数的延时时间是1000ms,task2内部的异步函数延时时间也是1000ms,但是p1的执行时间优先于task2内部的异步函数,因为代码从上往下执行。
然后依次打印:第一个then:p1 第二个then:p1 task2
常见的宏任务和微任务
宏任务
- I/O
- script(整体代码)
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame(下次页面重绘前执行的操作,而重回会作为宏任务的一个步骤存在)
微任务
- process.nextTick
- MutationObserver
- Promise.then(catch finally)
注:Promise中的回调函数是在同步任务中执行的,如果这个回调函数没有执行resolve或reject,那么回调函数内部不会有输出。
如下所示,同步在前,异步在后。Promise的回调函数是同步执行的,所以优先输出promise1和promise2,同时Promise的状态也变更了,then函数的回调被注册到微任务事件中。
然后继续执行,输出end。
然后同步代码执行完毕,观察异步代码的宏任务和微任务,在本次同步代码注册的微任务会在下一次的宏任务执行前执行。所以Promise.then的回调函数执行。所以输出promise then。
接下来就要看setTimeout和requestAnimationFrame两个宏任务。
setTimeout是在程序运⾏到setTimeout时⽴即注册⼀个宏任务,所以两个setTimeout的顺序⼀定是固定的timer1和timer2会按照顺序输出。
⽽requestAnimationFrame是请求下⼀次重绘事件,所以他的执⾏频率要参考浏览器的刷新率。
setTimeout(function () {
console.log("timer1");
}, 0);
requestAnimationFrame(function () {
console.log("UI更新");
});
setTimeout(function () {
console.log("timer2");
}, 0);
new Promise(function executor(resolve) {
console.log("promise 1");
resolve();
console.log("promise 2");
}).then(function () {
console.log("promise then");
});
console.log("end");
// promise1 -> promise2 -> end -> promise then -> UI更新 -> timer1 => timer2
补充——setTimeout(0)和requestAnimationFrame
setTimeout(fn(),0)
setTimeout是延时函数,一般第二个参数表示需要延时执行的时间,那如果是0,就真的是延时0毫秒么?
根据上面的学习,我们知道实际执行的时间是,等待同步任务执行完毕后开始延时。那如果是0毫秒,就真的是同步任务执行完毕后马上执行么?
看下面代码:
let i = 0;
let d = new Date().getTime();
let d1 = new Date().getTime();
function loop() {
d1 = new Date().getTime();
i++;
if (d1 - d >= 1000) {
d = d1;
console.log(i);
i = 0;
console.log("经过了1秒");
}
setTimeout(loop, 0);
}
loop();
由此可见,差不多1秒能够执行200次,差不多5ms一次。
requestAnimationFrame 看下面代码:
let i = 0;
let d = new Date().getTime();
let d1 = new Date().getTime();
function loop() {
d1 = new Date().getTime();
i++;
//当间隔时间超过1秒时执⾏
if (d1 - d >= 1000) {
d = d1;
console.log(i);
i = 0;
console.log("经过了1秒");
}
requestAnimationFrame(loop);
}
loop();
由此可见,差不多1s能执行60次,平均16ms一次。当然不同游览器可能有不同的效果。
总结
代码执行前,同步在前,异步在后,异步任务可分为宏任务和微任务。
代码块的执行顺序是,同步任务执行,遇到异步任务会放入工作线程中挂起,工作线程会等待宏任务的时间结束,微任务直接进入当前的微任务队列(直接理解成微任务所需的时间永远比宏任务快)。然后继续执行同步任务直到同步任务执行完毕。
这时,事件循环机制就会启动,他会先从当前的微任务队列中取消息,放入执行栈中执行,知道微任务队列清空。
然后执行下一个宏任务,如果下一个宏任务产生了宏任务和微任务,则会将执行当前代码块产生的微任务,然后再执行下一个到期的宏任务。
所以宏任务执行顺序按照到期时间,微任务则在当前同步的代码执行完毕后立即执行。
其实每个执行栈的任务,都可以算是一个宏任务。
其中new Promise的回调函数是同步任务!!!