JavaScript 异步编程和事件循环详解

JavaScript 异步编程和事件循环详解

⏲️建议阅读时间: 15min

1. 异步编程的意义

什么是异步编程,解决了什么问题

异步编程是什么呢?

简单说,异步编程就是不需要等待一个任务执行完毕,就可以继续执行其他任务的编程方式。

🌰举个生活中的例子,平时我们做饭需要按顺序一步一步来,必须等一样做完才能做下一样。但是如果让好几个人一起做饭,他们就可以异步进行,同时做多件事,效率更高。(但是也不是所有的任务都可以异步,比如生孩子)

编程也是一样,默认是同步执行,只有前一个任务做完,才能执行后一个任务。异步编程允许我们在等待一个任务完成的同时,继续执行其他任务。

异步编程解决了什么问题呢?

主要解决了程序中一些不必要的等待时间问题。比如程序中有某些任务需要等待网络请求,文件读取,用户输入等情况。这种需要等待的时间对程序来说是"空转"时间。

📖浏览器和 Node.js 中的异步应用场景

在浏览器和 Node.js 中,异步编程很常见,主要应用在以下场景:

  • 网络请求。比如 Ajax 请求,需要等待服务器响应,可以异步执行。
  • 文件操作。读取文件、访问数据库等 IO 操作,都可能需要等待,可以异步处理。
  • 用户输入。比如点击、输入等用户交互,不能假设用户的响应时间,需要异步处理。
  • 定时任务。设置定时器执行某任务,也是典型的异步操作。

通过异步编程,这些需要等待的任务不会造成整个程序的阻塞,可以优化程序执行流程,提高用户体验。

所以异步编程对于今天的网络应用和交互程序来说,是非常重要的编程方式。正确使用异步编程可以让你的程序更高效、更流畅。

2. 事件循环机制简介

💡事件循环的概念和原理

什么是事件循环呢?

事件循环是一种编程模式。它的核心思想是维护一个事件队列,循环检查队列,处理队列中的事件。

我们比较一下同步模型和事件循环模型:

  • 同步模型:程序按顺序依次执行任务,必须等待前一个任务结束才能执行下一个任务。
  • 事件循环模型:程序维护一个待处理事件队列,循环检查队列,找出需要处理的事件并触发回调函数,然后再循环检查队列......这样一个无结束的循环。

事件循环允许我们注册事件和对应的回调函数,然后程序会自动按照发生顺序调用这些回调函数。

举个例子,假设我们注册了点击和滚动事件的监听器函数,然后点击并滚动页面,事件循环内部会自动调用注册的回调函数处理点击和滚动事件。

通过这样的机制,我们就可以采用异步编程方式来响应事件,不需要像同步模型那样等待事件处理完再做其他事情。

🚀事件循环解决的核心问题

  • 可以异步处理并发事件,不需要阻塞程序等待。
  • 可以 registers 多个事件和回调函数,事件循环会自动调用它们。
  • 不同事件可以共享同一个线程,不需要为每个事件创建新线程。
  • 可以将事件队列作为线程间通信的消息通道。

3. JavaScript 中的事件循环

💻浏览器环境下的事件循环(Event Loop)

在浏览器环境下,JavaScript 语言实现了一个 Event Loop 来处理事件循环。

它的工作流程是这样的:

  1. 所有同步任务直接在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

所以浏览器的事件循环是通过任务队列来实现的。异步任务被抛到队列中,等待同步任务结束,然后从队列中读取事件并执行。

🖥️Node.js 环境下的事件循环

而在 Node.js 中,它实现了一个 Event Loop 来处理事件,与浏览器略有不同:

  1. Node 的 Event Loop 只包含 6 个阶段,它们会控制事件的执行。
  2. 每个阶段都有一个先进先出的回调队列,只有回调队列清空后才会进入下一阶段。
  3. 通过这种阶段控制机制,Node 实现了非阻塞 I/O 模型。

🔍两者的区别和联系

区别:

  1. 浏览器的事件循环是单一的,而 Node.js 有多个阶段的事件循环。
  2. 浏览器将异步任务统一放入任务队列,Node.js 每个阶段有独立的回调队列。
  3. 浏览器没有阶段的概念,所有任务都在一个队列里,Node.js 通过不同阶段来控制任务执行。
  4. 浏览器的事件循环是由 HTML5 标准定义,Node.js 的事件循环规范是独立的。

