本文深入探讨 JavaScript 事件循环中的任务队列(Task Queue)和微任务队列(Job Queue/Microtask Queue)的执行顺序。通过分析 setTimeout 和 promise 的交互,揭示了即使微任务队列优先级更高,依赖于任务队列中任务的微任务也必须等待其依赖的任务执行完毕后才能执行。本文将通过代码示例详细解释这一过程,并提供相关注意事项,帮助开发者更好地理解 JavaScript 的异步机制。
深入理解 JavaScript 事件循环
JavaScript 是一种单线程语言,这意味着它一次只能执行一个任务。为了处理异步操作,JavaScript 使用事件循环机制。事件循环不断地检查调用栈是否为空,如果为空,则从任务队列中取出第一个任务放入调用栈中执行。
事件循环主要包含以下几个关键组成部分:
- 调用栈(Call Stack): 用于执行同步代码。
- 任务队列(Task Queue/Callback Queue): 用于存放异步任务的回调函数,例如 setTimeout、setInterval、事件监听器等。也称宏任务队列。
- 微任务队列(Job Queue/Microtask Queue): 用于存放微任务的回调函数,例如 Promise.then、MutationObserver 等。
- WebAPIs: 浏览器提供的 API,例如 setTimeout、dom 事件等。
任务队列与微任务队列的优先级
微任务队列的优先级高于任务队列。这意味着,当调用栈为空时,事件循环会首先检查微任务队列,如果微任务队列中有任务,则会优先执行微任务队列中的所有任务,直到微任务队列为空,才会去执行任务队列中的任务。
立即学习“Java免费学习笔记(深入)”;
setTimeout 与 Promise 的交互
setTimeout 的回调函数会被放入任务队列中,而 Promise.then 的回调函数会被放入微任务队列中。因此,通常情况下,Promise.then 的回调函数会比 setTimeout 的回调函数更早执行。
考虑以下代码:
setTimeout(() => { console.log('1'); }, 0); Promise.resolve('2').then(console.log); console.log('3');
这段代码的执行顺序如下:
- setTimeout 被添加到调用栈。
- setTimeout 被推送到 WebAPIs,并从调用栈中弹出。
- Promise.resolve 被识别为异步函数,其回调函数被推入微任务队列。
- console.log(‘3’) 被推入调用栈并执行,输出 3。
- 调用栈为空,事件循环将微任务队列中的 Promise.then 回调函数推入调用栈并执行,输出 2。
- 调用栈为空,事件循环将任务队列中的 setTimeout 回调函数推入调用栈并执行,输出 1。
因此,最终输出结果为 3 2 1。
依赖关系的影响
现在,考虑以下代码:
setTimeout(() => { console.log('1'); }, 0); Promise.resolve(setTimeout(() => { console.log('2'); }, 0)); console.log('3');
这段代码的执行顺序有所不同。Promise.resolve 接收的是 setTimeout 的返回值(timeout ID),而不是 setTimeout 回调函数本身。这意味着 Promise.resolve 立即被解析,并将一个已经完成的 Promise 放入微任务队列。而 setTimeout 的回调函数仍然在任务队列中等待执行。
因此,这段代码的执行顺序如下:
- setTimeout 被添加到调用栈。
- setTimeout 被推送到 WebAPIs,并从调用栈中弹出。
- Promise.resolve 被识别为异步函数,其回调函数被推入微任务队列。
- console.log(‘3’) 被推入调用栈并执行,输出 3。
- 调用栈为空,事件循环将微任务队列中的 Promise.then 回调函数推入调用栈并执行。由于 Promise 已经 resolve,所以 Promise.then 立即执行,但是它并没有输出任何东西,因为它接收的是 setTimeout 的 ID。
- 调用栈为空,事件循环将任务队列中的第一个 setTimeout 回调函数推入调用栈并执行,输出 1。
- 调用栈为空,事件循环将任务队列中的第二个 setTimeout 回调函数推入调用栈并执行,输出 2。
因此,最终输出结果为 3 1 2。
关键在于,即使 Promise.then 的回调函数在微任务队列中,但它依赖于 setTimeout 的执行结果(虽然这里只是 setTimeout 的 ID,但仍然存在依赖关系)。因此,setTimeout 必须先执行,Promise.then 才能完成。
总结与注意事项
- JavaScript 事件循环是理解异步编程的关键。
- 微任务队列的优先级高于任务队列。
- 即使微任务队列优先级更高,依赖于任务队列中任务的微任务也必须等待其依赖的任务执行完毕后才能执行。
- 在编写异步代码时,要仔细考虑任务之间的依赖关系,以确保代码按照预期的顺序执行。
- 避免在微任务中执行耗时操作,以免阻塞事件循环。
通过理解 JavaScript 事件循环的机制,我们可以更好地编写高效、可靠的异步代码。