JavaScript是怎样运行的?

同步代码,包括同步函数、表达式,以调用栈的方式执行;所有回调函数在任务会依次进入任务队列(Task Queue),依次等待执行,也就是 Event Loop。任务队列有多个, 可以分为两大类:Macro Tasks 和 Micro Tasks。下面以伪代码演示浏览器和 Node 中的 Event Loop 运行模型。

浏览器

// Event Loop可以看成程序的入口函数Main
while (true) {
  // 任务队列的执行顺序是任意的,但同一来源的任务必须在同一队列中,同一队列中的任务严格按照进入队列的顺序执行。
  // XHR请求,Web APIs,setTimeout,setInterval中的回调都会进入Macro Task队列。
  queue = getNextQueue();

  // 任务执行耗时如果太长,假设超过16ms,就会导致后面的重绘函数无法及时执行,画面就会出现卡顿掉帧现象。
  task = queue.pop();
  execute(task);

  // 99.99%的Micro Task是Promise.resolve()。只有当所有的Promise.resolve()执行完后,才会进入重绘函数。所以如果有无限嵌套的,Promise.resolve(),
  // 页面将会卡住,停止响应。
  while (microtaskQueue.hasTasks()) doMicrotask();

  if (isRepaintTime()) {
    // 执行动画任务队列,也就是requestAnimationFrame中的回调函数。
    animationTasks = animationQueue.copyTasks();
    for (task in animationTasks) doAnimationTask(task);

    // 执行重绘
    repaint();
  }
}

Node

Node 中的 Event Loop 模型相对简单,没有 Web API,没有动画队列。但多了 process.nextTick 这种 Micro Task。

while (tasksAreWaiting()) {
  queue = getNextQueue();

  while (queue.hasTasks()) {
    task = queue.pop();
    execute(task);

    // setImediate()在nextTick()中执行
    while (nextTickQueue.hasTasks()) {
      doNextTickTask();
    }

    // Promise任务队列
    while (promiseQueue.hasTasks()) {
      doPromiseTask();
    }
  }
}