Appearance
Event Loop
进程、线程
进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
线程是进程的执行流,是 CPU 调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。
浏览器内核
浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。
浏览器内核有多种线程在工作。
GUI 渲染线程:
- 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。
- 和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
JS 引擎线程:
- 单线程工作,负责解析运行 JavaScript 脚本。
- 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
事件触发线程:
- 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
定时器触发线程:
浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
http 请求线程:
http 请求的时候会开启一条请求线程。
请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
JavaScript 引擎是单线程
JavaScript 引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。
HTML5 中提出了 Web-Worker API,主要是为了解决页面阻塞问题,但是并没有改变 JavaScript 是单线程的本质。了解 Web-Worker。
堆(Heap)
堆是一种数据结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆是线性数据结构,相当于一维数组,有唯一后继。
栈
栈(Stack) 栈在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。 栈是只能在某一端插入和删除的特殊线性表。
Event Loop
在 JavaScript 中,任务被分为两种,一种宏任务(MacroTask)也叫 Task,一种叫微任务(MicroTask)。
s | 宏任务(macrotask) | 微任务(microtask) |
---|---|---|
谁发起的 | 宿主(Node、浏览器) | JS 引擎 |
具体事件 | script 全部代码、setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有 IE10 支持,具体可见 MDN)、I/O、UI Rendering。 | Process.nextTick(Node 独有)、Promise、Object.observe(废弃)、MutationObserver(具体使用方式查看这里 |
谁先运行 | 后运行 | |
会触发新一轮 Tick 吗 | 会 |
浏览器中的 Event Loop
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
JS 调用栈
JS 调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
同步任务和异步任务
Javascript 单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
任务队列 Task Queue
,即队列,是一种先进先出的一种数据结构。
事件循环的进程模型
选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即 null,则执行跳转到微任务(MicroTask)的执行步骤。
- 将事件循环中的任务设置为已选择任务。
- 执行任务。
- 将事件循环中当前运行任务设置为 null。
- 将已经运行完成的任务从任务队列中删除。
- microtasks 步骤:进入 microtask 检查点。
- 更新界面渲染。
- 返回第一步。
执行进入 microtask 检查点时,用户代理会执行以下步骤:
设置 microtask 检查点标志为 true。 当事件循环 microtask 执行不为空时:选择一个最先进入的 microtask 队列的 microtask,将事件循环的 microtask 设置为已选择的 microtask,运行 microtask,将已经执行完成的 microtask 为 null,移出 microtask 中的 microtask。 清理 IndexDB 事务 设置进入 microtask 检查点的标志为 false。
上述可能不太好理解,下图是我做的一张图片。

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务 (microTask) 队列是否为空,如果为空的话,就执行 Task(宏任务),否则就一次性执行完所有微任务。 每次单个宏任务执行完毕后,检查微任务 (microTask) 队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务 (microTask) 后,设置微任务 (microTask) 队列为 null,然后再执行宏任务,如此循环。
例子
javascript
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
第一次执行:
Tasks:run script、 setTimeout callback
Microtasks:Promise then
JS stack: script
Log: script start、script end。
1
2
3
4
5
6
2
3
4
5
6
执行同步代码,将宏任务(Tasks
)和微任务 (Microtasks
) 划分到各自队列中。
第二次执行:
Tasks:run script、 setTimeout callback
Microtasks:Promise2 then
JS stack: Promise2 callback
Log: script start、script end、promise1、promise2
1
2
3
4
5
6
2
3
4
5
6
执行宏任务后,检测到微任务 (Microtasks
) 队列中不为空,执行Promise1
,执行完成Promise1
后,调用Promise2.then
,放入微任务 (Microtasks
) 队列中,再执行Promise2.then
。
第三次执行:
Tasks:setTimeout callback
Microtasks:
JS stack: setTimeout callback
Log: script start、script end、promise1、promise2、setTimeout
1
2
3
4
5
6
2
3
4
5
6
当微任务 (Microtasks) 队列中为空时,执行宏任务(Tasks),执行 setTimeout callback,打印日志。
第四次执行:
Tasks:setTimeout callback
Microtasks:
JS stack:
Log: script start、script end、promise1、promise2、setTimeout
1
2
3
4
5
6
2
3
4
5
6
清空 Tasks 队列和 JS stack。
以上执行帧动画可以查看Tasks, microtasks, queues and schedules或许这张图也更好理解些。
举例子
javascript
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
这里需要先理解 async/await。 async/await 在底层转换成了 promise 和 then 回调函数。 也就是说,这是 promise 的语法糖。 每次我们使用 await, 解释器都创建一个 promise 对象,然后把剩下的 async 函数中的操作放到 then 回调函数中。 async/await 的实现,离不开 Promise。从字面意思来理解,async 是“异步”的简写,而 await 是 async wait 的简写可以认为是等待异步方法执行完成。
关于 73 以下版本和 73 版本的区别
在老版本版本以下,先执行 promise1 和 promise2,再执行 async1。 在 73 版本,先执行 async1 再执行 promise1 和 promise2。
主要原因是因为在谷歌(金丝雀)73 版本中更改了规范,如下图所示:
区别在于 RESOLVE(thenable) 和之间的区别 Promise.resolve(thenable)。
在老版本中
首先,传递给 await 的值被包裹在一个 Promise 中。然后,处理程序附加到这个包装的 Promise,以便在 Promise 变为 fulfilled 后恢复该函数,并且暂停执行异步函数,一旦 promise 变为 fulfilled,恢复异步函数的执行。 每个 await 引擎必须创建两个额外的 Promise(即使右侧已经是一个 Promise)并且它需要至少三个 microtask 队列 ticks(tick 为系统的相对时间单位,也被称为系统的时基,来源于定时器的周期性中断(输出脉冲),一次中断表示一个 tick,也被称做一个“时钟滴答”、时标。)。
引用知乎上的一个例子
javascript
async function f() {
await p
console.log('ok')
}
1
2
3
4
2
3
4
简化理解为:
javascript
function f() {
return RESOLVE(p).then(() => {
console.log('ok')
})
}
1
2
3
4
5
2
3
4
5
- 如果 RESOLVE(p) 对于 p 为 promise 直接返回 p 的话,那么 p 的 then 方法就会被马上调用,其回调就立即进入 job 队列。
- 而如果 RESOLVE(p) 严格按照标准,应该是产生一个新的 promise,尽管该 promise 确定会 resolve 为 p,但这个过程本身是异步的,也就是现在进入 job 队列的是新 promise 的 resolve 过程,所以该 promise 的 then 不会被立即调用,而要等到当前 job 队列执行到前述 resolve 过程才会被调用,然后其回调(也就是继续 await 之后的语句)才加入 job 队列,所以时序上就晚了。
谷歌(金丝雀)73 版本中
使用对 PromiseResolve 的调用来更改 await 的语义,以减少在公共 awaitPromise 情况下的转换次数。 如果传递给 await 的值已经是一个 Promise,那么这种优化避免了再次创建 Promise 包装器,在这种情况下,我们从最少三个 microtick 到只有一个 microtick。
详细过程:
73 以下版本
- 首先,打印 script start,调用 async1() 时,返回一个 Promise,所以打印出来 async2 end。
- 每个 await,会新产生一个 promise, 但这个过程本身是异步的,所以该 await 后面不会立即调用。
- 继续执行同步代码,打印 Promise 和 script end,将 then 函数放入微任务队列中等待执行。
- 同步执行完成之后,检查微任务队列是否为 null,然后按照先入先出规则,依次执行。
- 然后先执行打印 promise1, 此时 then 的回调函数返回 undefinde,此时又有 then 的链式调用,又放入微任务队列中,再次打印 promise2。
- 再回到 await 的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,打印 async1 end。
- 当微任务队列为空时,执行宏任务,打印 setTimeout。
谷歌(金丝雀 73 版本)
- 如果传递给 await 的值已经是一个 Promise,那么这种优化避免了再次创建 Promise 包装器,在这种情况下,我们从最少三个 microtick 到只有一个 microtick。
- 引擎不再需要为 await 创造 throwaway Promise - 在绝大部分时间。
- 现在 promise 指向了同一个 Promise,所以这个步骤什么也不需要做。然后引擎继续像以前一样,创建 throwaway Promise,安排 PromiseReactionJob 在 microtask 队列的下一个 tick 上恢复异步函数,暂停执行该函数,然后返回给调用者。
具体详情查看 这里。