一、为什么会有Event Loop
JavaScript 的任务分为同步和异步两种,它们的处理方式也各自不同,同步任务
是直接放在主线程上排队一次执行,异步任务
会放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到调用栈
,然后主线程执行调用栈的任务。
调用栈:调用栈是一个栈结构,函数调用会形成一个栈帧,帧中包含了当前执行函数的参数和局部变量等上下文信息,函数执行完后,它的执行上下文会从栈中弹出。
JavaScript 是单线程的,单线程是指在 js 引擎中解析和执行 js 代码的线程只有一个(主线程),每次只能做一件事情,然而 ajax 请求中,主线程在等待响应的过程中会去做其他事情,浏览器先在事件表注册 ajax 的回调函数,响应回来后回调函数会被添加到任务队列中等待执行,不会造成线程阻塞,所以说 js 处理 ajax 请求的方式是异步的。
综上所述,检查调用栈是否为空以及将某个任务添加到调用栈中的过程就是 event loop,这就是 JavaScript 实现异步的核心。
二、浏览器中的 Event Loop
Micro-Task 与 Macro-Task
浏览器端事件循环中的异步队列有两种:macro(宏任务)和 micro(微任务)队列。
常见的 macro-task:setTimeout
、setInterval
、script(整体代码)
、I/O 操作
、UI 渲染
等。
常见的 micro-task:new Promise().then(回调)
、MutationObserve
等。
requestAnimationFrame
requestAnimationFrame 也属于异步执行的方法,但该方法既不属于宏任务,也不属于微任务。按照 MDN 中的定义:
window.热questAnimationFrame() 告诉浏览器,你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
requestAnimationFrame
是 GUI 渲染之前执行,但在 Micro-Task
之后,不过 requestAnimationFrame
不一定会在当前帧必须执行,由浏览器根据当前的策略自行决定在那一帧执行。
event loop 过程
- 检查 macro-task 队列是否为空,非空则到2,为空则到3
- 执行 macro-task 中的一个任务。
- 继续检查 micro-task 队列是否为空,若有则到4,否则到5。
- 取出 micro-task 中的任务执行,执行完成返回到步骤3.
- 执行视图更新。
当某个宏任务执行后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。
三、node 中的 Event Loop
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js 采用 V8 作为 js 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv,libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是它里面的实现。
根据上图 node 的运行机制如下:
- V8 引擎解析 JavaScript 脚本。
- 解析后的代码,调用 Node API。
- libuv 库负责 Node API 的执行,它将不同的任务分配给不同线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
- V8 引擎再将结果返回给用户。
六大阶段
其中 libuv 引擎中的事件循环分为 6个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量达到系统设定的阀值,就会进入下一阶段。
timers
阶段:这个阶段执行 timer(setTimeout、setInterval)的回调,并且是由 poll 阶段控制的。I/O callbacks
阶段:处理一些上一轮循环中的少数未执行的 I/O 回调。idle、prepare
阶段:仅 node 内部使用。poll
阶段:获取新的 I/O 事件,适当的条件下 node 将阻塞在这里。check
阶段:执行 setImmediate()的回调。close callbacks
阶段:执行 socket 的 close 事件回调。
NodeJs 中宏任务主要有4个
- Timers Queue
- IO Callbacks Queue
- Check Queue
- Close Callbacks Queue
这4个都属于宏队列,但是在浏览器中,可以认为只有一个宏队列,所有的 macro-task 都会被加到这一个宏队列中,但是在 NodeJs 中,不同的 macro-task 会被放置在不同的宏队列中。
NodeJs 中微任务主要有2个
- Next Tick Queue:是放置 process.nextTick(callback) 的回调任务的。
- Other Micro Queue:放置其他的 micro-task,比如 Promise 等。
在浏览器中,也可以认为只有一个微队列,所有的 micro-task 都会被加到这一个微队列中,但是在 NodeJs 中,不同的 micro-task 会被放置在不同的微任务队列中。
NodeJs 宗的 EventLoop 过程
- 执行全局 Script 的同步代码。
- 执行 micro-task 微任务,先执行所有 Next Tick Queue 中的所有任务,再执行 Other Micro-task Queue 中的所有任务。
- 开始执行 macro-task 宏任务,共6个阶段,从第一个阶段开始执行相应每一个阶段 macro-task 中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的 Event Loop 中是只取宏队列的第一个任务出来执行,每一个阶段的 macro-task 任务执行完毕后,开始执行微任务,也就是步骤2。
- Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Timers Queue ……
- 这就是 Node 的 Event Loop。
Node 11.x 新变化
现在 node11 在 timer 阶段的 setTimeout、setInterval…和在 check 阶段的 immediate 都在 node11 里面都修改为一旦执行一个阶段里的一个任务就立刻执行微任务队列。一切都是为了和浏览器更加趋同。