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) - 掘金

相关推荐
崔庆才丨静觅12 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606113 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了13 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅13 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅13 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅14 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment14 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅14 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊14 小时前
jwt介绍
前端