JS

搞懂 Event Loop

2020-12-28  本文已影响0人  我是Msorry

本篇文章参考 Node.js 官方文档 javascript.info

什么是事件循环?

Event Loop 是实现异步的一种机制,由几个阶段(等待任务,执行任务等)循环组成,在 Node.js 和 Chrome 中不一样

Chrome EventLoop 两个阶段

宏任务(一会)和微任务(立刻)

按照中文语义,立刻任务立刻做,一会任务一会做,相同等级的任务按进入队列的顺序做(先进先出)

宏任务

引擎的一般算法:

  1. 当有任务时:
    从最先进入的任务开始执行。
  2. 休眠直到出现任务,然后转到第 1 步。


    image.png

微任务

ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(ES8 术语)

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。
更详细的事件循环图示如下(顺序是从上到下,即:首先是脚本,然后是微任务,渲染等):

image.png
在微任务之间没有 UI 或网络事件的处理:它们一个立即接一个地执行。所以,可以使用queueMicrotask()来在保持环境状态一致的情况下,异步地执行一个函数。

总结

  1. 从 宏任务 队列(例如 “script”)中出队(dequeue)并执行最早的任务。
  2. 执行所有 微任务:
    • 当微任务队列非空时:
      • 出队(dequeue)并执行最早的微任务。
  3. 如果有渲染,执行渲染。
  4. 如果宏任务队列为空,则休眠直到出现宏任务。
  5. 转到步骤 1。

API

setTimeout => 一会儿(宏任务)
setInterval => 一会儿(宏任务)
setImmediate => 一会儿(宏任务),优先级比 setTimeout 高
UI 渲染 => 一会儿(宏任务)
queueMicrotask() => 立刻(微任务)
.then(fn) => 立刻(微任务)
then 决定加入微任务队列,resolve 决定执行成功函数(第一个参数)不是失败函数(第二个参数)
await 转化成 Promise 再做,await下面的都属于回调函数,立刻(微任务)
new Promise(fn) 这里的fn相当于fn()

练一练

 setTimeout(function () {
     console.log(4);
 }, 0);
 new Promise(function (resolve) {
     console.log(1);
     resolve();
     console.log(2);
 }).then(function () {
     console.log(5);
 });

 console.log(3);

1 2 3 5 4

 async function async1() {
     console.log(1);
     await async2();
     console.log(2);
 }
 async function async2() {
     console.log(3)
 }

 async1();

 new Promise(function (resolve) {
     console.log(4);
     resolve();
 }).then(function () {
     console.log(5);
 });

1 3 4 2 5

Node.js EventLoop 各个阶段

因为 JavaScript 是单线程的,Event Loop 使 Node.js 能够将操作转移到系统内核中来执行非阻塞 I / O操作。
目前多数系统内核是多线程的,因此可以处理多个正在后台执行的操作。当某个操作完成,内核会告诉 Node.js,将相应的回调函数添加到轮询队列,然后执行回调函数,这个过程在 Node.js 中分为 6 个阶段。

当发送一个异步请求,请求时间为 1s 时,这段时间 JavaScript 会继续执行以下20行代码,轮询请求是否成功;当请求成功时,会把请求的内容返回给 JavaScript。由于JavaScript 是单线程的,因此肯定不是 JavaScript 做轮询,做轮询的是 C++ 。
当Node.js启动时,先初始化 Event Loop;执行 JavaScript,次过程可能调用异步API(计时器或 process.nextTick());然后处理事件循环。

下图显示了事件循环操作顺序的简化概述,每个方框是事件循环的阶段,每个阶段都有特定的操作。每个阶段都有一个任务队列,用于存放回调函数的地址;当 EventLoop 进入某个阶段时,它将执行该阶段特定的操作,然后执行该阶段任务队列中的回调函数;当任务队列用尽或回调函数全部执行完,EventLoop 将进入下一个阶段,依此类推。

  ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

阶段描述

  1. timers: 此阶段执行 setTimeout() 和安排 setInterval() 回调函数。
    timers 的时间值表示在该时间值之后可以执行回调,而不是执行回调的确切时间。操作系统或其他回调函数的会延迟 timers 的回调,poll阶段控制 timers 阶段的执行时间。
  1. I/O callbacks: 执行推迟到下一个循环迭代的I / O回调。
  2. idle,prepare: 仅在内部使用
  3. poll: 某些时候 Node.js 将在此阶段阻塞,为了防止 poll 阶段占用太多时间,libuv(实现Node.js Event Loop 和所有异步操作的C库)对该阶段设定最大停留时间(具体时间取决于系统),EventLoop 大多数都是处在poll阶段。

