Skip to content
扫码联系作者

前端异步编程解决方案

为什么会有异步编程

首先 Javascript 语言的执行环境是"单线程"。就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。在这种情况下,如果中途出现一个非常耗时的任务,比如读取文件、进行网络请求、处理用户输入等就会导致整个程序等待,在这些等待的过程中,程序会被阻塞,无法进行其他操作,导致用户界面卡死或程序变得非常慢。为了解决这个问题,引入了异步编程的概念。
异步编程允许程序在等待某些操作完成时继续执行其他任务,而不是一直等待阻塞。在这种模式下,可以发起一个操作,然后继续执行其他任务,当操作完成后,通过回调函数或者类似的机制来处理操作的结果。
主要原因和需求如下:

  1. 避免阻塞:异步编程允许程序在等待耗时的操作时继续响应其他任务,提高程序的并发性和响应性能。
  2. 提高性能:对于耗时的操作(例如网络请求、文件读写等),异步编程可以允许同时执行多个操作,从而提高整体性能。
  3. 用户体验:在前端应用中,异步编程非常重要,可以确保用户界面在进行耗时操作时不会被阻塞,保持流畅的交互体验。
  4. 并发编程:在服务器端或多线程环境中,异步编程也是必要的,可以提高系统的吞吐量和并发性能。

异步编程的几种方案

回调

回调函数曾经是 JavaScript 中实现异步函数的主要方式。

例子

javascript
function doStep1(init, callback) {
  const result = init + 1;
  callback(result);
}
function doStep2(init, callback) {
  const result = init + 2;
  callback(result);
}
function doStep3(init, callback) {
  const result = init + 3;
  callback(result);
}
function doOperation() {
  doStep1(0, (result1) => {
    doStep2(result1, (result2) => {
      doStep3(result2, (result3) => {
        console.log(`结果:${result3}`);
      });
    });
  });
}
doOperation();

优缺点

优点:简单易懂,是一种传统的异步编程方式。
缺点:容易导致回调地狱,代码嵌套过深,可读性和维护性差。面对这样的嵌套回调,处理错误也会变得非常困难:你必须在“金字塔”的每一级处理错误,而不是在最高一级一次完成错误处理。

事件监听

主要是取决于事件的发生,有事件发生,对应事件绑定的函数就会执行。

例子

javascript
element.addEventListener("click", function () {
  alert("Hello World!");
});

优缺点

优点:可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。
缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。

发布订阅

前端发布订阅模式(Pub/Sub)是一种常见的设计模式,用于实现组件之间的解耦和事件通信,在异步处理的场景中应用也非常广泛。

例子

javascript
// 发布订阅管理器
const pubSub = {
  events: {},
  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },
  publish(event, data) {
    if (!this.events[event]) return;
    this.events[event].forEach((callback) => callback(data));
  },
};
javascript
// 按钮组件
<button id="btn">点击我</button>;

const button = document.getElementById("btn");

button.addEventListener("click", () => {
  // 发布按钮点击事件,传递消息
  pubSub.publish("buttonClicked", "按钮被点击了!");
});
javascript
// 消息组件
<div id="message"></div>;

// 消息显示组件
const messageElement = document.getElementById("message");

// 订阅按钮点击事件
pubSub.subscribe("buttonClicked", (message) => {
  // 显示接收到的消息
  messageElement.textContent = message;
});

在这个例子中,我们通过 pubSub 对象实现了一个简单的发布订阅管理器。在按钮组件中,当按钮被点击时,我们通过pubSub.publish方法发布了一个名为buttonClicked的事件,并传递了相应的消息。在消息显示组件中,我们通过pubSub.subscribe方法订阅了buttonClicked事件,并提供了一个回调函数来处理接收到的消息。当按钮被点击时,消息显示组件会接收到通知,并将消息显示在页面上。

优缺点

优点:

  1. 解耦:发布订阅模式可以将组件解耦,使它们不需要直接知道彼此的存在。这样,当一个组件发生改变时,不会影响其他组件的功能,提高了代码的灵活性和可维护性。
  2. 可扩展性:由于组件之间的通信通过发布订阅模式实现,新的组件可以很容易地加入到系统中,无需修改现有代码。
  3. 异步处理:发布订阅模式适用于异步事件处理,可以在某个事件发生时通知所有订阅者进行相应的处理。
  4. 事件中心:发布订阅模式提供了一个事件中心,方便管理和维护不同事件及其对应的订阅者。

