本文旨在深入剖析JavaScript事件循环机制中任务队列(Task Queue)与微任务队列(Job Queue,也称Microtask Queue)的执行优先级和相互影响。通过具体代码示例,详细解释了setTimeout、promise等异步操作在事件循环中的调度方式,以及微任务如何优先于任务队列中的任务执行,从而帮助开发者更深入地理解JavaScript的异步编程模型。
JavaScript事件循环机制
JavaScript 是一种单线程语言,这意味着它一次只能执行一个任务。为了处理异步操作,JavaScript 引入了事件循环机制。事件循环不断地检查调用栈(Call Stack)是否为空,如果为空,则从任务队列(Task Queue)中取出第一个任务放入调用栈中执行。执行完毕后,再次检查调用栈,如此循环。
任务队列(Task Queue)与微任务队列(Job Queue)
任务队列和微任务队列都是用于存放待执行任务的队列,但它们的优先级不同。
- 任务队列(Task Queue):也称为宏任务队列,用于存放诸如 setTimeout、setInterval、I/O 操作等异步任务的回调函数。
- 微任务队列(Job Queue):也称为微任务队列,用于存放诸如 Promise.then、MutationObserver 等异步任务的回调函数。
执行顺序:
立即学习“Java免费学习笔记(深入)”;
- 执行全局同步代码。
- 当调用栈为空时,检查微任务队列。
- 如果微任务队列不为空,则依次执行微任务队列中的所有微任务。
- 当微任务队列为空时,从任务队列中取出一个任务放入调用栈中执行。
- 重复步骤 2-4。
关键在于,每次从任务队列中取出一个任务执行后,都会立即清空微任务队列,然后再取下一个任务。 这意味着微任务的优先级高于任务队列中的任务。
代码示例分析
以下面的代码为例:
setTimeout(() => { console.log('1'); }, 0); Promise.resolve('2').then(console.log); console.log('3');
这段代码的执行过程如下:
- setTimeout 被添加到调用栈,然后被推入 WebAPI,并从调用栈中弹出。setTimeout 的回调函数被放入任务队列。
- Promise.resolve(‘2’).then(console.log) 被添加到调用栈。Promise.resolve 创建一个已解决的 Promise,其 then 方法的回调函数被放入微任务队列。
- 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');
这段代码的输出结果是 3 1,然后一段时间后输出 2。
原因分析:
这段代码中,Promise.resolve 的参数是 setTimeout 的返回值,也就是 setTimeout 的 ID(一个数字)。Promise.resolve 创建一个已解决的 Promise,其值为 setTimeout 的 ID。因此,Promise.then 的回调函数并没有被执行,而 setTimeout 的回调函数仍然被放入任务队列,按照 setTimeout 的执行顺序执行。
正确的理解是:setTimeout 的调用是同步的,它将一个回调函数注册到 WebAPI,并返回一个 timer ID。这个 timer ID 被 Promise.resolve 包裹,并立即resolve。console.log(‘2’) 最终会在 setTimeout 的回调函数中执行,它与 Promise 的状态无关。
总结与注意事项
- 微任务队列的优先级高于任务队列。
- 每次从任务队列中取出一个任务执行后,都会立即清空微任务队列。
- Promise.resolve(setTimeout(…)) 这种写法容易产生误解,应该避免。
- 理解事件循环机制对于编写高性能的 JavaScript 代码至关重要。
通过深入理解 JavaScript 的事件循环机制,我们可以更好地控制异步代码的执行顺序,避免出现意外的行为,并编写出更加高效和可靠的 JavaScript 应用。