事件循环小结

102 阅读5分钟

v2-33a1cfc8de65c9ba2e416a9e1f23655c_1440w.jpeg

浏览器中的事件循环

为了协调事件(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. 宏任务执行完成后,立马执行微任务队列中的任务。微任务执行过程中再次收集宏任务,并加入宏任务队列中
  3. 反复执行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 为空,将会发生以下情况:
  1. 如果代码已经被 setImmediate 设定了 callback,Event Loop 将结束 poll 阶段进入 check 阶段,并执行 check 阶段的 queue (check 阶段的 queue 就是 setImmediate 阶段设定的 )
  2. 如果代码没有设定 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

juejin.cn/post/719959…