javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的“多线程”都是用单线程模拟出来的。但对于一些异步操作JS是如何使用Event Loop去处理他们不会导致阻塞呢,我们下面来看一下。
- Event Loop 是什么?
- Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
- 比如加载一个多媒体网页,页面骨架屏和页面元素的渲染是同步任务,图片、音乐、视频耗时比较久的是异步任务
下面来看一下js在内存中的运行方式:
如上图所示,同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。 当指定的事情完成时,Event Table会将这个函数移入Event Queue。 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
为了更好的了解这个机制,我们来进行一些小测试来试验下
测试一:
代码语言:javascript复制console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)
可能大家都知道,上面代码会先输出 1 5,但是到底是先执行setTimeout还是Promise呢,把上面代码复制到浏览器控制台中执行输出 1/5/3/4/2,说明在浏览器中,Promise执行的顺序比setTimeout高,这是为什么呢?
浏览器中的Event Loop
下面我们来看一下在浏览器中Event Loop的机制:
我们可以看到,定时器和一些异步xhr属于 Task Queue这个队列,Promise和mutaition observer 属于一个Microtask Queue这个队列,Event Loop执行一次,先检查microtask队列是否为空,不为空的话依次执行直至清空队列,然后再执行Task Queue。
经过上面的分析,我们直到Promise的优先级为什么会比timer类型的高,下面我们再来看一题
测试二:
代码语言:javascript复制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,然后执行Promise中的7,8,然后执行第一个timer接着输出2,然后执行里面的Promise执行4,5,然后执行第二个timer输出9 ,11,12,没错,正确答案就是 1,7,8,2,4,5,9,11,12
Node环境中的Event Loop
看完了浏览器中的Event Loop,下面我们来看一下node环境中的,在Node环境中运行以下代码会怎么输出呢?
测试三:
代码语言:javascript复制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)
})
其它的咱们上面已经分析过,但这次比上面多了一个 peocess,它的优先级是怎么样的呢?
我们先来看下Node环境下的Event Loop(Node中libuv模块)
上图的意思是
1. 先执行即到期的setTimeout/setInterval;
2. 再执行I/O 事件;
3. 执行setimmediates注册的函数;
4. 执行 close handlers,比如tcp连接断开等;
重要的是,在执行以上每一种事件类型之间,会先清空上图中中间的 nexy tick queue 和Micro task Queue队列,会优先优先清空next tick queue,即通过process.nextTick注册的函数
经过上面的分析,相信大家已经把测试三的答案写出来了,没错就是1、7、6、8、2、4、3、5
上面的这个gif图也直观的解释了运行机制,Poll 阶段是I/O阶段,check queue 是setImmediate
当然,这只是比较浅显的理解,具体的大家可以去看libuv的源码。
应用
其实Node的单线程非阻塞 IO 模型,就是基于这种Event Loop来实现的,具备强大的并发能力。
先来说一下为什么Node会采用单线程:因为多线程切换会有CPU消耗,将第一个线程的state写到内存里,再把要执行的线程的state加载到寄存器和缓存里,但是采用了单线程却无法进行任务的切换,I/O会使机器block,所以不能在主进程中进行I/O,要使用异步IO。
上图的例子中
收到请求1,开始处理请求 进行请求1的 IO 读取,并注册一个回调函数(处理数据并响应客户端),同时线程不阻塞,继续处理请求2 进行请求2的 IO 读取,并注册一个回调函数(处理数据并响应客户端),同时线程不阻塞,继续处理剩下的请求 请求处理结束后,依次执行 IO 读取是注册的回调函数(处理数据并响应客户端),完成处理。
Node默认4个I/O线程~