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下, 会先清空微任务的调用栈, 然后再执行宏任务
当我们研究深入会发现一些更细节的问题:
- 浏览器上和Nodejs上的EventLoop还是存在些许差异的, 那么它们各自是怎么实现的?
- Nodejs上的特异性API如process.nextTick和setImmidiate似乎与EventLoop的概念有些相悖
一. 浏览器和Nodejs的EventLoop差异
在浏览器上, 我们众所周知的是: JS是一个单线程的语言, 也就意味着它无法通过语言本身的语法, 语句来创造出另一个线程, 以使其来进行一些异步的工作.
但是, 单线程的理念与异步又是互斥的, 因为异步意味着我的异步任务一定是在线程之外去执行的, 当执行完毕之后, 才会将结果"插"回JS的执行线程上.
基本的理念如此, 那么在执行细节上呢?
1. 浏览器
在浏览器上, 我们知道浏览器是将绘制与逻辑执行放到了不同的线程
在UI线程上, 使用CSSOM + DOM来组合生成UI.
在JS执行线程上, 则进行JS逻辑脚本的执行.
同时, 二者相互阻塞, JS阻塞时, UI绘制会暂停执行. UI绘制阻塞时, JS逻辑会暂停执行.
以上其实是相对通识的内容, 下面便是这次需要重点讲的点: Eventloop线程
如上面所说, 对于异步的任务, 其实浏览器是额外开辟了一个线程来处理的, 在这个线程上处理异步任务, 然后将其传回到JS的执行线程上
其实除此之外, 网络IO也会开启一个网络线程, 这个不在本次的讨论范围之内
这时候, 宏任务, 微任务就能很好解释其行为了.
- 每一次的EventLoop, 都会创建一个微任务队列, 和宏任务队列
- 每次循环会执行微任务队列中的回调, 再执行宏任务.
其中有一个特殊情况,
requestAnimationFrame
看似是一个微任务, 但是其实这个回调是执行在渲染阶段, 并非Eventloop中2. Nodejs
Nodejs上没有UI线程, 虽然也用到了V8引擎, 但是本质的实现方式还是有很大区别的
在Nodejs上, EventLoop是分阶段的
- timers执行setTimeout和setInterval的回调
- pendding callbacks执行延迟到下一个循环迭代的IO回调
- idle, prepare仅系统内部使用
- pool 用来检查新的IO时间, 执行与IO相关的回调. 事实上除了其他几个阶段处理的事情, 其他几乎所有异步都在这里处理
- check setImmediate在这里执行
- 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则使用了分层的机制来处理异步任务, 主要的几个层的情况如下:
- process.nextTick 不属于任何层, 一定最先执行
- timers (setInterva setTimeout)
- poll 几乎所有的异步都在这个阶段处理
- check setImmediate执行
Vue.nextTick相比于process.nextTick来说, 虽然同名为nextTick, Vue.nextTick是一个代码实现, 支持优雅降级的异步API, 而process.nextTick则是Nodejs的原生API