JavaScript的事件循环机制是其非阻塞I/O模型的核心,它使得JavaScript能够在单线程环境中高效地处理异步操作。事件循环机制主要由以下几个部分组成:
-
调用栈(Call Stack):
- 这是JavaScript执行同步代码的地方,后进先出(LIFO)的数据结构。
- 当一个函数执行时,它会被推入栈顶,执行完毕后从栈顶弹出。
-
事件队列(Event Queue):
- 也称为任务队列,用于存放异步操作的回调函数。
- 事件队列可以有多个,但最常见的是宏任务队列和微任务队列。
-
宏任务(Macrotasks):
- 包括如
setTimeout
、setInterval
、I/O操作、网络请求、UI渲染等。 - 每个宏任务执行完毕后,会检查并执行所有微任务队列中的微任务。
- 包括如
-
微任务(Microtasks):
- 包括
Promise
的.then()
、.catch()
、.finally()
,以及MutationObserver
。(注意区分:new Promise,Promise构造函数是同步执行的) - 微任务的优先级高于宏任务,当调用栈清空后,会立即执行所有微任务队列中的微任务。
- 包括
-
事件循环(Event Loop):
- 事件循环是JavaScript运行时的调度机制,它不断地检查调用栈和事件队列。
- 当调用栈清空时,事件循环会从宏任务队列中取出第一个任务执行,然后执行所有微任务队列中的微任务,接着检查是否需要进行UI渲染,然后再次检查宏任务队列。
事件循环的工作流程:
- 执行同步代码:同步代码在调用栈中执行,直到调用栈清空。
- 执行宏任务:调用栈清空后,事件循环从宏任务队列中取出第一个任务执行,将其推入调用栈。
- 执行微任务:宏任务执行完毕后,事件循环会立即执行所有微任务队列中的微任务,直到微任务队列清空。
- UI渲染:如果需要,浏览器会进行UI渲染更新。
- 重复循环下一个宏任务:如果宏任务队列中还有任务,事件循环会再次开始,执行下一个宏任务,直到调用栈和事件队列都为空。
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('Script end');
执行顺序如下:
- 执行同步代码
console.log('Script start')
。 - 执行同步代码
setTimeout(function() { console.log('setTimeout'); }, 0);
。这个宏任务被添加到宏任务队列。 - 执行同步代码
Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });
。这两个微任务被添加到微任务队列。 - 执行同步代码
console.log('Script end')
。 - 同步代码执行完毕,开始执行微任务队列中的所有微任务。首先执行第一个微任务
console.log('promise1')
。 - 执行第二个微任务
console.log('promise2')
。 - 微任务队列清空,执行渲染更新(如果有的话)。
- 检查宏任务队列,执行
setTimeout
中的回调函数console.log('setTimeout')
输出结果:
Script start
Script end
promise1
promise2
setTimeout
这道题会做了,应该就彻底理解了:
setTimeout(() => {
console.log('1');
new Promise(function (resolve, reject) {
console.log('2');
setTimeout(() => {
console.log('3');
}, 0);
resolve();
}).then(function () {
console.log('4')
})
}, 0);
console.log('5');
setTimeout(() => {
console.log('6');
}, 0);
new Promise(function (resolve, reject) {
console.log('7');
reject();
resolve();
}).then(function () {
console.log('8')
}).catch(function () {
console.log('9')
})
console.log('10');
输出结果:// 5 7 10 8 1 2 4 6 3
分析:
setTimeout(() => { console.log('1'); ... }, 0);
被推入宏任务队列,因为setTimeout
是一个宏任务。
console.log('5');
执行,输出 '5'。
setTimeout(() => { console.log('6'); }, 0);
被推入宏任务队列。
new Promise(...)
创建了一个 Promise 实例,其执行器函数立即执行(因为Promise构造函数是同步执行的),console.log('7');
输出 '7'。
resolve();
在Promise的执行器函数中被调用,这将Promise状态变为resolved,并将.then(function () { console.log('8') })
中的回调函数推入微任务队列。
console.log('10');
执行,输出 '10'。至此,主线程中的同步代码执行完毕,事件循环开始处理微任务队列:
微任务队列中的
.then(function () { console.log('8') })
执行,输出 '8'。由于没有其他的微任务,事件循环开始处理宏任务队列中的下一个宏任务:
- 首先执行
setTimeout(() => { console.log('1'); ... }, 0);
中的回调:
console.log('1');
输出 '1'。new Promise(...)
创建并立即执行执行器函数中的代码:
console.log('2');
输出 '2'。- 内部的
setTimeout(() => { console.log('3'); }, 0);
被推入宏任务队列。resolve();
被调用,.then(function () { console.log('4') })
中的回调函数被推入微任务队列。console.log('4')
由于resolve()
调用后立即推入微任务队列,输出 '4'。事件循环再次处理微任务队列,输出 '4'。
然后,事件循环处理宏任务队列中的下一个宏任务,执行
setTimeout(() => { console.log('6'); }, 0);
中的回调,输出 '6'。由于宏任务队列中还有之前由
console.log('2');
内部创建的setTimeout(() => { console.log('3'); }, 0);
,事件循环最终处理这个宏任务,输出 '3'。