产生背景
javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。
单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。
而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。
单线程是必要的,也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。
前面提到javascript的另一个特点是“非阻塞”,那么javascript引擎到底是如何实现的这一点呢?答案就是今天这篇文章的主角——event loop(事件循环)。
由于浏览器和nodejs在实现eventloop时机制有不同之处,所以将分开讲述这两种实现方法
浏览器的Eventloop
Eventloop事件循环中的执行栈
、事件队列
、宏任务
、微任务
等概念需要首先理清
什么是执行栈
执行栈就是js代码运行的地方,上图call stack所示。
什么是事件队列
当浏览器中的事件监听函数被触发(DOM)、网络请求的相应(ajax)、定时器被触发(setTimeout)相对应的回调函数就会被推送到事件队列中,等待执行;如上图中的Callback Queue。
什么是宏队列
script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
什么是微任务
process.nextTick(Nodejs), Promises, Object.observe, MutationObserver;
事件循环(event-loop)是什么?
宏队列与微任务的执行顺序![宏队列与微任务的执行顺序](https://i-blog.csdnimg.cn/blog_migrate/5c3001004dafda2f48ca6a6b21abbe57.png)
具体来说,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行,以此类推。
例子说明一切
首先,全局代码(main())压入调用栈执行,打印start
;
接下来setTimeout压入macrotask队列,promise.then回调放入microtask队列,最后执行console.log(‘end’),打印出end
;
至此,调用栈中的代码被执行完成,回顾macrotask的定义,我们知道全局代码属于macrotask,macrotask执行完,那接下来就是执行microtask队列的任务了,执行promise回调打印promise1
;
promise回调函数默认返回undefined,promise状态变为fullfill触发接下来的then回调,继续压入microtask队列,event loop会把当前的microtask队列一直执行完,此时执行第二个promise.then回调打印出promise2
;
这时microtask队列已经为空,从上面的流程图可以知道,接下来主线程会去做一些UI渲染工作(不一定会做),然后开始下一轮event loop,执行setTimeout的回调,打印出setTimeout
;
这个过程会不断重复,也就是所谓的事件循环。
async/await
async 做一件什么事情?
带 async 关键字的函数,它使得你的函数的返回值必定是 promise 对象
也就是
如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装
如果async关键字函数显式地返回promise,那就以你返回的promise为准
await 在等什么?
await等的是右侧「表达式」的结果
也就是说,
右侧如果是函数,那么函数的return值就是「表达式的结果」
右侧如果是一个 ‘hello’ 或者什么值,那表达式的结果就是 ‘hello’
async function async1() {
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
async function async2() {
console.log( 'async2' )
}
async1()
console.log( 'script start' )
这里注意一点,可能大家都知道await会让出线程,阻塞后面的代码,那么上面例子中, ‘async2’ 和 ‘script start’ 谁先打印呢?
是从左向右执行,一旦碰到await直接跳出, 阻塞async2()的执行?
还是从右向左,先执行async2后,发现有await关键字,于是让出线程,阻塞代码呢?
实践的结论是,从右向左的。先打印async2,后打印的script start
await 等到之后,做了一件什么事情?
那么右侧表达式的结果,就是await要等的东西。
等到之后,对于await来说,分2个情况
- 不是promise对象
- 是promise对象
如果不是 promise , await会阻塞后面的代码,先执行async外面的同步代码,同步代码执行完,再回到async内部,把这个非promise的东西,作为 await表达式的结果
如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
注意:
在async/await的处理上谷歌70和73实现是不同,具体可以看这篇文章
Node JS 的Eventloop实现
- timers 阶段:这个阶段执行timer(
setTimeout
、setInterval
)的回调 - I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
- idle, prepare 阶段:仅node内部使用
- poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
- check 阶段:执行
setImmediate()
的回调 - close callbacks 阶段:执行
socket
的close
事件回调
timers 阶段
timers 是事件循环的第一个阶段,Node 会去检查有无已过期的timer,如果有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout()
和 setImmediate()
的执行顺序是不确定的。
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
执行的结果是这样子的:
首先进入的是timers
阶段,如果我们的机器性能一般,那么进入timers
阶段,一毫秒已经过去了(setTimeout(fn, 0)
等价于setTimeout(fn, 1)
),那么setTimeout
的回调会首先执行。
如果没有到一毫秒,那么在timers
阶段的时候,下限时间没到,setTimeout
回调不执行,事件循环来到了poll
阶段,这个时候队列为空,此时有代码被setImmediate()
,于是先执行了setImmediate()
的回调函数,之后在下一个事件循环再执行setTimemout
的回调函数。
而我们在执行代码的时候,进入timers
的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。
再对比一段代码:
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
这里我们就会发现,setImmediate
永远先于setTimeout
执行。
原因如下:
fs.readFile
的回调是在poll
阶段执行的,当其回调执行完毕之后,poll
队列为空,而setTimeout
入了timers
的队列,此时有代码被setImmediate()
,于是事件循环先进入check
阶段执行回调,之后在下一个事件循环再在timers
阶段中执行有效回调。
poll 阶段
poll 阶段主要有2个功能:
- 处理 poll 队列的事件
- 当有已超时的 timer,执行它的回调函数
even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate()
,分两种情况:
- 若有预设的
setImmediate()
, event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列 - 若没有预设的
setImmediate()
,event loop将阻塞在该阶段等待
注意一个细节,没有setImmediate()
会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段。
check 阶段
setImmediate()
的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。
Nodejs的Eventloop小结
- event loop 的每个阶段都有一个任务队列
- 当 event loop 到达某个阶段时,将执行该阶段的任务队列,直到队列清空或执行的回调达到系统上限后,才会转入下一个阶段
- 当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick
一个例子
首先进入timers阶段,执行timer1的回调函数,打印timer1
,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2
;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask
队列的所有任务,依次打印promise1
、promise2
。