JavaScript如何实现异步编程,详细描述EventLoop机制

1,728 阅读4分钟

问题

  • 单线程如何实现异步
  • EventLoop循环的过程

单线程和异步

js的任务分为同步和异步两种,它们的处理方式也不同,

同步任务是直接在主线程上排队执行,

异步任务则会被放到任务队列中,

若有多个任务(异步任务)则要在任务队列中排队等待,

任务队列类似一个缓冲区,任务下一步会被移到调用栈(callstack),然后主线程执行调用栈的任务。

EventLoop

根据第二部分的表述可以知道 同步任务和异步任务是分开的

而事件循环就是基于之上的

调用栈的任务执行完之后会去查看任务队列是否存在任务

若是存在则推到调用栈执行

执行完之后再去查看任务队列

基于这种机制形成的循环就叫 EventLoop

宏任务和微任务

从上一部分的表述知道了事件循环的基本原理

但事实上任务队列 并不只有一个

宏任务 包括

  • 整体JS代码,
  • 事件回调,
  • XHR回调,
  • 定时器(setTimeout, setInterval, setImmediate),
  • IO操作,
  • UI render

微任务 包括

  • promise回调
  • MutationObserver
  • process.nextTick
  • Object.observe(已废弃)

其中定时器 setImmediate(某些浏览器也有,非标准 )和process.nextTick是node独有

宏任务与微任务执行的机制(浏览器端)

基于宏任务与微任务又存在一个运行机制

  1. 检查macrotask队列是否为空,非空则到2,为空则到3
  2. 执行macrotask中的一个任务
  3. 继续检查microtask队列是否为空,若有则到4,否则到5
  4. 取出microtask中的任务执行,执行完成返回到步骤3
  5. 执行视图更新

nodeJS的差异

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

看这样一端代码 按照上诉的事件循环机制在浏览器端是

timer1
promise1
timer2
promise2

事实在浏览器端确实如此,可是在node中结果却大相径庭

timer1
timer2
promise1
promise2

造成这一切的原因是什么呢?

目前发现node14以上的版本 打印结果已经与浏览器表现一致 但是未在更新说明中找到这一块的描述

事实NodeJS的事件循环分为 六个阶段

  1. timer阶段
  2. I/O callback阶段
  3. idle, prepare 阶段
  4. poll 阶段
  5. check 阶段
  6. close callbacks 阶段
node 的初始化
  1. 初始化 node 环境。
  2. 执行输入代码。
  3. 执行 process.nextTick 回调。
  4. 执行 microtasks。
进入 event-loop
  1. 进入 timers 阶段
  • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
  • 检查是否有 process.nextTick 任务,如果有,全部执行。
  • 检查是否有microtask,如果有,全部执行。
  • 退出该阶段。
  1. 进入IO callbacks阶段。
  • 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
  • 检查是否有 process.nextTick 任务,如果有,全部执行。
  • 检查是否有microtask,如果有,全部执行。
  • 退出该阶段。
  1. 进入 idle,prepare 阶段:

这两个阶段与我们编程关系不大,暂且按下不表。

  1. 进入 poll 阶段

首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。

  • 第一种情况:
  1. 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
  2. 检查是否有 process.nextTick 回调,如果有,全部执行。
  3. 检查是否有 microtaks,如果有,全部执行。
  4. 退出该阶段。
  • 第二种情况:
  1. 如果没有可用回调。

    • 检查是否有 immediate 回调,
    • 如果有,退出 poll阶段。如果没有,阻塞在此阶段,等待新的事件通知。
    • 如果不存在尚未完成的回调,退出poll阶段。
  2. 进入 check 阶段。

  • 如果有immediate回调,则执行所有immediate回调。
  • 检查是否有 process.nextTick 回调,如果有,全部执行。
  • 检查是否有 microtaks,如果有,全部执行。
  • 退出 check 阶段
  1. 进入 closing 阶段。
  • 如果有immediate回调,则执行所有immediate回调。

  • 检查是否有 process.nextTick 回调,如果有,全部执行。

  • 检查是否有 microtaks,如果有,全部执行。

  • 退出 closing 阶段

  • 检查是否有活跃的 handles(定时器、IO等事件句柄)。