同步任务

因为 JavaScript 是单线程的,所以同步任务只能在主线程上排队执行,并且只有当前一个任务完成才能执行下一个任务

当 JavaScript 运行时,每个函数的执行都会进入调用栈(Call Stack)中,当前函数执行完毕后将其移出栈,然后执行下一个函数,如果其中有非常耗时的函数执行,那么调用栈就阻塞了,后面的任务只能一直处于等待状态无法执行,且此时 UI 对面任何用户操作都不会响应,看起来就像程序崩溃了一样。

为了解决这个问题,我们有了异步任务。

异步任务

常见的异步:

  • setTimeout()
  • setInterval()
  • Promise.resolve().then()
  • fetch.then()

异步任务在执行之前不会进入调用栈,它进入事件循环队列(Event Loop Queue),当主线程执行完毕后,且异步时机到了,就会将事件循环队列中的任务推入调用栈中执行。

Event Loop

Event Loop 的工作非常简单,它只是监控调用栈和回调队列(包含每个事件对应的回调函数,比如 click 的回调函数)。如果调用栈为空,Event Loop 将队列中的第一个任务移除并推送到调用栈。

从 ES6 开始,Event Loop 处理异步也分两种,在Macrotask中处理setTimeout这类的,在Microtask中处理Promise这类的。它会优先处理Microtask中的任务

browser event model promise constructor

Node.js 中的 Event Loop

Node.js 中的 Event Loop 分为 6 个阶段,依次是:

  • timers 定时器
    • 这个阶段执行已被setTimeout()setInterval()调度的回调函数
  • pending callbacks 待定回调
    • 执行延迟到下一个循环迭代的I/O回调
  • idle, prepare
    • Node.js 系统内部使用
  • poll 轮询
    • 检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞(等待)
  • check 检测
    • setImmediate()回调函数在这里执行
  • close callback 关闭的回调函数
    • 一些关闭的回调函数,如:sockt.on('close', ...)

process.nextTick()的回调会在每个阶段结束之前调用。

每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。

当我们在 Node 中首次执行 JavaScript 代码时,会有开启 Event Loop 和执行 JavaScript 两个阶段,这两个阶段是串行发生,开启速度受制于机器性能,这就是为什么在主线程中执行这两个异步,他们的顺序并不总是相同。

Promise Aplus

这里是我根据规范实现的 z-promise.js,另外还根据 promise aplus 画了两张流程图:

promise constructor promise constructor

promise.then promise.then

A+ 译文参考

以下内容待更新

  • async/await