Event Loop事件循环全解析

青山隐隐水迢迢,秋尽江南草未凋。 --寄扬州韩绰判官

前言

JavaScript 异步编程的前提是深刻了解事件循环,

都已经知道了JS引擎是单线程,如果不是单线程的,那么就在同一时间可以做多件事情,听起来很美好是不是,但是,这对面向前端的语言是危险的,比如,同时好几个线程同时操作一个 DOM 对象,该以谁的为准呢?等等会造成很多的问题。所以 JS 设计成单线程的作用就是简单化编程。

任务队列

当我们调用一个方法的时候,JavaScript 会生成一个与这个方法对应的执行环境,又叫执行上下文(context)。这个执行环境中保存着该方法的私有作用域、上层作用域(作用域链)、方法的参数等等;同步任务都在主线程上执行,形成一个执行栈,对于异步代码。主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

下图是事件循环示意图:

对于上图,我们可以认为同步代码块执行都位于左侧的同步执行栈,而所有的异步代码都位于右侧的任务队列,这里存放着所有的异步程序。JS 引擎会先执行同步代码块,当同步代码块执行完毕后,再去右侧异步代码存放区域取出异步代码执行。然后循环往复。

上图大致描述就是:

●主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)

●而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调

微任务和宏任务

先记住一个前提,宏任务和微任务是对应异步代码来说的,同步代码是没这么多事情的,都是乖乖的按照执行栈执行的。

首先看一下常见的微任务和宏任务

宏任务:script(整体代码)、setTimeout、setInterval、I/O、事件、postMessage、 MessageChannel、setImmediate (Node.js)

微任务:Promise.then、Promise.catch、Promise.final、 MutaionObserver、process.nextTick (Node.js)

同时存在微任务和宏任务的时候,微任务是先于宏任务执行的,所以,执行顺序是:同步代码 > 微任务 >

宏任务

EventLoop 和 Async/Await

async/await是 ES7 新增的 API,是生成器的语法糖,await会暂停代码的执行;

await 等待的是一个表达式(主要是等待一个 async 函数的返回值),这个表达式的计算结果是 Promise 对象(也就是异步方法)或者其它值,如字符串、数字等(换句话说,就是没有特殊限定)。所以,await 后面实际是可以接普通函数调用或者直接量的。

所以这可以当成 Promise 来看:

比如这段代码:

kotlin 复制代码
async function foo() {
  // await 前面的代码
  await fun();
  // await 后面的代码
}

async function fun() {
  //  代码执行
}

foo();

这段代码,await前边是同步代码, 这句可以被转换成 Promise.resolve(bar());await后边是异步代码,会转换成 Promise ,await后面代码就是会在 Promise 的then 方法中执行的代码块,所以,await后面的代码属于微任务。

我们将上边的代码转换成 Promise 语法的代码。

kotlin 复制代码
function foo() {
  // await 前面的代码
  Promise.resolve(fun()).then(() => {
    // await 后面的代码
  });
}

function fun() {
  //  代码执行
}

foo();

定时器

事件循环机制的核心是:JS引擎线程和事件触发线程

当我们调用setTimeout后,并不是JS 引擎在负责定时器,而是有单独的定时器线程在负责,这就与其他的异步程序有所区分,

为什么要单独的定时器线程?因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响定时器的准确性,因此需要单独开辟一个线程来维护定时器,以保证其计时的准确性。当使用定时器时的时候,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。

譬如:

javascript 复制代码
setTimeout(function(){     console.log('hello!'); }, 1000);

这段代码的作用是当1000毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行

javascript 复制代码
setTimeout(function(){     console.log('hello!'); }, 0); console.log('begin');

这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行

注意:

  • 执行结果是:先begin后hello!
  • 虽然代码的本意是0毫秒后就推入事件队列,但是W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

(不过也有一说是不同浏览器有不同的最小时间设定)

  • 就算不等待4ms,就算假设0毫秒就推入事件队列,也会先执行begin(因为只有可执行栈内空了后才会主动读取事件队列)

浅谈 Web Workers

