从同步到异步:JavaScript 异步编程与 Promise 的救赎

501 阅读6分钟

从同步到异步:JavaScript 异步编程与 Promise 的救赎

在现代 Web 开发中,JavaScript 肩负着处理用户交互、动态更新页面内容等重要职责。然而,JavaScript 是单线程的,这意味着它一次只能执行一个任务。为了不阻塞主线程,避免页面卡顿,异步编程成为了 JavaScript 的必备技能。

一、异步是什么?为什么会出现异步?

同步,顾名思义,就是按部就班地执行任务,只有前一个任务完成,才会执行下一个任务。想象一下排队买奶茶,你必须等到前面的人买完,才能轮到自己。

异步则不同,它允许你在等待一个任务完成的同时,去执行其他任务。就像在餐厅点餐,你不需要一直盯着厨师做饭,而是可以先玩手机,等餐好了服务员会通知你。

为什么会出现异步?

  • 避免阻塞主线程: JavaScript 运行在浏览器的主线程中,如果所有任务都是同步的,那么一个耗时的任务就会阻塞整个页面,导致用户无法进行其他操作。
  • 提高效率: 异步操作可以充分利用 CPU 和网络资源,提高程序的执行效率。
  • 更好的用户体验: 异步操作可以使页面更加流畅,提升用户体验。

二、异步与同步的区别

特性同步异步
执行顺序按顺序执行,前一个任务完成才能执行下一个不按顺序执行,可以同时执行多个任务
阻塞会阻塞主线程不会阻塞主线程
效率效率较低效率较高
用户体验可能导致页面卡顿页面更加流畅

三、JavaScript 如何解决异步问题?

JavaScript 提供了多种机制来处理异步操作,包括:

  • 回调函数: 将函数作为参数传递给另一个函数,在异步操作完成后调用该函数。
  • 事件监听: 监听特定事件的发生,并在事件触发时执行相应的操作。
  • Promise: 一种更优雅的异步编程解决方案,可以避免回调地狱。
  • Async/Await: 基于 Promise 的语法糖,使异步代码看起来像同步代码一样简洁。

四、Promise:异步编程的救星

Promise 是 JavaScript 中处理异步操作的一种更优雅的方式。它代表一个异步操作的最终完成或失败,并允许我们以链式调用的方式处理结果。

Promise 的基本用法:

  1. 创建 Promise: 使用 new Promise() 构造函数创建一个 Promise 对象,并传入一个执行器函数。执行器函数接收两个参数:resolvereject,分别用于在异步操作成功或失败时调用。

  2. 处理结果: 使用 .then() 方法注册成功回调函数,使用 .catch() 方法注册失败回调函数。

  3. 链式调用: 每个 .then() 方法都会返回一个新的 Promise,允许我们将多个异步操作串联起来。

用 Promise 解决回调地狱:

  • 回调地狱写法:
function getUser(userId, callback) {
  setTimeout(() => {
    callback({ id: userId, name: "Liang" });
  }, 1000);
}

function getOrder(user, callback) {
  setTimeout(() => {
    callback({ orderId: 123, userId: user.id });
  }, 1000);
}

function getProduct(order, callback) {
  setTimeout(() => {
    callback({ productId: 456, orderId: order.orderId });
  }, 1000);
}

// 回调地狱
getUser(1, (user) => {
  getOrder(user, (order) => {
    getProduct(order, (product) => {
      console.log("Product details:", product);
    });
  });
});
  
  • Promise和.then的改进
function getUser(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: userId, name: "Liang" });
    }, 1000);
  });
}

function getOrder(user) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ orderId: 123, userId: user.id });
    }, 1000);
  });
}

function getProduct(order) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ productId: 456, orderId: order.orderId });
    }, 1000);
  });
}

