> For the complete documentation index, see [llms.txt](https://onenesss-organization.gitbook.io/jj-xiaoce/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://onenesss-organization.gitbook.io/jj-xiaoce/qian-duan-mian-shi-zhi-dao/08.-event-loop.md).

# Event Loop

在前两章节中我们了解了 JS 异步相关的知识。在实践的过程中，你是否遇到过以下场景，为什么 `setTimeout` 会比 `Promise` 后执行，明明代码写在 `Promise` 之前。这其实涉及到了 Event Loop 相关的知识，这一章节我们会来详细地了解 Event Loop 相关知识，学习 JS 异步运行代码的原理，并且这一章节也是面试常考知识点。

## 更新日志

* 各小节内容优化

## 进程与线程

相信大家经常会听到 JS 是**单线程**执行的，但是你是否疑惑过什么是线程？

讲到线程，那么肯定也得说一下进程。本质上来说，这两个名词都是 CPU **工作时间片**的一个描述。

进程描述了 CPU 在**运行指令及加载和保存上下文所需的时间**，放在应用上来说就代表了一个程序。线程是进程中的更小单位，描述了执行一段指令所需的时间。

把这些概念拿到浏览器中来说，当你打开一个 Tab 页时，其实就是创建了一个进程，一个进程中可以有多个线程，比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时，其实就是创建了一个线程，当请求结束后，该线程可能就会被销毁。

### 常见考点

* 进程线程区别
* JS 单线程执行带来的优缺点

## 执行栈

可以把执行栈认为是一个存储函数调用的**栈结构**，遵循先进后出的原则。

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/13/1670d2d20ead32ec~tplv-t2oaga2asx-image.image)

当开始执行 JS 代码时，首先会执行一个 `main` 函数，然后执行我们的代码。根据先进后出的原则，后执行的函数会先弹出栈，在图中我们也可以发现，`foo` 函数后执行，当执行完毕后就先从栈中弹出了。

另外在平时开发过程中，大家也可以在报错中找到执行栈的痕迹：

```js
function foo() {
  throw new Error('error')
}
function bar() {
  foo()
}
bar()
```

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/13/1670c0e21540090c~tplv-t2oaga2asx-image.image)

大家可以在上图清晰的看到报错在 `foo` 函数，`foo` 函数又是在 `bar` 函数中调用的。

当我们使用递归的时候，因为执行栈的内存空间是有限的，一旦存放了过多的函数且没有得到释放的话，就会出现爆栈的问题。

```js
function bar() {
  bar()
}
bar()
```

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/13/1670c128acce975f~tplv-t2oaga2asx-image.image)

### 常见考点

* 什么是执行栈

## 浏览器中的 Event Loop

上一小节中我们讲到了什么是执行栈，大家也知道了当我们执行 JS 代码的时候其实就是往执行栈中放入函数，那么当遇到异步代码的时候该怎么办？

其实当遇到异步代码时，会被**挂起**并在需要执行的时候加入到 Task（有多种 Task） 队列中。一旦执行栈为空，Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行。

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/23/16740fa4cd9c6937~tplv-t2oaga2asx-image.image)

不同的任务源会被分配到不同的 Task 队列中，任务源可以分为 **微任务**（microtask） 和 **宏任务**（macrotask）。在 ES6 规范中，microtask 称为 `jobs`，macrotask 称为 `task`。

微任务包括 `process.nextTick` 、`queueMicrotask`、`promise.then` 、`MutationObserver`，其中 `process.nextTick` 为 Node 独有。

宏任务包括 `script` 、 `setTimeout` 、`setInterval` 、`setImmediate` 、`I/O` 及 `UI rendering`。

讲了这么多，想必大家肯定对于这个 Loop 是怎么样执行的很感兴趣吧。