联系:

  1. 它们底层都实现了事件循环机制,可以处理异步任务而不阻塞主线程。
  2. 都维护一个队列来存储待处理事件,然后循环查找新的事件。
  3. 都会在主线程空闲时,从队列中取出事件处理回调函数执行。
  4. 都允许非阻塞 IO 和事件驱动的编程方式。
  5. 都需要避免长时间运行的任务 blocking 主线程。
  6. 它们帮助 JavaScript 实现了异步非阻塞的并发模型。

4. 🧰事件循环的作用

🔑启用异步编程

事件循环的第一个作用是启用异步编程。

通过事件循环,我们可以把一些需要等待结果的任务注册为异步任务,不会阻塞主线程,非常适合处理诸如网络请求、文件 I/O 等异步操作。

举个例子,在浏览器中发一个 Ajax 请求。我们注册一个回调函数来接收响应,然后该请求被加入事件循环的"任务队列"。我们的主线程可以接着做其他工作,当这个 Ajax 响应返回的时候,对应的回调函数会在事件循环中被调用。这样就实现了异步编程方式。

🔗调度任务执行

事件循环的第二个作用是调度任务执行。

事件循环会按照正确的顺序执行回调函数,比如先点击按钮,后滚动页面,事件循环会保证先调用点击的回调函数,然后再调用滚动的回调。这样就实现了调度作用。

🔧优化性能

事件循环的第三个作用是优化性能。

通过异步非阻塞的方式执行任务,可以大大减少程序中的等待时间,充分利用 CPU,也节约了线程资源,非常高效。

🧯避免长时间阻塞

最后,事件循环避免了长时间运行的任务 blocking 主线程。

因为事件循环会按照队列顺序执行回调函数,所以每个回调函数不能运行太长时间,否则会堵塞后面的任务。通过这种方式,事件循环很好的避免了单个任务的长时间阻塞。

5. 👓常见的异步编程方法

回调函数

第一种是回调函数。一个函数可以接受另一个函数作为参数,这个作为参数传递进去的函数就叫回调函数。回调函数通常在当前任务完成后被执行。

例如,我们可以注册一个回调函数来处理 Ajax 返回的结果:

js 复制代码
function ajaxCallback(result) {
  console.log(result);
}

ajax("http://example.com", ajaxCallback);

Promise

Promise 是一个对象,代表了一个异步任务的最终完成或失败。我们可以在 Promise 对象上注册回调函数。

使用 Promise 的话,代码如下:

js 复制代码
let p = new Promise();

p.then(function (result) {
  console.log(result);
});

p.catch(function (err) {
  console.log(err);
});

async/await

async/await,这是基于 Promise 实现的语法糖。async 函数会返回一个 Promise 对象,await 等待 Promise 结果:

js 复制代码
async function myFunc() {
  let result = await doSomethingAsync();

  console.log(result);
}

事件和监听器

最后是事件和监听器,这是浏览器异步编程的主要方式。例如可以为按钮添加点击事件:

js 复制代码
let btn = document.getElementById("myBtn");

btn.addEventListener("click", function () {
  // 收到点击事件后的处理
});

6. 实现一个简单的事件循环

往往事件循环代表了应用的整个生命周期,事件循环结束意味着应用也就退出了。

我们先来看一个简陋版的事件循环系统。

js 复制代码
class EventSystem {
  constructor() {
    // 任务队列
    this.queue = [];
  }

  // 追加任务
  enQueue(func) {
    this.queue.push(func);
  }

  // 事件循环
  run() {
    while (1) {
      while (this.queue.length) {
        const func = this.queue.shift();
        func();
      }
    }
  }
}
// 新建一个事件循环系统
const eventSystem = new EventSystem();

// 生产任务
eventSystem.enQueue(() => {
  console.log("hi");
});

// 启动事件循环
eventSystem.run();

// 生产任务
eventSystem.enQueue(() => {
  console.log("hi");
});

根据上面我们了解到的知识,我们可以知道这个简陋版的事件循环逻辑如下

  1. 新建一个事件循环系统;
  2. 生产任务;
  3. 启动事件循环系统处理任务。

但是!如果你运行就会发现,当没有任务时,它就会陷入死循环,这不仅浪费了 CPU,新的任务也无法继续添加了。

因此,在一个事件循环系统中,如何处理没有任务时的情况非常关键。这应该怎么解决呢?