需要强调的是,Worker 是浏览器 (即宿主环境) 的功能,实际上和 JavaScript 语言本身几乎没有什么关系。也就是说,JavaScript 当前并没有任何支持多线程执行的功能。

所以,JavaScript 是一门单线程的语言!JavaScript 是一门单线程的语言!JavaScript 是一门单线程的语言!

浏览器可以提供多个 JavaScript 引擎实例,各自运行在自己的线程上,这样你可以在每个线程上运行不同的程序。程序中每一个这样的的独立的多线程部分被称为一个 Worker。这种类型的并行化被称为 任务并行,因为其重点在于把程序划分为多个块来并发运行。下面是 Worker 的运作流图。

Web Worker 实例

下面用一个乘法的例子浅谈 Worker 的用法。

首先新建一个 index.html ,直接上代码:

xml 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width" />

    <title>Web Workers basic example</title>

    <link rel="stylesheet" href="style.css" />
  </head>

  <body>
    <h1>Web<br />Workers<br />basic<br />example</h1>

    <div class="controls" tabindex="0">
      <form>
        <div>
          <label for="number1">Multiply number 1: </label>
          <input type="text" id="number1" value="0" />
        </div>
        <div>
          <label for="number2">Multiply number 2: </label>
          <input type="text" id="number2" value="0" />
        </div>
      </form>

      <p class="result">Result: 0</p>
    </div>
    <script src="main.js"></script>
  </body>
</html>

新建一个 main.js,内容如下:

ini 复制代码
const first = document.querySelector('#number1');
const second = document.querySelector('#number2');

const result = document.querySelector('.result');

if (window.Worker) {
  const myWorker = new Worker("worker.js");

  [first, second].forEach(input => {
    input.onchange = function() {
      myWorker.postMessage([first.value, second.value]);
      console.log('Message posted to worker');
    }
  })

  myWorker.onmessage = function(e) {
    result.textContent = e.data;
    console.log('Message received from worker');
  }
} else {
  console.log('Your browser doesn't support web workers.');
}

新建一个worker.js

ini 复制代码
onmessage = function(e) {
  console.log('Worker: Message received from main script');
  const result = e.data[0] * e.data[1];
  if (isNaN(result)) {
    postMessage('Please write two numbers');
  } else {
    const workerResult = 'Result: ' + result;
    console.log('Worker: Posting message back to main script');
    postMessage(workerResult);
  }
}

首先,我们通过const myWorker = new Worker("worker.js");引入worker,为两个输入框注册了onchange点击事件,点击事件的执行是为worker线程传入两个输入框的值,

worker线程中,onmessage被调用开始执行,计算结果,得到结果后,执行postMessage(workerResult);,向主线程发送数据,主线程通过myWorker.onmessage 接收数据,然后更新 DOM。

引用

最后一次搞懂 Event Loop

浅析JavaScript事件循环(Event Loop) - 掘金

相关推荐
m0_748244969 分钟前
【AI系统】LLVM 前端和优化层
前端·状态模式
明弟有理想9 分钟前
Chrome RCE 漏洞复现
前端·chrome·漏洞·复现
平行线也会相交9 分钟前
云图库平台(二)前端项目初始化
前端·vue.js·云图库平台
shimmer00811 分钟前
抖音小程序登录(前端通过tt.login获取code换取openId)
前端·小程序·状态模式
嘤嘤怪呆呆狗21 分钟前
【开发问题记录】使用 Docker+Jenkins+Jenkins + gitee 实现自动化部署前端项目 CI/CD(centos7为例)
前端·vue.js·ci/cd·docker·gitee·自动化·jenkins
鱼钓猫的小鱼干26 分钟前
table 表格转成 excell 导出
前端·vue·excel
一只搬砖的猹28 分钟前
cJson系列——常用cJson库函数
linux·前端·javascript·python·物联网·mysql·json
CodeClimb40 分钟前
【华为OD-E卷-租车骑绿道 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
懒羊羊我小弟1 小时前
包管理工具npm、yarn、pnpm、cnpm详解
前端·npm·node.js·yarn·cnpm
ppo_wu1 小时前
更改 pnpm 的全局存储位置
前端·vue