缺点:

  1. 内存管理:如果不适当地使用发布订阅模式,可能会导致内存泄漏。因为发布订阅模式需要维护事件订阅列表,如果订阅者没有正确地进行取消订阅操作,可能会导致订阅者一直存在于内存中,无法被回收。
  2. 可读性:使用发布订阅模式可能会导致代码逻辑变得复杂,特别是在多个组件之间存在复杂的事件关系时,代码可能会比较难以理解和维护。
  3. 不适合所有场景:并不是所有的应用场景都适合使用发布订阅模式。在简单的应用中,使用发布订阅模式可能会增加不必要的复杂性,导致代码冗余。
  4. 调试困难:由于发布订阅模式是通过事件来通信的,当出现问题时,可能需要跟踪事件的传递过程,对于复杂的应用可能会增加调试的难度。

Promise

Promise 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。

例子

javascript
const fetchPromise = fetch(
  "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json"
);

console.log(fetchPromise);

fetchPromise.then((response) => {
  console.log(`已收到响应:${response.status}`);
});

console.log("已发送请求……");

优缺点

优点:状态改变就不会再变,任何时候都能得到相同的结果。将异步事件的处理流程化,写法更方便。
缺点:仍然需要通过.then()和.catch()方法来处理异步操作,可能会有一些回调嵌套。

Generator

ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供解决方案。

例子

javascript
function* foo(index) {
  while (index < 2) {
    yield index;
    index++;
  }
}

const iterator = foo(0);

console.log(iterator.next().value);
// Expected output: 0

console.log(iterator.next().value);
// Expected output: 1

优缺点

优点:Generator 函数可以在函数执行的不同阶段返回值,结合 Promise 可以实现更复杂的异步流程控制。
缺点:相对于其他方案,Generator 函数语法较复杂,不太直观。

async/await

async 和 await 关键字是最近添加到 JavaScript 语言里面的。它们是 ECMAScript 2017 的一部分,简单来说,它们是基于 promises 的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码。

例子

javascript
function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("resolved");
    }, 2000);
  });
}

async function asyncCall() {
  console.log("calling");
  const result = await resolveAfter2Seconds();
  console.log(result);
  // Expected output: "resolved"
}

asyncCall();

优缺点

优点:使用 async/await 可以让异步代码看起来更像同步代码,可读性更好。
缺点:需要在支持 ES6 的环境下运行,对于旧的浏览器可能需要进行转译。

响应式编程

响应式编程,它是一种基于事件的模型。在上面的异步编程模式中,我们描述了两种获得上一个任务执行结果的方式,一个就是主动轮训,我们把它称为 Proactive 方式。另一个就是被动接收反馈,我们称为 Reactive。简单来说,在 Reactive 方式中,上一个任务的结果的反馈就是一个事件,这个事件的到来将会触发下一个任务的执行。

例子

我们用 RxJS 库来给出一个简单的例子。
在 HTML 文件中,添加一个按钮和一个用于显示结果的元素:

javascript
<button id="btn">点击我</button>
<p id="result"></p>

在 JavaScript 文件中,使用 RxJS 来创建 Observables 并订阅它:

javascript
// 引入RxJS库
import { fromEvent } from "rxjs";

// 获取按钮和结果元素
const button = document.getElementById("btn");
const resultElement = document.getElementById("result");

// 创建一个点击事件的Observables
const clickObservable = fromEvent(button, "click");

// 订阅Observables,当按钮点击时触发回调函数
clickObservable.subscribe(() => {
  resultElement.textContent = "按钮被点击了!";
});

在上面的例子中,我们使用 RxJS 的 fromEvent 函数来创建一个点击事件的 Observables。然后,我们使用 subscribe 方法来订阅这个 Observables,并传入一个回调函数。当按钮被点击时,回调函数会执行,将文本内容更新为"按钮被点击了!"。

优缺点

优点:RxJS 基于 Observables 序列,可以更方便地处理事件、处理异步数据以及构建复杂的数据流,非常适合处理实时数据和事件驱动的应用。
缺点:学习曲线较陡峭,对于简单的应用可能会显得过于复杂。

总结

综合考虑,对于简单的异步操作,Promise 和 async/await 是较为常用和简洁的解决方案。而对于复杂的异步数据流和事件处理,RxJS 提供了更强大的工具和抽象能力。Generator 函数在一些特定的场景中也可以发挥一定作用,但使用较少。回调函数在现代前端开发中使用较少,更多地被其他方案所替代。
在选择合适的异步编程方案时,需根据具体项目需求、团队熟悉程度和项目规模等因素进行权衡和取舍。