事件循环
事件循环被称作循环的原因在于,它一直在查找新的事件并且执行。一次循环的执行称之为 tick, 在这个循环里执行的代码称作 task
while (eventLoop.waitForTask()) {
eventLoop.processNextTask()
}
任务(Tasks)中同步执行的代码可能会在循环中生成新的任务。一个简单的生成新任务的编程方式就是 setTimtout(taskFn, deley)
,当然任务也可以从其他的资源产生,比如用户的事件、网络事件或者DOM的绘制。
任务队列
让事情变得复杂的情况是,事件循环可能有几种任务任务队列。唯一的两个限制是同一个任务源中的事件必须属于同一个队列,并且必须在每个队列中按插入顺序处理任务。除了这些之外,执行环境可以自由地做它所做的事情。例如,它可以决定下一步要处理哪些任务队列。
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
}
基于这个模型,我们失去了对事件执行时间的控制权。浏览器可能决定在执行我们设定的setTimeout
之前先清空其他几个队列.
Microtask queue
幸运的是,事件循环也有一个单独的队列叫做 microtask,microtask 将会在百分百在当前task队列执行完毕以后执行
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask()
}
}
最简单的方式生成一个 microtask 任务是 Promise.resolve().then(microtaskFn)
, Microtasks 的插入执行是按照顺序的,而且因为只有一个唯一的 microtask 队列。执行环境不会再搞错执行的时间了。
另外,microtask任务 也可以生成新的 microtask任务 并且插入到同样的队列中(插入当前microtask)并且在同一个 tick 里执行
渲染
最后一个是关于渲染的任务,不同于其他的任务处理,渲染任务并不是被独立的后台任务处理。它可能会是一个独立运行在每一个tick结束后的算法。执行环境拥有较大的选择空间,它可能会在每一个任务队列后执行渲染,也可能执行多个任务队列而不渲染。
幸运的是这里有一个 requestAnimationFrame(handle)
函数,它会正确的在下一次渲染时执行内置的函数
最后这就是我们整个的渲染模型
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask()
}
if (shouldRender()) {
applyScrollResizeAndCSS()
runAnimationFrames()
render()
}
}
以上内容翻译自writing-a-javascript-framework-execution-timing-beyond-settimeout
思考
以上就是对整个event loop的翻译与解释,文章解释比较简洁明细,但是相信大部分同学可能还是不太明白,那么我们换个思路,如果面试官问什么是event loop,面试官是想知道些什么?我应该怎么回答?
event loop顾名思义就是事件循环,为什么要有事件循环呢?因为V8是单线程的,即同一时间只能干一件事情,但是呢文件的读取,网络的IO处理是很缓慢的,并且是不确定的,如果同步等待它们响应,那么用户就起飞了。于是我们就把这个事件加入到一个 事件队列里(task),等到事件完成时,event loop再执行一个事件队列。
值得注意的是,每一种异步事件加入的 事件队列是不一样的。唯一的两个限制是同一个任务源中的事件必须属于同一个队列,并且必须在每个队列中按插入顺序处理任务。 也就是说由系统提供的执行task的方法,如 setTimeout setInterval setimmediate 会在一个task,网络IO会在一个task,用户的事件会在一个task。event-loop将会按照以下顺序执行
update_time
在事件循环的开头,这一步的作用实际上是为了获取一下系统时间,以保证之后的timer有个计时的标准。这个动作会在每次事件循环的时候都发生,确保了之后timer触发的准确性。(其实也不太准确....)timers
事件循环跑到这个阶段的时候,要检查是否有到期的timer,其实也就是setTimeout和setInterval这种类型的timer,到期了,就会执行他们的回调。I/O callbacks
处理异步事件的回调,比如网络I/O,比如文件读取I/O。当这些I/O动作都结束的时候,在这个阶段会触发它们的回调。idle, prepare
这个阶段内部做一些动作,与理解事件循环没啥关系I/O poll阶段
这个阶段相当有意思,也是事件循环设计的一个有趣的点。这个阶段是选择运行的。选择运行的意思就是不一定会运行。check
执行setImmediate操作close callbacks
关闭I/O的动作,比如文件描述符的关闭,链接断开,等等等
(以上参考自方正——Node.js源码解析:深入Libuv理解事件循环)
除了task还有一个microtask,这一个概念是ES6提出Promise以后出现的。这个microtask queue只有一个。并且会在且一定会在每一个task后执行,且执行是按顺序的。加入到microtask 的事件类型有Promise.resolve().then(), process.nextTick() 值得注意的是,event loop一定会在执行完micrtask以后才会寻找新的 可执行的task队列。而microtask事件内部又可以产生新的microtask事件比如
(function microtask() {
process.nextTick(() => microtask())
})()
这样就会不断的在microtask queue添加事件,导致整个eventloop堵塞
最后就是一个渲染的事件队列,这个队列只出现在浏览器上,并且执行环境会根据情况决定执行与否(可能执行很多task queue也不执行渲染队列)。它如果执行则一定会在microtask后执行,通过requestAnimationFrame(handle)
方法,能够保证中间的代码一定能在下一次执行渲染函数前执行
补充常见的产生microtask和task事件的方法
microtasks:
- process.nextTick
- promise
- Object.observe
- MutationObserver
tasks:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI渲染
Tips
- 我们通过node运行一个js文件,如果没有可执行事件的事件队列,进程就会退出,那么怎么不让它退出呢?
setInterval方法,这货会一直循环建立新的事件,这样能够保证node进程不退出
监听 beforeExit 事件,通过process.on('beforeExit', handle) 这个事件在node进程退出前会触发,但是如果这里面的handle包含了一个可以生成异步事件的操作,则node进程也不会退出。手动触发process.exit(EXIT_CODE)不会触发该事件
- setInterval会导致node进程不能正常退出,但是如果希望即使有setInterval也能正常退出怎么办(有一些循环并不希望挂起node进程)?
const timer = process.setInterval(handle, deley) 调用setInterval方法会返回一个timer,调用 timer.unref() 则event-loop判断除它以外,没有可进行的事件队列后也会推出
- process.on('exit', handle)中,handle里的异步事件不能执行
exit事件在手动执行process.exit(EXIT_CODE)后,或者event loop中没有可执行的事件队列 时触发。触发 exit 事件后,执行环境就不会再生成新的 事件队列了,因此这里面的异步事件都会被强制队列
最后
以上都是我瞎编的
如果你喜欢我瞎编的文章,欢迎star Github