Event Loop 是什么?

Event Loop 是什么?

本文写于 2020 年 12 月 6 日

广义上来说 Event Loop 并不是 JavaScript 独有的概念,他是一个计算机的通用概念。

狭义上来说,只有 Node.js 才有 Event Loop,浏览器并没有。

一个场景引发的困惑

为什么需要 Event Loop 呢?先看一个常见的场景,如果我们同时执行了三种不同的异步事件:

setTimeout(foo, 100);
fs.readFile('./README.md', bar);
server.on('close', doSth);

我们知道在计算机中,操作系统会帮我们新建一个「进程」使应用程序可以执行,但是一个进程在一个时刻只能执行一个任务,如果我们需要同时执行这三个任务,有三个方法:

  1. 新建一个进程;
  2. 新建一个线程;
  3. 排队执行。

如果不太了解,可以看我之前的一篇文章,「进程与线程」

JavaScript 作为一门单线程的语言,肯定不能分出三条线程去执行这三条语句。那么假设计时器结束的一瞬间,三个回调函数同时触发了,Node 会怎么处理呢?

PS:Node.js 读取文件用的是 libuv,自己并不会去读写文件,所以单线程也可以异步的读写文件

我们可以推测出以下两点:

  1. Node.js 肯定会以某种顺序执行;
  2. 这种顺序应该是规定好的(优先级)。

Event Loop 的解释

回到 Event Loop 本身,我们拆开来看看。

什么叫做 Event?Event 就是事件,比如回调函数的触发就是一个事件。

什么叫做 Loop?Loop 就是循环,比如 for 循环 while 循环。

事件是有优先级的,所以处理时候是分先后的。Node.js 按照顺序去“询问”每个 Event,并且循环往复的去“询问”:

Event1 => Event2 => Event3 => Event1 => Event2 => ...

这就变成了 「poll(轮询)」

操作系统触发事件,JavaScript 处理事件,Event Loop 就是事件处理顺序管理的「解决方案」。

维基是这么解释的:Event Loop 是一个程序结构,用于等待和发送消息和事件。

简单说,就是在程序中设置两个线程:

  • 一个负责程序本身的运行,称为"主线程";
  • 另一个负责主线程与其他进程的通信,被称为 「Event Loop 线程」,也有人叫他「消息线程」。

Node 中的 Event Loop

     |-----------------------|
---->|         timer         |
|    |-----------------------|
|                |
|                |
|    |-----------------------|
|    |    close callbacks    |
|    |-----------------------|
|                |
|                |
|    |-----------------------|
|    |     idle, prepare     |
|    |-----------------------|
|                |
|                |                 |------------------|
|    |-----------------------|     |     incoming:    |
|    |          poll         |<----|    connections,  |
|    |-----------------------|     |     data, etc.   |
|                |                 |------------------|
|                |
|    |-----------------------|
|    |         check         |
|    |-----------------------|
|                |
|                |
|    |-----------------------|
-----|    close callbacks    |
     |-----------------------|

这是 Node.js 官方文档上的示意图,我们来分析一下。

  • 首先第一步,Node 会先去寻找是否存在计时器;
  • 然后在看有没有其他的 I/O 相关的回调函数;
  • idle 和 prepare 阶段根据字面意思推断,应该是清理一下,休息一下;
  • 下一步进入轮询的阶段,检查系统事件;
  • check 阶段检查 setImmediate 回调;
  • close callbacks 就是处理类似 socket 关闭的事件。

这里可以看到 check 阶段检查的是 setImmediate 回调,有的面试题目可能会问 setImmediate(fn)setTimeout(fn, 0) 谁先执行,这里我们就能回答了:不确定,因为 Event Loop 是个圈。

另外,setImmediate 不是一个标准特性,MDN 不建议我们在生产环境中使用它,浏览器只有新版的 IE 支持

在 Node 的 Event Loop 中,最常用的就是:

  • timer 检查;
  • poll 轮询;
  • check 检查。

我们先当作其他阶段都不存在了,只把这三个阶段作为循环。

因为 timer 阶段处理定时器函数回调,check 阶段处理 setImmediate 回调,所以如果这两个执行栈没有任务的话,我们就不会走这两个阶段了。

Node.js 会很智能的在其他阶段空闲时只停留在该阶段。

并且大部分时间,Node.js 都停留在 poll 阶段,文件请求、网络请求的事件处理也多半在这个阶段进行。

官方例子:

const fs = require('fs');