js 复制代码
class EventSystem {
  constructor() {
    // 需要处理的任务队列
    this.queue = [];
    // 标记是否需要退出事件循环
    this.stop = 0;
    // 有任务时调用该函数"唤醒" await
    this.wakeup = null;
  }
  // 没有任务时,事件循环的进入"阻塞"状态
  wait() {
    return new Promise((resolve) => {
      // 记录 resolve,可能在睡眠期间有任务到来,则需要提前唤醒
      this.wakeup = () => {
        this.wakeup = null;
        resolve();
      };
    });
  }
  // 停止事件循环,如果正在"阻塞",则"唤醒它"
  setStop() {
    this.stop = 1;
    this.wakeup && this.wakeup();
  }
  // 生产任务
  enQueue(func) {
    this.queue.push(func);
    this.wakeup && this.wakeup();
  }

  // 处理任务队列
  handleTask() {
    if (this.queue.length === 0) {
      return;
    }
    // 本轮事件循环的回调中加入的任务,下一轮事件循环再处理,防止其他任务没有机会处理
    const queue = this.queue;
    this.queue = [];
    while (queue.length) {
      const func = queue.shift();
      func();
    }
  }

  // 事件循环的实现
  async run() {
    // 如果 stop 等于 1 则退出事件循环
    while (this.stop === 0) {
      // 处理任务,可能没有任务需要处理
      this.handleTask();
      // 处理任务过程中如果设置了 stop 标记则退出事件循环
      if (this.stop === 1) {
        break;
      }
      // 没有任务了,进入睡眠
      if (this.queue.length === 0) {
        await this.wait();
      }
    }
    // 退出前可能还有任务没处理,处理完再退出
    this.handleTask();
  }
}

// 新建一个事件循环系统
const eventSystem = new EventSystem();

// 启动前生成的任务
eventSystem.enQueue(() => {
  console.log("1");
});

// 模拟定时生成一个任务
setTimeout(() => {
  eventSystem.enQueue(() => {
    console.log("3");
    eventSystem.setStop();
  });
}, 1000);

// 启动事件循环
eventSystem.run();

// 启动后生成的任务
eventSystem.enQueue(() => {
  console.log("2");
});

现在我们就有了一个简单但是核心功能都有的事件循环系统。

  1. 事件循环的整体架构是一个 while 循环。
  2. 定义任务类型和队列,这里只有一种任务类型和一个队列,比如 Node.js 里有好几种,每种类型的任务有不同的作用。
  3. 没有任务的时候怎么处理:进入阻塞状态,而不是靠忙轮询。

第 3 点是事件循环系统中非常重要的逻辑,任务队列中不可能一直都有任务需要处理,这就意味着生产任务可以是一个异步的过程,所以事件循环系统就需要有一种等待 / 唤醒机制。

7. 总结

🔔异步编程模式的意义

异步编程模式非常重要,它允许我们在等待某个操作时继续执行其他代码,大大提高了效率。

其核心是事件循环机制。它维护一个事件队列,然后循环检查队列并执行回调函数,这样就实现了异步执行流程。

JavaScript 语言利用事件循环实现了异步编程。比如,在浏览器中我们可以注册事件回调函数,Node.js 也使用事件循环处理非阻塞 IO。

💾事件循环的工作机制

  1. 事件循环的整体架构是一个 while 循环。
  2. 定义任务类型和队列,这里只有一种任务类型和一个队列,比如 Node.js 里有好几种,每种类型的任务有不同的作用。
  3. 没有任务的时候怎么处理:进入阻塞状态,而不是靠忙轮询。

💻在 JavaScript 中高效利用事件循环

要在 JavaScript 中高效利用事件循环,我们可以:

  1. 多用异步 API,比如定时器、Promise 等,避免阻塞。
  2. 合理使用异步语法 async/await 来组织代码。
  3. 避免长时间运行的任务阻塞线程。
  4. 使用 Promise 链或 async/await 处理回调地狱。
  5. 合理使用定时器和请求拆分来控制并发。
  6. 使用事件循环可视化工具观察事件循环状态。
相关推荐
M_emory_10 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito13 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
abments1 小时前
JavaScript逆向爬虫教程-------基础篇之常用的编码与加密介绍(python和js实现)
javascript·爬虫·python
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
老码沉思录1 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript