JavaScript中的异步与EventLoop
2021-9-30
| 2023-2-19
0  |  0 分钟
password
Created
Feb 19, 2023 04:09 PM
type
Post
status
Published
date
Sep 30, 2021
slug
summary
JavaScript中的异步与EventLoop
tags
Eventloop
JavaScript
category
原理
icon

EventLoop

说起EventLoop, 我们可以很自然的想到宏任务, 微任务, 微任务比宏任务先执行
同时, 同一次Loop下, 会先清空微任务的调用栈, 然后再执行宏任务
当我们研究深入会发现一些更细节的问题:
  1. 浏览器上和Nodejs上的EventLoop还是存在些许差异的, 那么它们各自是怎么实现的?
  1. Nodejs上的特异性API如process.nextTick和setImmidiate似乎与EventLoop的概念有些相悖

一. 浏览器和Nodejs的EventLoop差异

在浏览器上, 我们众所周知的是: JS是一个单线程的语言, 也就意味着它无法通过语言本身的语法, 语句来创造出另一个线程, 以使其来进行一些异步的工作.
但是, 单线程的理念与异步又是互斥的, 因为异步意味着我的异步任务一定是在线程之外去执行的, 当执行完毕之后, 才会将结果"插"回JS的执行线程上.
基本的理念如此, 那么在执行细节上呢?

1. 浏览器

在浏览器上, 我们知道浏览器是将绘制与逻辑执行放到了不同的线程
在UI线程上, 使用CSSOM + DOM来组合生成UI.
在JS执行线程上, 则进行JS逻辑脚本的执行.
同时, 二者相互阻塞, JS阻塞时, UI绘制会暂停执行. UI绘制阻塞时, JS逻辑会暂停执行.
以上其实是相对通识的内容, 下面便是这次需要重点讲的点: Eventloop线程
如上面所说, 对于异步的任务, 其实浏览器是额外开辟了一个线程来处理的, 在这个线程上处理异步任务, 然后将其传回到JS的执行线程上
其实除此之外, 网络IO也会开启一个网络线程, 这个不在本次的讨论范围之内
这时候, 宏任务, 微任务就能很好解释其行为了.
  1. 每一次的EventLoop, 都会创建一个微任务队列, 和宏任务队列
  1. 每次循环会执行微任务队列中的回调, 再执行宏任务.
其中有一个特殊情况, requestAnimationFrame看似是一个微任务, 但是其实这个回调是执行在渲染阶段, 并非Eventloop中

2. Nodejs

Nodejs上没有UI线程, 虽然也用到了V8引擎, 但是本质的实现方式还是有很大区别的
notion image
在Nodejs上, EventLoop是分阶段的
  1. timers执行setTimeout和setInterval的回调
  1. pendding callbacks执行延迟到下一个循环迭代的IO回调
  1. idle, prepare仅系统内部使用
  1. pool 用来检查新的IO时间, 执行与IO相关的回调. 事实上除了其他几个阶段处理的事情, 其他几乎所有异步都在这里处理
  1. check setImmediate在这里执行
  1. close callbacks 一些关闭的回调函数 如: socket.on('close', ...)

二. Nodejs的特殊API的执行时机

1. setImmediate

Nodejs其实可以看到, 对于浏览器上的一些宏任务/微任务API, 在Nodejs上是在不同的层去执行的, 基于这个, 有一个示例代码:
 setTimeout(() => {    setTimeout(() => {      console.log('setTimeout');   }, 0);    setImmediate(() => {      console.log('setImmediate');   });  }, 0);
上面代码的执行结果很容易猜到, 是setImmediate先执行
因为在外城setTimeout执行回调的时候, 此时处于timers阶段, 接下来会优先到check阶段, 这个时候setImmediate就执行了, 而下一次的timer一定比check阶段晚. 所以一定是setImmediate先执行
但是, 下面的示例是怎么样的运行结果呢?
 setImmediate(() => {    setTimeout(() => {      console.log('setTimeout');   }, 0);    setImmediate(() => {      console.log('setImmediate');   });  });
我们发现, setimeout和setImmediate会有时先有时后.
我们了解了这个阶段的概念之后, 就能很好的解释了, 这里有一个前置知识是: setTimeout设置为0的时候, 会被Nodejs强制改为1
当我们执行外层的setImmediate的时候, 此时在check阶段, 下一个阶段是timers阶段, 这个时候我们已经将setTimeout入到调用栈了, 但是因为延时为1ms, 所以这个时候setTimeout可能执行也可能不执行, 执行则打印出来, 不执行则会进入check阶段, 所以先打印了setImmediate
其实到这里我们就能够理解nodejs的EventLoop机制的通知, 顺便理解nodejs上的setImmediate这个api了

2. process.nextTick

除了setImmediate以外, 其实还有另一个API, process.nextTick
process.nextTick是一个特殊的异步API, 它不属于任何EventLoop阶段, 在执行到process.nextTick时, Nodejs会马上停下来执行这个api, 之后再继续执行EventLoop
也就是说, process.nextTick一定会在同步执行完成之后立即执行这个api
 const promise = Promise.resolve()  setImmediate(() => {    console.log('setImmediate');  });  promise.then(()=>{      console.log('promise')  })  process.nextTick(()=>{      console.log('nextTick')  })  console.log('sync')  // 执行顺序: sync nextTick(同步结束后立即执行) promise(poll阶段执行) setImmediate(check阶段执行)

3. 扩展: vue.nextTick Vs process.nextTick

Vue.nextTick保证了我们在回调中能够安全获取到DOM节点, 这是Vue.nextTick的核心能力
在Vue.nextTick中对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0
而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。
其实这里就很好理解了: 不同于process.nextTick所提供的的原生实现, Vue.nextTick是一个支持优雅降级的异步API的实现, 优先使用微任务, 不行就逐个降级使用宏任务.

三. 总结

浏览器与Nodejs的EventLoop实现有很大不同
浏览器依赖于宿主(浏览器)环境提供的多线程模型, 将异步任务放在异步线程上执行, 执行完毕后传入JS线程
Nodejs则使用了分层的机制来处理异步任务, 主要的几个层的情况如下:
  1. process.nextTick 不属于任何层, 一定最先执行
  1. timers (setInterva setTimeout)
  1. poll 几乎所有的异步都在这个阶段处理
  1. check setImmediate执行
Vue.nextTick相比于process.nextTick来说, 虽然同名为nextTick, Vue.nextTick是一个代码实现, 支持优雅降级的异步API, 而process.nextTick则是Nodejs的原生API

四. 参考文献

原理
  • Eventloop
  • JavaScript
  • Vue中AST如何生成可运行代码Vue中Props的实现机制
    目录