如果 poll 队列不是空的,Event Loop 会依次执行队列里的回调函数,直到队列清空或到达 poll 阶段的时间上限(最大停留时间)。
如果 pull 队列为空,EventLoop 会检查计时器的时间是否到期,到期后 Eventloop 回到 timers 阶段,执行回调。

  1. check: 执行setImmediate()
    setImmediate()实际上是一个特殊的计时器,poll 队列空了,Event Loop 会结束 poll 阶段进入 check 阶段。
  2. close callbacks: 一些关闭回调,例如 socket.on('close', ...)
    如果一个 socket 或者 handle 突然关闭(例如 socket.destroy()),那么会进入这个阶段。否则将发出 process.nextTick()

Node.js API

setTimeout()setImmediate()

setTimeout()在 timers 阶段执行;setImmediate() 在 check 阶段执行

如果两者都是从主模块中调用的,则时序将受到进程性能的限制

setTimeout(() => {
  console.log('fn1');
}, 0);
setImmediate(() => {
  console.log('fn2');
});
setImmediate(() => {
  console.log('fn2');
});
setTimeout(() => {
  console.log('fn1');
}, 0);

执行时序不确定

因为初始化 Event Loop 是启动一个进程,需要一段时间;执行JavaScript 需要启动 V8 引擎,也需要时间,这两个进程的快慢是不确定的。
如果启动 Event Loop 很快,执行 JavaScript 时在 poll 阶段,这样先执行console.log('fn2'),再执行 console.log('fn1')
如果启动 Event Loop 很慢,JavaScript 先执行,这样会先把 console.log('fn1') 加入到timers 队列,再进入 poll 阶段,先执行console.log('fn1') 再执行 console.log('fn2')

process.nextTick()

在当前操作完成后执行,而不管 Eventloop 的当前阶段如何

作用

  1. 允许用户处理错误,清除所有不必要的资源,或者在事件循环继续之前重新尝试请求。

  2. 有时,有必要让回调在调用栈解开之后但事件循环继续之前运行

一个例子是匹配用户的期望。简单的例子:

const server = net.createServer();
server.on('connection', (conn) => { });

server.listen(8080);
server.on('listening', () => { });

练一练

setTimeout(() => {
  setTimeout(fn1,0)
  setImmediate(fn2)
},1000)

fn2先执行,然后fn1执行

setTimeout(() => {
  setTimeout(fn1,0)
  setImmediate(fn2)
  process.nextTick(fn3)
},1000)

fn3,fn2,fn1

setTimeout(() => {
  setTimeout(()=>{
    console.log('fn1')
    process.nextTick(()=>{console.log('fn4')})
},20)
  setImmediate(()=>{console.log('fn2')})
  process.nextTick(()=>{console.log('fn3')})
},1000)

fn3,fn2,fn1,fn4

setTimeout(() => {
  setTimeout(()=>{
        console.log('fn1')
        process.nextTick(()=>{console.log('fn4')})
},20)
  setImmediate(()=>{
        console.log('fn2')
        process.nextTick(()=>{console.log('fn5')})
})
  process.nextTick(()=>{console.log('fn3')})
},1000)

fn3 fn2 fn5 fn1 fn4

setTimeout(() => {
 setImmediate(() => {
  console.log('1');
  setTimeout(() => {console.log('2'),0})
});
setTimeout(() => {
  console.log('3');
  setImmediate(() => {console.log('4')})
});
},1000)

1 3 4 2或 1 3 2 4

setTimeout(() => {
 setImmediate(() => {
  console.log('1');
  setTimeout(() => {console.log('2'),0})
});
setTimeout(() => {
  console.log('3');
  process.nextTick(()=>{console.log('5')})
  setImmediate(() => {console.log('4')})
});
},1000)

1 3 5 4 2 或 1 3 5 2 4

setTimeout(() => {
 setImmediate(() => {
  console.log('1');
  process.nextTick(()=>{console.log('5')});
  setTimeout(() => {console.log('2'),0})
});
setTimeout(() => {
  console.log('3');
  setImmediate(() => {console.log('4')})
});
},1000)

1 5 3 4 2 或 1 5 3 2 4

上一篇下一篇

猜你喜欢

热点阅读