浏览器中的事件循环
为了协调事件(event)、用户交互(user interaction)、脚本(script)、渲染(rendering)、网络(network)等,用户代理(user agent)必须使用事件循环(event loop)。
- 事件:PostMessage MutationObserver 等
- 用户交互:click onScroll 等
- 渲染: 解析 dom css 等
- 脚本:js执行等
Node.js中的事件循环
事件循环允许 Node.js 执行非阻塞 I/O 操作,尽管 Javascript 是单线程的,通过尽可能将操作卸载到系统内核。由于大多数现代内核都是多线程的,因此它们可以处理后台执行的多个操作。当其中一个操作完成时,内核会告诉 Node.js,以便可以将相应的回调添加到事件轮询队列中以最终执行。
- 事件:EventEmitter
- 非阻塞 I/O:网络请求、文件读写等
- 脚本:js执行等
事件循环的本质
在浏览器或者是 Node.js 环境中,runtime 对 js 脚本的调度方式叫做事件循环
浏览器中的事件循环
Javascript 为什么是单线程的?
浏览器 js 的作用是操作 DOM,这决定了他只能是单线程的,否则会带来很复杂的同步问题。
任务队列
单线程意味着所有任务需要排队,如果因为任务 cpu 计算量大还好,但是 I/O 操作 cpu 也是空闲的,所以 js 就设计成了一门一步的语言,不会做无谓的等待。任务可以分为两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个任务队列(task queue),只要异步任务有了运行结果,就在任务队列之中放置一个事件回调函数
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件回调函数。那些对应的异步任务于是就结束了等待状态,进入执行栈开始执行
- 主线程不断重复上面的第三步
主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop
宏任务与微任务
除了广义的同步任务和异步任务,Javascript 单线程中的任务可以细分为宏任务(macrotask)和微任务(microtask)。
- macrotask:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering
- microtask:process.nextTick、promise、Object.observe、MutationObserver
- 宏任务进入主线程,执行过程中会收集微任务加入微任务队列中
- 宏任务执行完成后,立马执行微任务队列中的任务。微任务执行过程中再次收集宏任务,并加入宏任务队列中
- 反复执行1、2步骤
Tip: 在浏览器中,一轮事件循环会执行一次(一个)宏任务,以及所有的微任务
Node.js 中的事件循环
当 Node.js 启动时会初始化 Event Loop,每个 Event Loop 都会包含如下 6 个循环阶段,Node.js 事件循环和浏览器的事件循环完全不一样
timers ---> I/O callbacks ---> idle,prepare ---> poll ---> check ---> close callbacks
以上 6 个阶段为一轮事件循环
阶段概览
- timers(定时器): 此阶段执行那些由 setTimeout 和 setInterval 调度的回调阶段
- I/O callbacks: 此阶段会执行几乎所有的回调函数,除了 close callbacks(关闭回调)和那些由 timers 与 setImmediate 调度的回调(setImmediate 约等于 setTimeout(cb, 0))
- idle(空转)prepare: 此阶段只在内部使用
- poll(轮询):检索新的 I/O 事件;在恰当的时候 Node 会阻塞在这个阶段
- check:setImmediate 设置的回调在此阶段执行
- close callbacks: 诸如 socket.on('close', cb) 此类的回调在此阶段被执行
在事件循环的每次运行前,Node.js 会检查它是否在等待异步 I/O 或定时器,如果没有的话就会自动关闭
如果 Event Loop 进入了 poll 阶段,且代码未包含 timer(其实就想表达在同步任务中是否包含了setTimeout),将会发生下面情况:
- 如果 poll queue 不为空,Event Loop 将同步的执行queue 里的 callback,直到 queue 为空或者执行的 callback 达到系统上限
- 如果 poll queue 为空,将会发生以下情况:
- 如果代码已经被 setImmediate 设定了 callback,Event Loop 将结束 poll 阶段进入 check 阶段,并执行 check 阶段的 queue (check 阶段的 queue 就是 setImmediate 阶段设定的 )
- 如果代码没有设定 setImmediate, Event Loop 将阻塞在该阶段等待 callbacks 加入 poll queue, 一旦到达立即执行
如果 Event Loop 进入了 poll 阶段,且代码设定了timer,将会发生下面情况:
- 如果 poll queue 进入空状态时 (即 poll 阶段为空闲状态),Event Loop 将检查 timers, 如果有1个或者多个timers 事件已经到达,Event Loop 将按循环顺序进入 timers 阶段,并执行 timer queue
面试题
在 Node.js 中 setTimeout(0, cb) === setTimeout(1, cb)
在浏览器中 setTimeout(0, cb) === setTimeout(4, cb)
setTimeout( () => {
console.log( 'timeout' );
} );
setImmediate( () => {
console.log( 'immediate' );
} );
以上代码在 Node.js 中的执行顺序不固定,主要是因为 Node.js 在启动事件循环的过程中也会消耗时间,因为 setTimeout 最少为 1ms 执行,如果到达 poll 阶段消耗时间小于 1ms,那么事件循环会执行 setImmediate,如果到达 poll 阶段时间大于 1ms,此时 tiemrs queue 里面包含回调函数,则会开启一个新的事件循环,先执行 timers