* 首先执行同步代码，这属于宏任务
* 当执行完所有同步代码后，执行栈为空，查询是否有异步代码需要执行
* 执行所有微任务
* 当执行完所有微任务后，如有必要会渲染页面
* 然后开始下一轮 Event Loop，执行宏任务中的异步代码，也就是 `setTimeout` 中的回调函数

这里很多人会有个误区，认为微任务快于宏任务，其实是错误的。因为宏任务中包括了 `script` ，浏览器一定会**先执行一个 `script`**，接下来才会执行微任务。

了解完以上内容，我们通过一段代码来看看掌握情况：

```js
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')
  })

console.log('script end')
// script start => async2 end => Promise => script end 
// => async1 end  => promise1 => setTimeout
```

以上代码能很好地帮助我们理解宏任务及微任务的执行顺序，务必搞懂。

如果你对执行顺序有迷惑的话，可以再结合 Event Loop 的执行顺利再思考下。我们首先根据代码执行顺序分离出同步代码，也就是注释第一行的 `log` 顺序。另外在这些同步代码执行过程中，微任务及宏任务也被推入了相应的队列中等待执行。

接下来我们再根据微任务进入队列的顺序得出打印结果为 `async1 end` 及 `promise1`。

最后当微任务执行完毕后需要执行宏任务，此时打印 `setTimeout`。

### 常见考点

* 笔试题，类似于上面的代码，让你写出 `log` 顺序
* 如何理解 Event loop

## Node 中的 Event Loop

Node 中的 Event Loop 和浏览器中的区别很大。

Node 的 Event Loop 分为 6 个阶段，它们会按照**顺序**反复运行。每当进入某一个阶段的时候，都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值，就会进入下一阶段。

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/13/1670c3fe3f9a5e2b~tplv-t2oaga2asx-image.image)

### timer

timers 阶段会执行 `setTimeout` 和 `setInterval` 回调，并且是由 poll 阶段控制的。

同样，在 Node 中定时器指定的时间也不是准确时间，只能是**尽快**执行。

### pending callbacks

这个阶段会执行上一轮循环中的**poll 阶段被延迟执行**的 I/O 回调。

### idle, prepare

这个阶段内部使用，略过。

### poll

这个阶段略复杂，做的事情会比较多。

首先如果队列不为空，会遍历回调队列并执行，直到队列为空或者达到系统限制。

接下来检查是否有 `setImmediate` 回调需要执行。有的话就直接结束 poll 阶段并且进入到 check 阶段执行回调。

如果没有 `setImmediate` 回调需要执行。会 block 线程并等待一会，看是否有新的事件进入队列，有的话就去执行。

在此过程中如果有 timer 需要执行，会回到 timer 阶段执行回调。

### check

这个阶段执行 `setImmediate` 回调。

### close callbacks

这个阶段执行关闭操作，比如关闭 `socket` 等。

上述阶段都为宏任务的执行阶段，对于 microtask 来说，它会在以上每个阶段完成前**清空** microtask 队列，下图中的 Tick 就代表了 microtask。

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/11/14/16710fb80dd42d27~tplv-t2oaga2asx-image.image)

最后我们来讲讲 Node 中的 `process.nextTick`，这个函数其实是独立于 Event Loop 之外的，它有一个自己的队列，当每个阶段完成后，如果存在 nextTick 队列，就会**清空队列中的所有回调函数**，并且优先于其他 microtask 执行。

```js
setTimeout(() => {
 console.log('timer1')

 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)

process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
```

对于以上代码，大家可以发现无论如何都是先把 `nextTick` 全部打印出来。

### 常见考点

* Node 中的 Event Loop 和浏览器中的有什么区别？
* process.nexttick 执行顺序？

## 小结

这一章节我们学习了 JS 实现异步的原理，并且了解了在浏览器和 Node 中 Event Loop 其实是不相同的。Event Loop 这个知识点对于我们理解 JS 是如何执行的至关重要，同时也是常考题。如果大家对于这个章节的内容存在疑问，欢迎在评论区与我互动。
