在 JavaScript 中,任务执行机制由 同步任务、微任务 和 宏任务 构成,它们共同构成了 事件循环 的核心部分。理解微任务和宏任务的区别对于理解 JavaScript 的异步行为至关重要。
同步任务
- 同步任务 是立即执行的代码块,在当前调用栈中按顺序执行。
- 例如:
console.log(2)
、console.log(3)
、console.log(7)
、console.log(9)
等都是同步任务。
异步任务:宏任务与微任务
微任务拥有更高的优先级,当事件循环遍历队列时,先检查微任务队列,如果里面有任务,就全部拿来执行,执行完之后再执行一个宏任务。执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。
宏任务(Macro Task)
宏任务 是较大级别的异步任务。通常,它们是由浏览器或 Node.js 提供的 API 调度的任务。
常见的宏任务包括:
setTimeout
setInterval
I/O
操作- 浏览器的 UI 渲染
setImmediate
(Node.js)
在代码中,
setTimeout
是一个典型的宏任务:javascript
setTimeout(() => { console.log(1); }, 0);
这个
setTimeout
将会被推入宏任务队列,等待所有同步任务和微任务完成后执行。
微任务(Micro Task)
微任务 是较小级别的异步任务,通常用于处理更紧急的任务。
常见的微任务包括:
Promise.then
MutationObserver
(DOM 变化监听)process.nextTick
(Node.js)
在代码中,
Promise.then
是一个典型的微任务:javascript
new Promise((resolve) => { console.log(2); resolve(); console.log(3); }).then(() => { console.log(4); });
这里的
then()
回调会被推入微任务队列,在所有同步任务完成后立即执行。
执行顺序:微任务 vs 宏任务
- 事件循环 的执行顺序是:同步任务 -> 微任务 -> 宏任务。
- 当主线程执行完所有同步任务后,它会先查看 微任务队列,执行所有微任务。如果微任务队列为空,才会执行 宏任务队列 中的第一个宏任务。
事件循环的工作原理
事件循环 是一条循环的流程,主要步骤如下:
- 执行同步任务
- 所有同步任务会在主线程中按顺序执行。
- 检查微任务队列
- 当所有同步任务执行完后,事件循环会检查并执行微任务队列中的所有任务。每次事件循环结束后,微任务队列会被完全清空。
- 执行宏任务
- 微任务执行完后,事件循环会从宏任务队列中取出一个宏任务并执行。
- 重复循环
- 事件循环会不断重复上述步骤,直到所有任务执行完毕。
详细示例:事件循环的执行过程
让我们通过一个简单的例子来详细说明事件循环和任务队列的工作机制:
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
});
console.log('End');
执行过程:
- 同步任务执行:
console.log('Start')
:立即执行,输出'Start'
。console.log('End')
:立即执行,输出'End'
。
- 异步任务加入队列:
setTimeout
:将回调函数放入宏任务队列。Promise.resolve().then()
:将回调函数放入微任务队列。
- 同步任务完成,检查微任务队列:
- 事件循环检测到同步任务执行完毕,检查微任务队列。
- 执行
Promise.then
回调,输出'Promise 1'
。
- 执行宏任务:
- 微任务队列清空后,事件循环从宏任务队列中取出任务。
- 执行
setTimeout
回调,输出'Timeout 1'
。
最终输出顺序:
Start
End
Promise 1
Timeout 1
任务执行优先级总结
- 同步任务:立即执行,放在主线程的调用栈中。
- 微任务:在每次事件循环结束时执行,优先级高于宏任务。
- 宏任务:在所有微任务完成后执行,每次事件循环只执行一个宏任务。
事件循环与浏览器渲染
- 浏览器渲染:浏览器在事件循环的某个时刻会执行渲染操作。这通常发生在宏任务执行之后、微任务执行之前的某个阶段。这意味着,如果微任务队列中任务非常多,可能会延迟浏览器的渲染,导致页面更新变慢。
结论
- 事件循环 确保 JavaScript 以非阻塞的方式运行,通过同步任务、微任务、和宏任务队列协调任务的执行顺序。
- 理解事件循环有助于优化 JavaScript 代码的执行效率,尤其是在处理异步任务和提高 UI 响应速度时。
新增案例
1.
javascript
console.log('1');
setTimeout(() => {
console.log('2');
new Promise((resolve) => {
console.log('3');
resolve();
}).then(() => {
console.log('4');
});
}, 0);
new Promise((resolve) => {
console.log('5');
resolve();
}).then(() => {
console.log('6');
});
setTimeout(() => {
console.log('7');
new Promise((resolve) => {
console.log('8');
resolve();
}).then(() => {
console.log('9');
});
}, 0);
console.log('10');
输出:
1 5 10 6 2 3 4 7 8 9
2.
javascript
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);
- 同步代码执行:
console.log('promise1')
:立即执行,打印'promise1'
。const promise1 = new Promise(...)
:创建一个新的 Promise 实例。const promise2 = promise1.then(res => {...})
:为promise1
添加一个then
处理函数。console.log('1', promise1);
:打印promise1
,此时它已经处于 pending 状态。console.log('2', promise2);
:打印promise2
,此时它也是一个 Promise 实例,但尚未解决,因为promise1
尚未解决。
- 微任务队列执行:
- Promise 的
resolve
调用将promise1
的状态改为 resolved,其值是'resolve1'
。 promise1
的then
处理函数被添加到微任务队列。- 微任务队列中的
then
处理函数执行,打印'resolve1'
。
- Promise 的
- 输出顺序:
- 首先,同步代码执行,打印
'promise1'
,然后打印promise1
的状态(pending),然后打印promise2
的状态(也是 pending)。 - 然后,微任务队列中的
then
处理函数执行,打印'resolve1'
。
- 首先,同步代码执行,打印
因此,输出将是:
text
promise1
1 Promise { 'resolve1' }
2 Promise { <pending> }
resolve1
2.
javascript
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
输出:
1
2
4
timerStart
timerEnd
success