// 使用 .then 链式调用
getUser(1)
  .then((user) => {
    console.log("User details:", user);
    return getOrder(user); // 返回下一个 Promise
  })
  .then((order) => {
    console.log("Order details:", order);
    return getProduct(order); // 返回下一个 Promise
  })
  .then((product) => {
    console.log("Product details:", product);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

Promise 的优势:

  • 代码更易读: 链式调用取代了嵌套回调,代码结构更加清晰。
  • 避免回调地狱: 回调地狱:代码嵌套过深,维护成本大,一旦出现问题很难排查

为了进一步简化异步编程,ES7 引入了 async/await 语法,它基于 Promise 并提供了更接近同步代码的写法,让异步编程变得更加直观和易于理解。


五、Async/Await:让异步代码像同步代码一样优雅

async/await 是 JavaScript 中处理异步操作的一种语法糖,它允许我们以同步的方式编写异步代码,同时保留异步操作的非阻塞特性。

1. Async/Await 的基本概念

  • async 关键字:

    • 用于声明一个异步函数。异步函数会自动返回一个 Promise 对象,无论函数内部是否有显式的 return 语句。
    • 例如:
      async function fetchData() {
        return "Data fetched!";
      }
      // 等价于
      function fetchData() {
        return Promise.resolve("Data fetched!");
      }
      
  • await 关键字:

    • 用于等待一个 Promise 对象的解析结果。await 会暂停当前异步函数的执行,直到 Promise 被解决(resolved)或拒绝(rejected)。
    • await 只能在 async 函数内部使用。
    • 例如:
      async function main() {
        const userData = await getUserData();
        console.log(userData.name);
        console.log(userData.age);
      }
      
      

2. Async/Await 的优势

  • 代码更简洁:
    • 使用 async/await 可以避免 .then().catch() 的链式调用,代码结构更加扁平化,更接近同步代码的写法。
  • 错误处理更方便:
    • 可以使用 try...catch 语句来捕获异步操作中的错误,就像处理同步代码中的错误一样。
  • 可读性更高:
    • async/await 使异步代码的执行流程更加清晰易懂,降低了代码的维护成本。

3. Async/Await 的使用示例

示例 1:基本用法

async function getUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching user data:', error);
  }
}

getUserData(1);
  • 代码解析:
    • fetch 是一个异步操作,返回一个 Promise。await 会等待 fetch 完成,并将结果赋值给 response
    • response.json() 也是一个异步操作,await 会等待其完成,并将解析后的 JSON 数据赋值给 data
    • 如果发生错误,try...catch 会捕获并处理错误。

4. Async/Await 的注意事项

  • 只能在 async 函数中使用 await
    • 如果在普通函数中使用 await,会抛出语法错误。
  • async 函数总是返回 Promise:
    • 即使函数内部返回一个非 Promise 的值,它也会被自动包装成一个 Promise。
  • 错误处理:
    • 使用 try...catch 捕获异步操作中的错误,避免未处理的 Promise 拒绝。

5. Async/Await 与 Promise 的关系

  • async/await 是基于 Promise 的语法糖,它并没有取代 Promise,而是让 Promise 的使用更加方便。

  • 任何使用 async/await 的代码都可以用 Promise 重写,反之亦然。

  • 例如:

    // 使用 Promise
      function getUserData() {
          return new Promise((resolve, reject) => {
              setTimeout(() => {
                  const user = { name: "Liang", id: 1 };
                  resolve(user);
              }, 1000);
          });
      }
      function getOrdersByUserId(userId) {
          return new Promise((resolve, reject) => {
              setTimeout(() => {
                  const orders = [
                      { id: 1, product: "Book" },
                      { id: 2, product: "Pen" }
                  ];
                  resolve(orders);
              }, 1000);
          });
      }
      function main() {
          getUserData().then((userData) => {
              getOrdersByUserId(userData.id).then((orders) => {
                  console.log(orders);
              });
          });
      }
    
    //使用Async/Await
        async function main() {
            const userData = await getUserData();
            const orders = await getOrdersByUserId(userData.id);
            console.log(orders);
        }
    

总结

JavaScript 的异步编程从回调函数到 Promise,再到 Async/Await,不断演进,旨在解决单线程带来的阻塞问题,并提供更优雅、更易读的代码编写方式。掌握这些技术,可以让你在异步编程的世界里游刃有余,构建出更加高效、流畅的 Web 应用。