1. 同步任务
与异步任务
所有的任务可以分为同步任务和异步任务。
- 同步任务就是立即执行的任务,同步任务一般会直接进入到执行栈中执行,且只有一个任务执行完毕,才能执行下一个任务;
- 异步任务,不进入主线程,而是放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。
2. 执行栈
与任务队列
- 执行栈:英文Call Stack,也叫调用栈。
执行栈用于组织JS代码,保障JS代码的有序执行。每当调用一个函数时,都会在执行栈中加入一个与之对应的执行期上下文,这个上下文定义了该函数执行时的环境,加入执行上下文之后,在执行函数。当函数执行后出栈,执行下一个。 - 任务队列:英文Event Queue,也叫事件队列。
任务队列使用到的是数据结构中的队列结构,它用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理。
3. macrotask(宏任务队列)
与microtask(微任务队列)
任务队列分成macrotask(宏任务队列) 和 microtask(微任务队列)
- 宏任务: script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
- 微任务: Promise.then()、Promise.catch()、MutaionObserver、process.nextTick(Node.js 环境);
4. Eventloop 在处理宏任务和微任务的逻辑时的执行情况如下:
- JavaScript 引擎首先从宏任务队列中取出第一个任务;
- 执行完毕后,再将微任务队列中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行,也就是说在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行。
- 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。
也是就是说,一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。
通过一段代码的运行来讲述事件循环机制:
console.log('同步代码1');
setTimeout(() => {
console.log('setTimeout1')
new Promise((resolve) => {
console.log('Promise 1')
resolve()
}).then(() => {
console.log('promise.then1')
})
}, 10)
new Promise((resolve) => {
console.log('同步代码2, Promise 2')
setTimeout(() => {
console.log('setTimeout2')
}, 10)
resolve()
}).then(() => {
console.log('promise.then2');
setTimeout(() => {
console.log('setTimeout3')
}, 10);
new Promise((resolve) => {
console.log('Promise 3')
resolve()
}).then(() => {
console.log('promise.then3')
});
})
console.log('同步代码3');
/* 宏任务队列中第1个宏任务script的打印:*/
// 同步代码1
// 同步代码2, Promise 2
// 同步代码3
// promise.then2
// Promise 3
// promise.then3
/* 宏任务队列中第2个宏任务setTimeout1的打印:*/
// setTimeout1
// Promise 1
// promise.then1
/* 宏任务队列中第3个宏任务setTimeout2的打印:*/
// setTimeout2
/* 宏任务队列中第3个宏任务setTimeout3的打印:*/
// setTimeout3
以上代码的执行逻辑为:
- 当前任务队列只有script这个代码片段,开始执行:
- 遇到第一个console,它是同步代码,加入执行栈,执行并出栈,打印出 "同步代码1";
- 遇到setTimeout(取名叫:setTimeout1),它是一个宏任务,加入宏任务队列, 所以我们的宏任务队列就变为macrotask: [setTimeout1];
- 遇到new Promise 中的console,它是同步代码,加入执行栈,执行并出栈,打印出 "同步代码2, Promise 2";
- 遇到setTimeout(取名叫:setTimeout2),它是一个宏任务,加入宏任务队列, 即macrotask: [setTimeout1,setTimeout2];
- 遇到Promise2的 then,它是一个微任务,加入微任务队列, 我们的微任务队列变为microtask: [Promise 2的then];
- 遇到第三个console,它是同步代码,加入执行栈,执行并出栈,打印出 "同步代码3";
- 此时执行栈为空,去执行微任务队列中所有任务(由上可知我们的微任务队列此时为microtask: [Promise 2的then]);
- microtask: [Promise 2的then]出队列进入执行栈去执行Promise 2的then,即microtask: [];
- 遇到console,它是同步代码,加入执行栈,执行并出栈,打印出 "promise.then2";
- 遇到setTimeout(取名叫:setTimeout3),它是一个宏任务,加入宏任务队列, 即macrotask: [setTimeout1,setTimeout2,setTimeout3];
- 遇到new Promise 中的console,它是同步代码,加入执行栈,执行并出栈,打印出 "Promise 3";
- 遇到Promise3的 then,它是一个微任务,加入微任务队列, 我们的微任务队列变为microtask: [Promise3的then];
- promise.then2的微任务执行完毕,但是由上可知,当前微任务队列依旧有任务,所以继续将当前微任务队列中的任务push到执行栈中进行执行,即执行Promise3的then(microtask:[]);
- 打印"promise.then3";
- 执行栈为空且当前循环中微任务队列为空,那么去执行下个宏任务,(当前macrotask: [setTimeout1,setTimeout2,setTimeout3]);
- 执行宏任务setTimeout1,macrotask: [setTimeout2,setTimeout3]);
- 遇console,为同步,打印"setTimeout1";
- 遇new Promise为同步,打印"Promise 1";
- 遇Promise1的then,为微任务,放入微任务队列中,即microtask:[Promise1的then]
- 当前循环中宏任务执行完毕,执行当前的微任务队列中的任务,即执行Promise1的then,打印“promise.then1”;
- 当前循环的宏任务与微任务全部执行完毕,继续执行下个宏任务setTimeout2,macrotask: [setTimeout3]);
- 打印“ setTimeout2”;
- 当前循环中没有微任务,执行下个宏任务setTimeout3,macrotask:[];
- 打印“ setTimeout3”;
- 当前循环中没有微任务;
- 宏任务队列也为空,至此全部执行完毕。
从上面的宏任务和微任务的工作流程中,可以得出以下结论:
- 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
- 微任务的执行时长会影响当前宏任务的时长。比如一个宏任务在执行过程中,产生了 10 个微任务,执行每个微任务的时间是 10ms,那么执行这 10 个微任务的时间就是 100ms,也可以说这 10 个微任务让宏任务的执行时间延长了 100ms。
- 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行(优先级更高)。
参考:
https://juejin.cn/post/6992167223523541023
https://segmentfault.com/a/1190000022805523
https://juejin.cn/post/7031751891201884174