JavaScript 中的异步任务、同步任务、宏任务与微任务
在 JavaScript 的世界里,理解异步任务、同步任务、宏任务和微任务是非常重要的,它们共同构成了 JavaScript 独特的执行机制。
一、同步任务与异步任务
1. 同步任务
- 定义:同步任务是在代码执行过程中,按照顺序依次执行的任务。每个同步任务必须等待前一个任务完成后才能开始执行。
- 特点:
- 阻塞代码执行,直到任务完成。
- 按照代码书写的顺序依次执行。
- 示例:
在这个例子中,首先会输出“同步任务 1”,然后输出“同步任务 2”。这两个任务是按照顺序依次执行的,前一个任务完成后,后一个任务才会开始执行。console.log('同步任务 1'); console.log('同步任务 2');
2. 异步任务
- 定义:异步任务是在代码执行过程中,不会立即执行,而是在特定的时间或条件满足后才会执行的任务。异步任务不会阻塞代码的执行,允许其他任务在等待异步任务完成的过程中继续执行。
- 特点:
- 不会阻塞代码执行,可以在后台执行。
- 通常需要回调函数来处理结果。
- 示例:
在这个例子中,首先会输出“同步任务 1”,然后输出“同步任务 2”。接着,由于console.log('同步任务 1'); setTimeout(() => { console.log('异步任务'); }, 1000); console.log('同步任务 2');
setTimeout
是一个异步任务,它会在 1000 毫秒后执行回调函数,输出“异步任务”。在等待异步任务执行的过程中,其他同步任务可以继续执行。
二、宏任务与微任务
1. 宏任务
- 定义:宏任务是由浏览器或 Node.js 等环境提供的任务,通常包括
setTimeout
、setInterval
、Ajax 请求
、DOM 事件
等。宏任务会在主线程上依次执行,每个宏任务执行完毕后,会检查微任务队列是否为空,如果不为空,则执行微任务队列中的所有任务。 - 特点:
- 执行时间相对较长。
- 会导致页面的重新渲染。
- 示例:
在这个例子中,console.log('同步任务 1'); setTimeout(() => { console.log('宏任务 1'); }, 0); console.log('同步任务 2');
setTimeout
中的回调函数是一个宏任务,会在同步任务执行完毕后被放入任务队列等待执行。即使设置的时间为 0,也不会立即执行,而是在同步任务执行完毕后,按照任务队列的顺序执行。
2. 微任务
- 定义:微任务是在当前宏任务执行过程中产生的,并且会在当前宏任务执行完毕后立即执行。常见的微任务包括
Promise.then()
、Promise.catch()
、Promise.finally()
、MutationObserver
等。 - 特点:
- 执行时间相对较短。
- 优先级高于宏任务。
- 示例:
在这个例子中,console.log('同步任务 1'); Promise.resolve().then(() => { console.log('微任务 1'); }); console.log('同步任务 2');
Promise.resolve().then()
中的回调函数是一个微任务,会在同步任务执行完毕后,且在宏任务执行之前被执行。
三、事件循环
JavaScript 是单线程语言,通过事件循环来管理同步任务和异步任务的执行。事件循环的工作原理如下:
- 首先执行同步任务,将同步任务依次放入主线程执行。
- 当遇到异步任务时,将异步任务放入任务队列中等待执行。异步任务分为宏任务和微任务,分别放入不同的任务队列。
- 当主线程中的同步任务执行完毕后,会先检查微任务队列是否为空。如果微任务队列不为空,则执行微任务队列中的所有任务,这个过程会持续进行,直到微任务队列为空。
- 微任务队列处理完后,才会从宏任务队列中取出一个宏任务并执行。
- 重复步骤 3 和 4,直到任务队列中的所有任务都被执行完毕。
例如:
console.log('同步任务 1');
setTimeout(() => {
console.log('宏任务 1');
}, 0);
Promise.resolve().then(() => {
console.log('微任务 1');
});
console.log('同步任务 2');
在这个例子中,首先执行“同步任务 1”和“同步任务 2”。然后,由于setTimeout
是宏任务,它会被放入宏任务队列中等待执行。同时,Promise.resolve().then()
是微任务,会被放入微任务队列中。当同步任务执行完毕后,会先执行微任务队列中的“微任务 1”,然后才会从宏任务队列中取出“宏任务 1”执行。
面试题:
console.log('同步任务 start');
setTimeout(() => {
console.log('宏任务 1');
Promise.resolve().then(() => {
console.log('微任务 within 宏任务 1');
});
}, 0);
Promise.resolve().then(() => {
console.log('微任务 1');
setTimeout(() => {
console.log('宏任务 within 微任务 1');
}, 0);
});
setTimeout(() => {
console.log('宏任务 2');
}, 0);
console.log('同步任务 end');
输出结果:
> "同步任务 start"
> "同步任务 end"
> "微任务 1"
> "宏任务 1"
> "微任务 within 宏任务 1"
> "宏任务 2"
> "宏任务 within 微任务 1"
以下是对上述代码执行流程的解释:
- 首先,执行同步任务。
- 输出
同步任务 start
。 - 接着遇到第二个同步任务,输出
同步任务 end
。
- 输出
- 同步任务执行完毕后,开始检查微任务队列。
- 此时微任务队列为空,所以继续从宏任务队列中取出任务执行。
- 从宏任务队列中取出第一个由
setTimeout
注册的宏任务执行。- 输出
宏任务 1
。 - 在这个宏任务中,又有一个
Promise.resolve().then()
,它会注册一个微任务,即console.log('微任务 within 宏任务 1');
被放入微任务队列。
- 输出
- 接着,回到宏任务队列继续检查是否还有未执行的宏任务。
- 由于还有两个由
setTimeout
注册的宏任务未执行,但是根据事件循环机制,此时要先检查微任务队列。
- 由于还有两个由
- 微任务队列中有一个任务,即
console.log('微任务 within 宏任务 1');
,执行这个微任务,输出微任务 within 宏任务 1
。 - 微任务队列处理完毕后,再次从宏任务队列中取出下一个任务执行。
- 输出
宏任务 2
。
- 输出
- 此时宏任务队列中还有一个任务,是在
微任务 1
中注册的setTimeout
回调,即console.log('宏任务 within 微任务 1');
。 - 执行这个宏任务,输出
宏任务 within 微任务 1
。