详解|JS实现异步的三种方法

101 阅读6分钟

在 JavaScript 中,异步编程是处理非阻塞操作(如网络请求、定时器等)的一种常见方式。随着 JavaScript 的发展,异步编程的方法也逐渐演化。以下是一些常用的实现异步编程的方法及其特点:

一、回调函数(Callback)

概念:

  • 回调函数是最早的异步编程方式。通过将一个函数作为参数传递给另一个函数,待异步操作完成后,调用该回调函数来处理结果。

示例:

function fetchData(callback) {
    setTimeout(() => {
        const data = 'some data';
        callback(data);
    }, 1000);
}

fetchData((result) => {
    console.log(result); // 输出 'some data'
});

优点:

  • 简单直观,适用于简单的异步操作。

缺点:

  • 回调地狱:当需要处理多个异步操作时,回调嵌套会导致代码难以维护,形成所谓的“回调地狱”。

二、Promise

概念:

  • Promise 是 ES6 引入的一种异步编程方式。它代表了一个异步操作的最终完成(或失败)及其结果值。Promise 可以链式调用。
  • 三种方法:用.then().catch().finally() 方法处理异步操作的结果。
  • 三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。
  • 两个参数(创建promise实例):resolve 和 reject。
  • Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行。

示例:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const data = 'some data';
            resolve(data); // 或者 reject('error') 表示操作失败
        }, 1000);
    });
}

fetchData()
    .then((result) => {
        console.log(result); // 输出 'some data'
    })
    .catch((error) => {
        console.error(error);
    });

优点:

  • 链式调用,解决了回调地狱问题,代码更清晰。
  • 内建错误处理机制(catch),使得异常处理更为便捷。

缺点:

  • 仍然需要处理多个 then 方法的嵌套,且代码结构上不如同步代码直观。

三、async/await

概念:

  • async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行
  • async作为一个关键字放在函数前面,表示该函数是一个异步函数,异步函数意味着该函数的执行不会阻塞后面代码的执行;而 await 用于等待一个异步方法执行完成;
  • await 等待一个 Promise 对象,如果 Promise的状态变成了 resolve 或者 rejcet,那么 async函数会恢复执行。并会阻塞该函数内后面的代码。
  • 使用 async/await 可以实现用同步代码的风格来编写异步代码,这是因为 async/await 的基础技术使用了生成器和 Promise,生成器是协程的实现,利用生成器能实现生成器函数的暂停和恢复。
  • 为了优化 .then 链而开发出来的。
  • async/await 是基于 Promise 的语法糖。它使异步代码看起来更像同步代码,便于理解和维护。await 会暂停函数的执行,等待 Promise 完成,并返回其结果。
  • try/catch 是 JavaScript 中用于异常处理的语法结构。try 块包含可能会抛出错误的代码,而 catch 块用于处理这些错误。这样可以防止程序因未处理的错误而崩溃。通过使用 try/catch块,我们可以捕获和处理异步操作中的错误。这种语法使得代码更易于理解和维护,同时避免了回调地狱,使得处理复杂的异步逻辑更加简洁和直观。
  • 例如,如果一个 fetch 请求失败,我们可以在catch块中处理错误并给用户友好的提示。

Async/Await 如何通过同步的方式实现异步

Async/Await 就是一个自执行的 generate 函数。利用 generate 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象.

await后面可以:

  • +Promise对象:会阻塞函数后面的代码(即加入微任务队列),等待promise对象执行完毕,并返回结果。
  • +任意表达式:V8 会隐式地将该对象包装成一个已经 resolve 的 Promise 对象

当遇到 await 时,会阻塞函数内部处于它后面的代码(而非整段代码),去执行该函数外部的同步代码;当外部的同步代码执行完毕,再回到该函数执行剩余的代码。并且当 await 执行完毕之后,会优先处理微任务队列的代码。

image.png

示例:

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const data = 'some data';
            resolve(data);
        }, 1000);
    });
}

async function getData() {
    try {
        const result = await fetchData();
        console.log(result); // 输出 'some data'
    } catch (error) {
        console.error(error);
    }
}

getData();

优点:

  • 代码结构更清晰,接近同步代码的书写方式,易于理解。
  • 更加简化了 Promise 的链式调用和错误处理。

缺点:

  • 需要在 async 函数中使用 await,不能完全取代 Promise 的使用。

四、setTimeout/setInterval

概念:

  • setTimeoutsetInterval 是最基础的异步定时器函数,分别用于延迟执行和周期性执行某个操作。

示例:

// setTimeout:延迟执行
setTimeout(() => {
    console.log('Executed after 1 second');
}, 1000);

// setInterval:每隔一定时间执行一次
let count = 0;
const intervalId = setInterval(() => {
    count++;
    console.log('Executed every second');
    if (count === 5) {
        clearInterval(intervalId); // 停止定时器
    }
}, 1000);

优点:

  • 简单易用,适合定时操作。

缺点:

  • 只能用于定时异步操作,无法处理其他异步场景。

五、EventEmitter(Node.js)

概念:

  • 在 Node.js 中,EventEmitter 是处理异步事件的核心模块。它允许你定义并触发事件,以及监听和处理这些事件。

示例:

const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('dataReceived', (data) => {
    console.log(`Data received: ${data}`);
});

// 模拟异步事件
setTimeout(() => {
    emitter.emit('dataReceived', 'some data');
}, 1000);

优点:

  • 适合事件驱动的异步编程模型。
  • 灵活性高,适用于复杂的异步事件处理。

缺点:

  • 需要额外学习和掌握 EventEmitter 的使用方式。
  • 代码结构较为复杂,不如 Promiseasync/await 直观。

六、fetch API

概念:

  • fetch 是一种现代的、基于 Promise 的 API,用于进行网络请求。它简化了 AJAX 操作。

示例:

async function fetchData() {
    try {
        const response = await fetch('<https://jsonplaceholder.typicode.com/posts/1>');
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}

fetchData();

优点:

  • 原生支持 Promise,简化了网络请求的异步操作。
  • 语法简洁,易于使用。

缺点:

  • 相较于 XMLHttpRequestfetch 不会自动处理 HTTP 错误,需要手动检查响应状态。

总结

  • 回调函数 是最基础的异步实现方式,但容易陷入回调地狱。
  • Promise 提供了更优雅的异步解决方案,支持链式调用和错误处理。
  • async/await 是基于 Promise 的语法糖,使异步代码更像同步代码,结构更清晰。
  • setTimeout/setInterval 是定时异步操作的简单实现。
  • EventEmitter 在 Node.js 中用于事件驱动的异步编程。
  • fetch 提供了现代化的网络请求异步处理。

根据实际需求选择合适的异步方法,能有效提升代码的可读性和维护性。