前言
在JS的代码执行中,理解宏任务(macrotask)和微任务(microtask)是掌握异步行为的关键。这两种任务类型决定了代码的执行顺序和时机。在面试中,面试官经常问:“这段代码是如何执行的?输出结果是什么?请解释原因。” 难点不在于预测输出结果,而在于解释其背后的原因。能够清晰地阐述代码执行的过程和原理,才是展现对JavaScript异步机制和事件循环深刻理解的关键。
1.为什么有宏任务和微任务
JS的一个特点:单线程,也就说,同一个时间只能做一件事。 JS因为作为浏览器脚本语言,JS的主要用途是跟用户互动,以及DOM操作。所以这决定了它只能是单线程,否则会带来很复杂的同步问题。(比如:JS同时有两个线程,一个线程在为某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器就犯难了) 所以,为了避免复杂性,从JS诞生开始就是单线程,已经成了JS的核心特征了
2.宏任务有哪些
宏任务是那些在当前调用栈清空后才会被执行的任务。宏任务通常涉及较长时间的操作,如I/O操作、定时器等。常见的宏任务包括:
setTimeout和setInterval设置一个定时器,在指定的时间后执行某个回调函数。
setTimeout(() => {
console.log('setTimeout');
}, 1000);
setInterval(() => {
console.log('setInterval');
}, 1000);
- I/O操作 处理文件读写、网络请求等I/O操作。这些操作通常是异步的,不会阻塞主线程
const fs = require('fs');
fs.readFile('file.txt', (err, data) => {
if (err) throw err;
console.log(data);
});
- 事件处理 例如:处理DOM事件,如点击、滚动、键盘输入等
document.getElementById('myButton').addEventListener('click', () => {
console.log('Button clicked');
});
- UI渲染 在浏览器环境中,UI 渲染也是一个宏任务。当一个宏任务执行完毕后,浏览器会进行UI渲染。
setImmediate(Node.js环境) 在当前事件循环结束后的下一个事件循环迭代中执行回调。
setImmediate(() => {
console.log('setImmediate');
});
3.微任务有哪些
微任务是在当前宏任务执行完毕后立即执行的任务。微任务的优先级高于宏任务,这意味着在当前宏任务执行完毕后,事件循环会先执行所有微任务,然后再继续执行下一个宏任务。
Promise的then和catch回调 当Promise的状态变为fulfilled或rejected时,其then或catch方法注册的回调会被放入微任务队列
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
MutationObserver监听DOM变化并在变化发生时执行回调
const observer = new MutationObserver(mutations => {
console.log('DOM changed');
});
observer.observe(document.body, { childList: true, subtree: true });
process.nextTick(Node.js 环境) 将回调函数放到当前操作的末尾,但在事件循环继续之前执行
process.nextTick(() => {
console.log('process.nextTick');
});
queueMicrotask一个标准的API,用于将回调函数放入微任务队列。 queueMicrotask(() => { console.log('queueMicrotask'); });
4.JS是单线程,怎么通过事件循环执行异步代码?
JavaScript 是单线程的,这意味着它一次只能执行一个任务。所有任务需要排队,前一个任务结束之后才会执行后一个任务。如果前面的任务不结束,后面的任务就会一直等待。然而,JS 通过事件循环(Event Loop)和回调机制来处理异步代码,使得它能够在单线程环境下高效地执行异步操作。 事件循环是一种编程结构,它等待并发事件或消息。在 JS 中,事件循环是运行时系统的一部分,它负责执行代码、收集和处理事件以及执行队列中的子任务。事件循环不断地检查是否有待处理的任务,并根据一定的规则执行这些任务。具体来说,事件循环会先执行当前调用栈中的所有同步代码,然后处理微任务队列中的所有微任务,最后再执行宏任务队列中的下一个宏任务。这种机制确保了异步操作的有序执行,并且能够提高应用程序的响应性和性能。 通过这种方式,JS 能够在单线程环境下有效地管理异步操作,确保代码的高效执行和良好的用户体验。事件循环和回调机制使得异步代码能够按正确的顺序执行,而不阻塞主线程。
5.代码示例
示例1
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
console.log('Script end');
- 执行顺序
Script start
Script end
Promise 1
Promise 2
setTimeout
- 为什么这么执行?
- 同步代码执行: ○ console.log('Script start') 和 console.log('Script end') 依次执行。
- 宏任务: ○ setTimeout 的回调函数被放入宏任务队列。
- 微任务: ○ Promise 的 then 回调函数被放入微任务队列。
- 事件循环处理: ○ 当同步代码执行完毕,调用栈为空。 ○ 事件循环检查微任务队列,执行 Promise 1 和 Promise 2。 ○ 微任务队列清空后,事件循环从宏任务队列中取出 setTimeout 回调函数并执行。
示例2
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => {
console.log('Promise inside setTimeout 1');
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
queueMicrotask(() => {
console.log('queueMicrotask 1');
});
Promise.resolve().then(() => {
console.log('Promise ¼');
});
});
console.log('Script end');
- 执行顺序
Script start
Script end
Promise 1
queueMicrotask 1
Promise ¼
setTimeout 1
Promise inside setTimeout 1
- 为什么这么执行?
- 同步代码执行: ○ console.log('Script start') 和 console.log('Script end') 依次执行。 ○ setTimeout 的回调函数被放入宏任务队列。 ○ 第一个 Promise 的 then 回调函数被放入微任务队列。
- 处理微任务: ○ 事件循环检查微任务队列,执行 Promise 1。 ○ 在 Promise 1 的回调中,queueMicrotask 的回调函数被放入微任务队列,第二个 Promise 的 then 回调函数也被放入微任务队列。 ○ 事件循环继续执行微任务队列中的 queueMicrotask 1 和 Promise ¼。
- 处理宏任务: ○ 事件循环从宏任务队列中取出 setTimeout 1 的回调函数并执行。 ○ 在 setTimeout 1 的回调中,第三个 Promise 的 then 回调函数被放入微任务队列。 ○ 事件循环继续执行微任务队列中的 Promise inside setTimeout 1。
示例3
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => {
console.log('Promise inside setTimeout 1');
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('setTimeout inside Promise 1');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 2');
});
});
console.log('Script end');
- 执行顺序
Script start
Script end
Promise 1
Promise 2
setTimeout 1
Promise inside setTimeout 1
setTimeout inside Promise 1
- 为什么这么执行?
- 同步代码执行: ○ console.log('Script start') 和 console.log('Script end') 依次执行。 ○ setTimeout 的回调函数被放入宏任务队列。 ○ 第一个 Promise 的 then 回调函数被放入微任务队列。
- 处理微任务: ○ 事件循环检查微任务队列,执行 Promise 1。 ○ 在 Promise 1 的回调中,第二个 setTimeout 的回调函数被放入宏任务队列,第二个 Promise 的 then 回调函数被放入微任务队列。 ○ 事件循环继续执行微任务队列中的 Promise 2。
- 处理宏任务: ○ 事件循环从宏任务队列中取出 setTimeout 1 的回调函数并执行。 ○ 在 setTimeout 1 的回调中,第三个 Promise 的 then 回调函数被放入微任务队列。 ○ 事件循环继续执行微任务队列中的 Promise inside setTimeout 1。 ○ 事件循环从宏任务队列中取出 setTimeout inside Promise 1 的回调函数并执行。
示例4
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => {
console.log('Promise inside setTimeout 1');
setTimeout(() => {
console.log('setTimeout 2');
Promise.resolve().then(() => {
console.log('Promise inside setTimeout 2');
});
}, 0);
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('setTimeout inside Promise 1');
Promise.resolve().then(() => {
console.log('Promise inside setTimeout inside Promise 1');
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise 2');
});
});
console.log('Script end');
- 执行顺序
Script start
Script end
Promise 1
Promise 2
setTimeout 1
Promise inside setTimeout 1
setTimeout inside Promise 1
Promise inside setTimeout inside Promise 1
setTimeout 2
Promise inside setTimeout 2
- 为什么这么执行?
-
- 同步代码执行: ○ console.log('Script start') 和 console.log('Script end') 依次执行。 ○ 第一个 setTimeout 的回调函数被放入宏任务队列。 ○ 第一个 Promise 的 then 回调函数被放入微任务队列。
- 处理微任务: ○ 事件循环检查微任务队列,执行 Promise 1。 ○ 在 Promise 1 的回调中,第二个 setTimeout 的回调函数被放入宏任务队列,第二个 Promise 的 then 回调函数被放入微任务队列。 ○ 事件循环继续执行微任务队列中的 Promise 2。
- 处理宏任务: ○ 事件循环从宏任务队列中取出 setTimeout 1 的回调函数并执行。 ○ 在 setTimeout 1 的回调中,第三个 Promise 的 then 回调函数被放入微任务队列,第四个 setTimeout 的回调函数被放入宏任务队列。 ○ 事件循环继续执行微任务队列中的 Promise inside setTimeout 1。 ○ 事件循环从宏任务队列中取出 setTimeout inside Promise 1 的回调函数并执行。 ○ 在 setTimeout inside Promise 1 的回调中,第五个 Promise 的 then 回调函数被放入微任务队列。 ○ 事件循环继续执行微任务队列中的 Promise inside setTimeout inside Promise 1。 ○ 事件循环从宏任务队列中取出 setTimeout 2 的回调函数并执行。 ○ 在 setTimeout 2 的回调中,第六个 Promise 的 then 回调函数被放入微任务队列。 ○ 事件循环继续执行微任务队列中的 Promise inside setTimeout 2。