在 JavaScript 中,宏任务(macrotask)和微任务(microtask)都是指待执行的任务,它们之间的区别在于它们的执行顺序和触发时机。
宏任务
- setTimeout
- setInterval
- setImmediate (Node.js环境下)
- I/O 操作
- UI 渲染
宏任务通常包括整体代码中的任务、setTimeout、setInterval、requestAnimationFrame、UI 事件以及 I/O 操作等。当执行宏任务时,JavaScript 引擎会从宏任务队列中取出第一个宏任务执行,执行完毕后再执行下一个宏任务。
微任务
微任务是在当前宏任务执行完成后,立即执行的任务。它们也被称为 jobs,包括:
- Promise.then()、Promise.catch()
- Object.observe()
- MutationObserver
- process.nextTick (Node.js环境下)
当一个宏任务执行完毕后,事件循环会在它执行的上下文中查找微任务队列。如果找到微任务队列,它会按照队列中的顺序依次执行所有的微任务,直到队列为空。然后事件循环会再次从宏任务队列中取出队首任务执行。
因为微任务是在宏任务执行完成后立即执行的,所以它们的执行顺序优先于下一个宏任务。
当宏任务和微任务同时存在时,它们的执行顺序如下:
- 首先执行同步任务,执行完同步任务后会先执行所有微任务,直到微任务队列为空。
- 然后执行宏任务队列中的第一个宏任务,执行完宏任务后再执行所有微任务,依此类推。
- 第一个小栗子
console.log('1');
setTimeout(function() {
console.log('2');
Promise.resolve().then(function() {
console.log('3');
});
}, 0);
Promise.resolve().then(function() {
console.log('4');
});
console.log('5');
以上代码中,首先会执行整体代码中的任务,输出 1
和 5
。然后,执行宏任务 setTimeout
,将其回调函数添加到宏任务队列中,由于宏任务中有一个微任务 Promise.resolve().then()
,因此会先执行微任务队列中的 4
,再执行宏任务队列中的 2
。最后,由于宏任务中又有一个微任务 Promise.resolve().then()
,因此会执行微任务队列中的 3
。因此,最终输出的结果为:
1
5
4
2
3
需要注意的是,不同的宏任务和微任务之间也是存在优先级关系的,例如 Promise 中的 then
回调会先于 MutationObserver 中的回调执行。因此,在实际开发中,需要仔细考虑任务的执行顺序,以避免出现不可预期的结果。
- 第二个小栗子
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('end');
输出的结果是:
start
end
promise
timeout
可以看到,console.log(‘promise’) 是先于 console.log(‘timeout’) 执行的,因为 Promise.resolve().then() 是一个微任务,而 setTimeout 是一个宏任务。当 setTimeout 被添加到宏任务队列时,微任务队列已经有了任务,所以它们首先被执行。
- 经典的栗子,面试题中常出
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
});
console.log('5');
输出结果为:
1
5
4
2
3
解释如下:
- 执行同步任务,输出 1 和 5。
- 将 setTimeout 回调函数添加到宏任务队列中。
- 将 Promise 的 then 回调函数添加到微任务队列中。
- 执行宏任务队列中的第一个宏任务,即 setTimeout 回调函数,输出 2。
- 将 Promise 的 then 回调函数添加到微任务队列中。
- 执行微任务队列中的第一个微任务,即 Promise 的 then 回调函数,输出 4。
- 执行微任务队列中的第二个微任务,即 Promise 的 then
总之,理解宏任务和微任务对于理解 JavaScript 的事件循环非常重要。正确地使用它们可以优化代码的性能并避免一些常见的问题。
有什么错误或者需要改进的地方,希望大家不吝赐教。