单线程

  1. JavaScript 的主要宿主环境为浏览器,主要用途是与用户互动操作 DOM
    • 如果采用多线程模型,会带来复杂的同步问题
  2. 诞生伊始,JavaScript 就是单线程,是其核心特性
  3. 为了利用多核 CPU 的计算能力,HTML 5 提出了 Web Worker 标准
    • 允许 JavaScript 创建多个子线程,但子线程完全受主线程控制,且不允许操作 DOM

任务队列

  1. JavaScript 在单线程模式下,所有任务都需要排队
  2. 任务分类
    • 同步任务(synchronous
      • 主线程排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
    • 异步任务(asynchronous
      • 不进入主线程,而是进入任务队列的任务
      • 只有任务队列通知主线程,某个异步任务可以执行了,该异步任务才会进入主线程执行
  3. 运行机制
    • 所有同步任务都在主线程上执行,形成一个执行栈
    • 主线程之外,还存在一个任务队列
      • 只要异步任务有了运行结果,就会在任务队列中放置一个事件
    • 一旦执行栈所有的同步任务执行完毕,系统就会读取任务队列中的事件
      • 事件对应的异步任务结束等待状态,进入执行栈开始执行
    • 主线程不断重复上一步骤(只要主线程空闲,就会去读取任务队列

bg2014100801

Event + Callback

  1. 任务队列是一个事件的队列
  2. 异步任务完成后会向任务队列添加一个事件:表示相关的异步任务可以进入执行栈
    • 异步任务本身不需要 CPU 参与,如 IO(DMA)
    • 但异步任务对应事件的回调函数需要占用 CPU 时间
  3. 主线程读取任务队列,实际上就是读取事件
  4. 任务队列中的事件:IO 事件、鼠标点击、页面滚动
    • 只要为 Event 指定过 Callback,当这些 Event 发生时就会进入任务队列,等待主线程读取
  5. Callback被主线程挂起的代码 – 需要占用 CPU 时间
    • 异步任务必须指定 Callback
    • 主线程开始执行异步任务,实际执行的就是对应的 Callback
  6. 任务队列 是一个 FIFO 的数据结构:排在前面的事件,优先被主线程读取
    • 主线程的读取过程基本上自动
      • 主要执行栈一清空,任务队列第1位的事件会自动进入主线程
      • 针对定时器,主线程会首先检查下执行时间,某些事件只有到了规定时间,才能返回主线程

Event Loop

event loop:主线程循环不断地从任务队列读取事件

0_6T6KIVRkN9nWb3QU

  1. 主线程运行时,会产生 HeapStack
  2. Stack 中的代码调用各种外部 API,在任务队列中加入各种事件
  3. 只要 Stack 中的代码执行完毕
    • 主线程就会去读取任务队列Callback Queue),依次执行那些事件对应的回调函数
  4. 执行栈中的代码(同步任务),总是在读取任务队列(异步任务)之前执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let request = new XMLHttpRequest();
request.open("GET", "http://www.example.org/example.txt")

request.send(); // 异步任务,只有在当前脚本的所有代码执行完,主线程才会去读取任务队列

// 指定 Callback 属于执行栈的一部分,执行完后才会去执行上面的 send
request.onload = function () {
};
request.onerror = function () {
};

// 执行过程
// 1. 主线程为 Event 指定 Callback
// 2. 执行异步任务 send
// 3. send 执行完后,往任务队列插入 Event
// 4. 主线程读取 Event,执行 Event 对应的 Callback

定时器

  1. 任务队列除了放置异步任务的事件,还可以放置定时事件(指定某些代码在多少时间后执行)
  2. 定时器功能主要由 setTimeout(一次性) 和 setInterval(周期性) 来完成,内部运行机制完全一样
1
2
3
4
5
6
7
8
// 1
// 3
// 2
console.log(1);
setTimeout(function () { // Callback
console.log(2);
}, 0); // 当前代码执行完(执行栈清空)后,立即执行 Callback
console.log(3);

setTimeout(fn, 0):尽可能早地执行
任务队列尾部添加一个事件,等到同步任务任务队列现有的事件都处理完后,才会得到执行

setTimeout: 只是将事件插入到任务队列
必须等到执行栈执行完,主线程才会去执行它指定Callback

如果执行栈耗时很长,此时无法保证回调函数一定会在 setTimeout 指定的时间执行