在介绍 Event Loop之前,首先看下 JS引擎 和 渲染引擎。
JS引擎
- parser:分析器。负责把JavaScript源码转成AST(抽象语法树)
- interpreter:解释器。负责把AST转成字节码,并解释执行。
- JIT compiler:对执行时的热点函数进行编译,将字节码转成机器码,之后可以直接执行机器码,提高效率。
- gc:垃圾回收器,清理堆内存中不再使用的对象。
渲染引擎
每一次渲染流程叫做一帧,浏览器会有一个帧率(比如一秒60帧)来刷新。
Event Loop
首先我们应该知道,一般是在主线程中更新UI。
为什么要在主线程,或者为什么不能在子线程中更新UI?
因为在多个线程中操作一个UI时,很容易导致反向加锁和死锁的问题。
同样的道理在几乎所有编程领域中都是这样的,这背后是线程同步的开销问题。
通俗的讲,两个线程不能同时draw,否则屏幕会花;不能同时insert map,否则内存会花;
所以需要互斥,比如锁。结果就是同一时刻只有一个线程可以做UI。
顺着这个思路,我们联想下客户端UI架构和微信小程序的双线程架构,应该立马秒懂。
在主进程完成UI的更新、事件的绑定,其他逻辑可以放到别的线程中完成。
然后完成以后在消息队列中放一个消息,主线程不断循环地取消息来执行。
JavaScript不同于以上的多线程架构,它是单线程的,为何呢?
因为JS最开始只是被设计用来做表单处理,不会有特别大的计算量,故没采用多线程架构,
而是在一个线程内进行DOM操作和逻辑计算,这导致了渲染引擎和JS引擎执行时相互阻塞。
JS引擎只知道执行JS,渲染引擎只知道渲染,
它们两个并不知道彼此,而Event Loop就是连接的桥梁。
JS引擎并不提供Event Loop(可能很多同学以为Event Loop是JS引擎提供的,其实不是),
它是宿主环境为了集合渲染引擎和JS引擎执行,为了处理JS执行时的高优先级任务而设计的机制。
宿主环境有浏览器、Node、跨端引擎等,不同环境有一些区别。这里我们只关心浏览器的Event Loop。
浏览器每执行一个JS任务就是一个Event Loop,
每个Loop结束后都会检查下是否需要渲染,是否需要处理Worker的消息。
这样,就解决了渲染、JS执行、Worker这三者的调度问题。
异步I/O 与 非阻塞I/O
从实际效果而言,异步和非阻塞都达到了我们并行I/O的目的,
但是从计算机内核I/O而言,异步/同步和阻塞/非阻塞实际上是两回事。
阻塞、非阻塞说的是调用者,同步、异步说的是被调用者。
有人认为阻塞和同步是一回事儿,非阻塞和异步是一回事。但这是不对的。
先来看同步场景中是如何包含阻塞和非阻塞情况的。
- 我们是用传统的水壶烧水,在水烧开之前我们一直坐在水壶前面,等着水开。这就是阻塞的。
- 我们是用传统的水壶烧水,在水烧开之前我们先去客厅看电视了,但是水壶不会主动通知我们,需要我们时不时的去厨房看一下水有没有开。这就是非阻塞的。
再来看异步场景中是如何包含阻塞和非阻塞情况的。
- 我们是用带提醒功能的水壶烧水,在水烧开发出提醒之前我们一直坐在水壶前面,等着水开。这就是阻塞的。
- 我们是用带提醒功能的水壶烧水,在水烧开发出提醒之前我们先去客厅看电视了,等水壶发出声音提醒我们。这就是非阻塞的。
任何技术都并非完美的。
阻塞I/O造成CPU等待浪费,非阻塞带来的麻烦却是需要轮询去确认是否完全完成数据获取,它会让CPU处理状态判断,是对CPU资源的浪费。
这里我们且看轮询技术是如何演进的,以减小I/O状态判断的CPU损耗。
- read:它是最原始、性能最低的一种,通过重复调用来检查I/O的状态来完成完整数据的读取。在得到最终数据前,CPU一直耗用在等待上。
- select:它是在read的基础上改进的一种方案,通过对文件描述符上的事件状态来进行判断。select轮询具有一个较弱的限制,那就是由于它采用一个1024长度的数组来存储状态,所以它最多可以同时检查1024个文件描述符。
- poll:该方案较select有所改进,采用链表的方式避免数组长度的限制,其次它能避免不需要的检查。但是当文件描述符较多的时候,它的性能还是十分低下的。
- epoll:该方案是Linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒。它是真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。
- kqueue:该方案的实现方式与epoll类似,不过它仅在FreeBSD系统下存在
宏任务 与 微任务
在异步模式下,创建异步任务主要分为 宏任务 与 微任务 两种。
ES6 规范中,
宏任务(macrotask)称为 Task,由宿主(浏览器、Node)发起;
微任务(microtask)称为 Jobs,由 JavaScript 自身发起。
宏任务(macrotask) | 微任务(microtask) | | -------------- || ---------------------------- | | setTimeout | Promise.[then/catch/finally] | | setInterval | process.nextTick(Node环境) | | MessageChannel | MutationObserver(浏览器环境) | | I/O,事件队列 | queueMicrotask |
一次Loop的执行顺序:
graph TD
执行宏任务 --> 执行所有微任务 --> check_渲染线程 --> check_worker线程
代码测试
测试1
先来看一段代码,打印的结果会是?
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)
这个非常简单,正确结果是:1、5、3、4、2。
测试2
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
setTimeout(() => {
console.log(9)
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
分析步骤:
- 同步代码首先输出:1、7
- 接着,清空microtask队列:8
- 第一个task执行:2、4
- 接着,清空microtask队列:5
- 第二个task执行:9、11
- 接着,清空microtask队列:12
测试3
加入 process.nextTick 后结果又是怎样?
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
process.nextTick(() => {
console.log(6)
})
注意:process.nextTick 的优先级高于 Promise
正确结果:1、7、6、8、2、4、3、5
测试4
setTimeout的最低延时是多少?
setTimeout(() => {
console.log(2)
}, 2)
setTimeout(() => {
console.log(1)
}, 1)
setTimeout(() => {
console.log(0)
}, 0)
因为 MDN的setTimeout文档 写到最低延时为4ms,那么它应该输出:2、1、0。
但是在实测的过程中,不管是浏览器还是Node,运行结果都是1、0、2。
也就是说1ms和0ms的延时效果是一致的,那么会不会最低延时为1ms,而不是4ms了。
Node源码:github.com/nodejs/node…
function createSingleTimeout(callback, after, args) {
after *= 1; // coalesce to number or NaN
if (!(after >= 1 && after <= TIMEOUT_MAX))
after = 1; // schedule on next tick, follows browser behavior
...
}
代码中的注释直接说明了,设置最低1ms的行为是为了向浏览器行为看齐。
测试5
setImmediate 与 setTimeout 哪个先执行?
setImmediate(function () {
console.log(1);
}, 0);
setTimeout(function () {
console.log(2);
}, 0);
new Promise(function (resolve) {
console.log(3);
resolve();
console.log(4);
}).then(function () {
console.log(5);
});
console.log(6);
process.nextTick(function () {
console.log(7);
});
console.log(8);
正确结果:3、4、6、8、7、5、2、1