一个小实验

让我们先来做一个小实验, 看看是立即resolve的Promise先执行还是等待时间为0的setTimeout先执行:

Promise.resolve(1).then(() => {
  console.log('Resolved!');
});
setTimeout(() => {
  console.log('Timed out!');
}, 0);
// logs 'Resolved!'
// logs 'Timed out!'

可以看到是Promise先执行的, 可能有人会说是因为Promise写在了setTimeout前面, 那么就让我们将他俩换个位置:

setTimeout(() => {
  console.log('Timed out!');
}, 0);
Promise.resolve(1).then(() => {
  console.log('Resolved!');
});
// logs 'Resolved!'
// logs 'Timed out!'

可以看到结果是一样的, 为什么即使这个立即resolve的Promise写在了等待时间为0的setTimeout后面, 依旧是Promise先执行呢? 在这篇文章中便揭晓答案。

Event Loop

之前这个小实验体现的是JavaScript异步的特性, 所以我们需要从JavaScript的异步入手, 而JavaScript是通过Event Loop来实现异步的:

event_loop

  • Call Stack: 它是一个LIFO (后进先出) 的栈型数据结构, 他存储了JS代码执行的上下文, 简单来说就是call stack会执行JS中的function。
  • Web APIs: 它是异步操作(Fetch API、Promise、Timer)及其回调函数等待完成的地方。
  • Task Queue (也称宏任务macro tasks): 它是一个FIFO (先进先出) 的队列型数据结构, 在他的里面存储了所有准备执行的异步操作的回调函数, 例如setTimeout()的回调函数一旦该执行了,就会进入到这个macro task queue中。
  • Job Queue (也称微任务micro tasks): 它也是一个FIFO (先进先出) 的队列型数据结构, 它里面存储了准备执行的Promise的回调函数, 例如一个fulfilled的Promise的resolve或reject的回调会被送入这个队列中准备执行。

Job Queue vs Task Queue

回到文章开始的那个小实验, 让我们看看JavaScript是怎么执行他们的:

  1. Call Stack执行了setTimeout(…, 0)并且schedule了一个定时器, 将它的回调函数送入Web APIs中:
setTimeout(() => {
  console.log('Timed out!');
}, 0);

settimeout_event_loop

  1. Call Stack 执行了Promise.resolve().then(), 并schedule了一个resolve,将then的回调放入Web APIs中:
Promise.resolve(1).then(() => {
  console.log('Resolved!');
});

promise_event_loop

  1. 由于Promise会立即resolve, setTimeout会立即timeout, 所以他俩的回调函数会立即被送入Task Queue (setTimeout) 和 Job Queue (Promise):

event_loop_enqueue

  1. 接下来, Event Loop会先将Job Queue中的回调压入Call Stack中并执行, 此时“Resolved!”就被打印了出来:

event_loop_dequeue_micro

  1. 最后Event Loop将Task Queue中的回调压入Call Stack中并执行, 此时“Timed out!”便被打印出来了:

event_loop_dequeue_macro

可以从上面这个过程中看到, Event Loop会优先取出Job Queue中的回调, 然后直到等Job Queue空了才回去处理Task Queue, 这也说明了: 即便上面的小实验中setTimeout后面跟了好几个Promise.resolve(1).then(), 都将是Promises的回调们先执行, 最后才会执行setTimeout的回调。

另外, Event Loop会先等待Call Stack执行完里的所有方法, 直到Call Stack空了才回去处理Job Queue和Task Queue, 而非异步的方法会直接被放入Call Stack中去执行, 这也说明了一个问题: Promise和setTimeout不一定会按时执行, 如果Call Stack里的方法很多很耗时的话, Event Loop会一直等待这些方法执行完才会去处理Promise和setTimeout的回调。

总结

这篇文张讲述了为什么立即resolve的Promise会比立即timeout的setTimeout先执行, 从而引出了JavaScript的异步核心:Event Loop的工作流程。希望这篇文章可以在你的工作生活中对你有所帮助😎。