// 假设花费 95ms 读取文件
function someAsyncOperation() {
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms 后执行了 setTimeout 的回调函数`);
}, 100);

// 95ms 的异步操作
someAsyncOperation(() => {
  const startCallback = Date.now();
  // 10ms 的同步操作
  while (Date.now() - startCallback < 10) {
    // nothing to do
  }
});

这里我们花 95ms 读完文件后又做了 10ms 的同步操作。但问题是在 95ms 结束后 5ms 就需要执行定时器了呀,这里硬生生被 while 的同步操作卡住了 5ms,Node.js 在这个过程中是如何处理的呢?

  1. 当我们的 Event Loop 进入 poll 阶段,发现 poll 的队列是空的,因为文件没有读完。
  2. 于是 Event Loop 检查了一下最近的计时器,发现还需要 100ms,于是决定这段时间就停留在 poll 阶段。
  3. 在 poll 阶段停留了 95ms 之后,readFile 操作完成,耗时 10ms 的同步操作被系统放入 poll 队列,执行完毕之后 poll 队列为空。
  4. Event Loop 紧接着又去看了一眼计时器,发现已经超时了 5ms 了。于是由经 check 阶段,close callbacks 阶段,Event Loop 又回到了 timer 阶段执行了超时计时器的回调函数。

这里如果我们一直占着 poll 阶段做同步任务会怎么样呢?

Node.js 做了限制,会有最长占用时长的,根据操作系统而定。

Event Loop 例题

我们来做几道例题吧:

例题一

fs.readFile('xx.txt', () => {
  console.log('fs');
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
  });
});

他们的输出顺序该是如何?

因为 readFile 是读取文件,该操作既不是 timer,也不是 setImmediate,所以在 poll 阶段进行。

poll 之后是 check,check 之后才是 timer。所以先执行 setImmediate,后执行 setTimeout。

例题二

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

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

这题应该分情况讨论。

情况一:

如果是 timer 先执行,那我们必然会先执行外层的 setTimeout 的回调函数。输出完 'setTimeout' 之后,将 setImmediate 的回调放入 check 阶段——注意,这里 check 的执行栈中已经有了第一个 setImmediate 了。

执行结束后,进入 check 阶段,执行第一个 setImmediate,输出 'setImmediate',然后执行第二个 setImmediate,输出 'setImmediate in setTimeout'。再将 setTimeout 放入 timer 队列。执行结束。

进入 timer 队列,输出 'setTimeout in setImmediate'

情况二:略

例题三

console.log(1);

setTimeout(() => {
  console.log('setTimeout1');
  Promise.resolve('promise').then((res) => {
    console.log(res);
  });
}, 0);

setTimeout(() => {
  console.log('setTimeout2');
}, 0);

这里我们会发现一个问题,刚刚 Node.js 的官方图没有给出任何关于 Promise 的细节,甚至于在原文中搜索 promise 也是一无所获。

那是因为 Node.js 的 Promise 是以 nextTick 为基础实现的。

nextTick 触发的时机是在当前任务队列结束之后立即执行。这里应该先输出 'setTimeout1',再输出 'promise',最后输出 'setTimeout2'

浏览器的 Event Loop

浏览器只有宏队列和微队列,比 Node.js 简单很多。(注意,这都不是 JS 引擎提供的,而是 Node 和浏览器提供的)。

  • 宏列队:用来保存待执行的 macrotask (宏任务),比如:timer / DOM 事件监听 / ajax 回调;
  • 微列队:用来保存待执行的 microtask(微任务),比如:Promise 的回调(then 方法) / MutationObserver 的回调。

在 Promise/A+的规范中,Promise 的实现可以是微任务,也可以是宏任务。但是普遍的共识表示(至少 Chrome 是这么做的),Promise 应该是属于微任务的。

当前执行栈执行完毕时,会立刻先处理所有「微任务」队列中的事件,然后再去「宏任务」队列中取出一个事件。也就是说同一次 Event Loop 中,微任务永远在宏任务之前执行。

说到这里其实很明白了,Node.js 中的 Event Loop 有很多阶段,不停的循环;而浏览器中只有两个阶段,分别查看宏任务和微任务队列。

看个例子:

setTimeout(() => console.log(1), 0);

new Promise((resolve) => {
  console.log(2);
  resolve('promise');
}).then((res) => {
  console.log(res);
});

console.log(3);
  • 当我们执行 setTimeout 的时候,将回调函数存入宏队列;
  • 执行 new Promise 时,执行函数中的代码,将 then 任务存入微队列;
  • 执行 console.log(3),当前执行栈执行完毕;
  • 执行微队列;
  • 执行宏队列。

做道题总结一下

OK,我们再看一道题目:

const foo = async () => {
  console.log('foo');
  await bar();
  console.log('bar end');
};

const bar = async () => {
  console.log('bar');
};

foo();

new Promise((resolve) => {
  console.log('promise');
  resolve('promise down');
}).then((res) => {
  console.log(res);
});

首先第一步:因为 async await 是 Promise 的语法糖,所以需要将思维转化成 Promise,不可以真的把这段代码当成同步代码。

  • 所以先输出 'foo' 是没问题的,这相当于 new Promise(() => { console.log('foo') }) 一样;
  • 然后输出 'bar' 也是没问题的,相当于在第一次 newPromise 中又 new 了一次;
  • 接着输出 'bar end' 的语句应该被丢进微队列了;
  • 执行下方的 Promise,输出 'promise',将 then 丢进微队列;
  • 执行微队列:先输出 'bar end',后输出 'promise down'

(完)

(0)

相关推荐

  • 浏览器的 Event Loop

    前端技术优选 昨天 以下文章来源于掘金开发者社区 ,作者小蘑菇哥哥 掘金开发者社区 掘金,一个帮助开发者成长的技术社区 前端技术优选 为你精选前端领域优质技术博文,欢迎关注. 57篇原创内容 公众号 ...

  • 事件循环 EventLoop(Promise,setTimeOut,async/await执行顺序)

    什么是事件循环?想要了解什么是事件循环就要从js的工作原理开始说起: JS主要的特点就是单线程,所谓单线程就是进程中只有一个线程在运行. 为什么JS是单线程的而不是多线程的呢? JS的主要用途就是与用 ...

  • 事件循环Event loop到底是什么

    摘要:本文通过结合官方文档MDN和其他博客深入解析浏览器的事件循环机制,而NodeJS有另一套事件循环机制,不在本文讨论范围中.process.nextTick和setImmediate是NodeJS ...

  • JavaScript入门-函数function(二)

    JavaScript入门-函数function(二) 递归函数 什么是递归函数? 递归简单理解就是,在函数体里,调用自己. //我们在求一个10的阶乘的时候,可能会这么做 //写一个循环 var to ...

  • JS 运行机制

    单线程:js 是单线程的,js在同一时间只能做一件事情这就叫单线程 任务队列:异步过程与单线程本身看起来是冲突的,就是任务队列使这两个不冲突 注意:优先执行同步任务才执行异步任务 同步任务:在js同步 ...

  • Event Loop

    文章 console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console ...

  • js 彻底搞懂事件循环机制 Event Loop

    我们都知道javascript是单线程语言,就是因为单线程的特性,就不得不提js中的同步和异步 一.同步和异步 所谓单线程,无非就是同步队列和异步队列,js代码是自上向下执行的,在主线程中立即执行的就 ...

  • 【HAM说】高效率全向多波段HF天线——LOOP技术攻略

    前言 最近的几期关于端馈的分享,收获的很多朋友们的肯定.在不少群里刚刚冒泡就被冠以"大佬"的称呼,作为一个新人来说实在是愧不敢当.对于我个人而言我只是复制了国内外别人的固有成果而已 ...

  • TPAMI 2021 :基于 event stream 的步态识别,准确率高达90%!

    作者丨张贤同学 审稿丨邓富城 编辑丨极市平台 极市导读 本文介绍了作者被TPAMI接收的工作,基于 event stream 的两种不同表示形式,提出了一种新的基于 event stream 的步态识 ...

  • DEDECMS教程之loop循环标签的使用详解

    DedeCMS v5.7的loop标签非常有用,可以使用它来调用任意表中的数据,我们先来看看loop标签的语法结构. {dede:loop table='table_name' if='conditi ...

  • LIT.EVENT|世界读书日,故事从一杯咖啡开始

    文穴线上&线下双活动 4.23世界读书日 4.24文穴杭州书友面基日 活动一:4.23世界读书日知识竞赛 为了让大家的阅读生活更加有趣,文穴联合新民说推出了线上读书日知识竞赛活动,在阅读群内以 ...

  • 不熟悉新版houdini Loop循环使用方法的同学请看过来

    --  微资讯 · 微课程  -- 利用零碎时间,走上超神之路! 已经发布houdini材质翻译篇:有几位同学由于超过48小时不能回复了,打赏过的材质翻译的同学请再次回复一下,麻烦大家了~ 之前有同学 ...