JS异步详解 – 浏览器/Node/事件循环/消息队列/宏任务/微任务

33次阅读

共计 3033 个字符,预计需要花费 8 分钟才能阅读完成。

js 异步历史

一个 JavaScript 引擎会常驻于内存中,它等待着我们把 JavaScript 代码或者函数传递给它执行

在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,引擎就把代码直接顺次执行了,异步任务都是宿主环境(浏览器)发起的(setTimeout、AJAX 等)。

在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了

JS 异步实现原理

js 为单线程,js 引擎中负责解析执行 js 代码的线程只有一个(主线程 ),即每次只能做一件事,其他 IO 操作放入任务队列等待执行,异步过程中, 工作线程 异步操作完成后 需要 通知主线程 。那么这个 通知机制 是利用 消息队列 事件循环(EventLoop)实际上,主线程只会做一件事情 ,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且 主线程只有在将当前的消息执行完成后,才会去取下一个消息
node:node.js 单线程只是一个 js 主线程,本质上的异步操作还是由线程池完成的,node 将所有的阻塞操作都交给了内部的线程池去实现,本身只负责不断的往返调度,并没有进行真正的 I / O 操作,从而实现异步非阻塞 I /O,这便是 node 单线程的精髓之处了。

浏览器

概念

  • 消息队列 :消息队列是一个 先进先出 的队列,它里面存放着各种消息。
  • 事件循环 :事件循环是指 主线程重复从消息队列中取消息、执行的过程。(浏览器至少有一个事件循环,一个事件循环至少有一个任务队列(macrotask))
  • 微任务:

    • JavaScript 引擎发起的任务 – JS 引擎级别
    • promise 回调,MutationObserver,process.nextTick,Object.observe
  • 宏任务

    • 宿主发起的任务,每次的一段 js 代码执行过程,其实都是一个宏观任务 – 宿主级别
    • 整体的 js 代码,事件回调,XHR 回调,定时器(setTimeout/setInterval/setImmediate),IO 操作,UI render
  • 宏任务和微任务关系:每个 macro 宏任务会维护一个 micro 微任务列表

事件循环过程

  • 首先我们分析有多少个宏任务;
  • 在每个宏任务中,分析有多少个微任务;
  • 根据调用次序,确定宏任务中的微任务执行次序;
  • 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
  • 确定整个顺序

视图渲染时机:

  • 本轮事件循环的 microtask 队列被执行完之后(不是每轮事件循环都会执行视图更新,浏览器有自己的优化策略)
  • 注意:执行任务的耗时会影响视图渲染的时机。通常浏览器以每秒 60 帧(60fps)的速率刷新页面(16.7ms 渲染一帧)所以如果要让用户觉得顺畅,单个 macrotask 及它相关的所有 microtask 最好能在 16.7ms 内完成。

Node

概念

  • 非阻塞 I/O 操作: 尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去, 当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到 轮询 队列中等待时机执行

事件循环过程

  • 过程

    • event loop 的每个阶段都有一个任务队列(一个 FIFO 队列来执行回调)
    • 当 event loop 到达某个阶段时,将执行该阶段的任务队列,直到队列清空或执行的回调达到系统上限后,才会转入下一个阶段
    • 当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick
  • 每次事件循环都包含了 6 个阶段

    • timers 阶段:这个阶段执行 timer(setTimeoutsetInterval)的回调
    • I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
    • idle, prepare 阶段:仅 node 内部使用
    • poll 阶段:获取新的 I / O 事件, 适当的条件下 node 将阻塞在这里
    • check 阶段:执行 setImmediate() 的回调
    • close callbacks 阶段:执行 socketclose 事件回调

  • timers 阶段

    Node 会去检查有无已过期的 timer,如果有则把它的回调压入 timer 的任务队列中等待执行

    技术上来说,poll 阶段控制 timers 什么时候执行。

  • poll 阶段

    • poll 阶段主要有 2 个功能:

      • 处理 poll 队列的事件
      • 当有已超时的 timer,执行它的回调函数
    • 执行过程:当 event loop 进入 poll 阶段,并且 没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:

      如果 poll 队列不空,event loop 会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;

      如果 poll 队列为空,则发生以下两件事之一:

      1. 如果代码已经被 setImmediate()设定了回调, event loop 将结束 poll 阶段进入 check 阶段来执行 check 队列(里的回调)。
      2. 如果代码没有被 setImmediate()设定回调,event loop 将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。
    • 当 event loop 进入 poll 阶段,并且 有设定的 timers,一旦 poll 队列为空(poll 阶段空闲状态):event loop 将检查 timers, 如果有 1 个或多个 timers 的下限时间已经到达,event loop 将绕回 timers 阶段,并执行 timer队列。
    • 注意:没有 setImmediate() 会导致 event loop 阻塞在 poll 阶段,这样之前设置的 timer 岂不是执行不了了?所以咧,在 poll 阶段 event loop 会有一个检查机制,检查 timer 队列是否为空,如果 timer 队列非空,event loop 就开始下一轮事件循环,即重新进入到 timer 阶段。

process.nextTick() VS setImmediate()

  • process.nextTick()

    • 在各个事件阶段之间执行,一旦执行,要直到 nextTick 队列被清空,才会进入到下一个事件阶段
    • 递归调用 process.nextTick(),会导致出现 I /O starving(饥饿)
  • setImmediate

对比

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

http://lynnelv.github.io/img/…

http://lynnelv.github.io/img/…

补充阅读

  • node 单线程底层实现机制:

https://juejin.im/post/5b61d8…

https://yq.aliyun.com/article…

https://juejin.im/post/5b1e55…

  • node setTimeOut(), setInterval(), setImmediate() 以及 process.nextTick()区别
  • js 三种定时器的区别

https://www.cnblogs.com/onepi…

正文完
 0