Skip to content

JavaScript 异步编程

什么是异步编程

异步编程是一种编程模式,允许程序在执行一个可能需要长时间运行的操作时,不阻塞其他操作的执行。在 JavaScript 中,异步编程尤为重要,因为 JavaScript 是单线程的,如果所有操作都是同步执行的,那么长时间运行的操作会导致页面卡顿,用户体验不佳。

同步 vs 异步

同步执行

同步执行是指代码按照顺序依次执行,每一行代码必须等待上一行代码执行完成后才能执行。

javascript
// 同步执行示例
console.log("Start");

function synchronousTask() {
  // 模拟耗时操作
  let sum = 0;
  for (let i = 0; i < 1000000000; i++) {
    sum += i;
  }
  return sum;
}

const result = synchronousTask();
console.log("Result:", result);
console.log("End");

// 输出顺序:
// Start
// Result: 499999999500000000
// End
// 注意:在计算过程中,页面会卡顿

异步执行

异步执行是指代码不按照顺序执行,而是在某个操作完成后通过回调、Promise 或 async/await 等方式通知程序继续执行后续操作。

javascript
// 异步执行示例
console.log("Start");

function asynchronousTask(callback) {
  // 模拟异步耗时操作
  setTimeout(() => {
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
      sum += i;
    }
    callback(sum);
  }, 0);
}

asynchronousTask((result) => {
  console.log("Result:", result);
});

console.log("End");

// 输出顺序:
// Start
// End
// Result: 499999999500000000
// 注意:在计算过程中,页面不会卡顿

异步编程的实现方式

1. 回调函数

回调函数是最基本的异步编程方式,它是一个函数,作为参数传递给另一个函数,当异步操作完成时被调用。

javascript
// 回调函数示例
function fetchData(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open("GET", url, true);
  xhr.onload = function () {
    if (xhr.status === 200) {
      callback(null, JSON.parse(xhr.responseText));
    } else {
      callback(new Error("请求失败"), null);
    }
  };
  xhr.onerror = function () {
    callback(new Error("网络错误"), null);
  };
  xhr.send();
}

// 使用回调函数
fetchData("https://api.example.com/data", (error, data) => {
  if (error) {
    console.error("错误:", error);
  } else {
    console.log("数据:", data);
  }
});

回调地狱

当多个异步操作需要按顺序执行时,回调函数会嵌套在一起,形成所谓的"回调地狱",代码可读性和可维护性变差。

javascript
// 回调地狱示例
fetchData("https://api.example.com/user/1", (error, user) => {
  if (error) {
    console.error("错误:", error);
  } else {
    fetchData(
      `https://api.example.com/posts?userId=${user.id}`,
      (error, posts) => {
        if (error) {
          console.error("错误:", error);
        } else {
          fetchData(
            `https://api.example.com/comments?postId=${posts[0].id}`,
            (error, comments) => {
              if (error) {
                console.error("错误:", error);
              } else {
                console.log("用户:", user);
                console.log("帖子:", posts);
                console.log("评论:", comments);
              }
            }
          );
        }
      }
    );
  }
});

2. Promise

Promise 是 ES6 引入的异步编程解决方案,它代表一个异步操作的最终完成(或失败)及其结果值。

Promise 的基本用法

javascript
// Promise 示例
function fetchData(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.onload = function () {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error("请求失败"));
      }
    };
    xhr.onerror = function () {
      reject(new Error("网络错误"));
    };
    xhr.send();
  });
}

// 使用 Promise
fetchData("https://api.example.com/data")
  .then((data) => {
    console.log("数据:", data);
    return fetchData("https://api.example.com/other");
  })
  .then((otherData) => {
    console.log("其他数据:", otherData);
  })
  .catch((error) => {
    console.error("错误:", error);
  })
  .finally(() => {
    console.log("操作完成");
  });

Promise 的状态

Promise 有三种状态:

  • pending:初始状态,既不是成功,也不是失败
  • fulfilled:操作成功完成
  • rejected:操作失败

一旦 Promise 的状态从 pending 变为 fulfilled 或 rejected,就不能再改变。

Promise 的方法

Promise.resolve()

创建一个立即解决的 Promise。

javascript
const promise1 = Promise.resolve("成功");
promise1.then((value) => {
  console.log(value); // 成功
});

const promise2 = Promise.resolve(42);
promise2.then((value) => {
  console.log(value); // 42
});
Promise.reject()

创建一个立即拒绝的 Promise。

javascript
const promise = Promise.reject(new Error("失败"));
promise.catch((error) => {
  console.error(error); // Error: 失败
});
Promise.all()

接收一个 Promise 数组,返回一个新的 Promise,当所有 Promise 都解决时才解决,当任何一个 Promise 拒绝时就拒绝。

javascript
const promise1 = fetchData("https://api.example.com/data1");
const promise2 = fetchData("https://api.example.com/data2");
const promise3 = fetchData("https://api.example.com/data3");

Promise.all([promise1, promise2, promise3])
  .then((values) => {
    console.log("所有数据:", values);
  })
  .catch((error) => {
    console.error("错误:", error);
  });
Promise.race()

接收一个 Promise 数组,返回一个新的 Promise,当第一个 Promise 解决或拒绝时,就采用该 Promise 的结果。

javascript
const promise1 = new Promise((resolve) => setTimeout(resolve, 100, "第一个"));
const promise2 = new Promise((resolve) => setTimeout(resolve, 50, "第二个"));

Promise.race([promise1, promise2]).then((value) => {
  console.log(value); // 第二个
});
Promise.allSettled()

接收一个 Promise 数组,返回一个新的 Promise,当所有 Promise 都完成(无论成功或失败)时,才解决并返回所有 Promise 的结果。

javascript
const promise1 = fetchData("https://api.example.com/data1");
const promise2 = fetchData("https://api.example.com/data2"); // 假设这个会失败
const promise3 = fetchData("https://api.example.com/data3");

Promise.allSettled([promise1, promise2, promise3]).then((results) => {
  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log("成功:", result.value);
    } else {
      console.error("失败:", result.reason);
    }
  });
});
Promise.any()

接收一个 Promise 数组,返回一个新的 Promise,当第一个 Promise 解决时,就采用该 Promise 的结果;当所有 Promise 都拒绝时,才拒绝。

javascript
const promise1 = Promise.reject(new Error("第一个失败"));
const promise2 = Promise.resolve("第二个成功");
const promise3 = Promise.reject(new Error("第三个失败"));

Promise.any([promise1, promise2, promise3])
  .then((value) => {
    console.log(value); // 第二个成功
  })
  .catch((error) => {
    console.error("所有都失败:", error);
  });

3. async/await

async/await 是 ES2017 引入的异步编程语法糖,它基于 Promise,使异步代码看起来更像同步代码,提高了代码的可读性。

基本用法

javascript
// async/await 示例
async function fetchData(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error("请求失败");
  }
  const data = await response.json();
  return data;
}

// 使用 async/await
async function getData() {
  try {
    const data = await fetchData("https://api.example.com/data");
    console.log("数据:", data);

    const otherData = await fetchData("https://api.example.com/other");
    console.log("其他数据:", otherData);
  } catch (error) {
    console.error("错误:", error);
  } finally {
    console.log("操作完成");
  }
}

getData();

注意事项

  • async 函数返回一个 Promise
  • await 只能在 async 函数内部使用
  • await 后面跟着一个 Promise,等待该 Promise 解决
  • 可以使用 try/catch 捕获异步操作的错误

并行执行

使用 Promise.all() 可以在 async/await 中实现并行执行多个异步操作。

javascript
async function getMultipleData() {
  try {
    const [data1, data2, data3] = await Promise.all([
      fetchData("https://api.example.com/data1"),
      fetchData("https://api.example.com/data2"),
      fetchData("https://api.example.com/data3"),
    ]);

    console.log("数据1:", data1);
    console.log("数据2:", data2);
    console.log("数据3:", data3);
  } catch (error) {
    console.error("错误:", error);
  }
}

getMultipleData();

4. 事件监听

事件监听是另一种异步编程方式,通过监听事件的触发来执行相应的回调函数。

javascript
// 事件监听示例
const button = document.getElementById("myButton");

button.addEventListener("click", function (event) {
  console.log("按钮被点击了");
  // 执行异步操作
  fetchData("https://api.example.com/data")
    .then((data) => {
      console.log("数据:", data);
    })
    .catch((error) => {
      console.error("错误:", error);
    });
});

5. Generator 函数

Generator 函数是 ES6 引入的一种特殊函数,可以暂停执行和恢复执行,也可以用于异步编程。

javascript
// Generator 函数示例
function* generator() {
  console.log("开始");
  const value1 = yield "第一个值";
  console.log("接收到:", value1);
  const value2 = yield "第二个值";
  console.log("接收到:", value2);
  return "结束";
}

const gen = generator();
console.log(gen.next()); // { value: '第一个值', done: false }
console.log(gen.next("传入第一个值")); // { value: '第二个值', done: false }
console.log(gen.next("传入第二个值")); // { value: '结束', done: true }

使用 Generator 函数处理异步操作

javascript
// 使用 Generator 函数处理异步操作
function* fetchDataGenerator() {
  try {
    const user = yield fetchData("https://api.example.com/user/1");
    console.log("用户:", user);

    const posts = yield fetchData(
      `https://api.example.com/posts?userId=${user.id}`
    );
    console.log("帖子:", posts);

    const comments = yield fetchData(
      `https://api.example.com/comments?postId=${posts[0].id}`
    );
    console.log("评论:", comments);
  } catch (error) {
    console.error("错误:", error);
  }
}

// 手动执行 Generator 函数
function runGenerator(generator) {
  const gen = generator();

  function iterate(iteration) {
    if (iteration.done) return iteration.value;

    const promise = iteration.value;
    return promise
      .then((value) => iterate(gen.next(value)))
      .catch((error) => iterate(gen.throw(error)));
  }

  return iterate(gen.next());
}

// 运行 Generator 函数
runGenerator(fetchDataGenerator);

异步编程的应用场景

1. 网络请求

javascript
// 使用 fetch API 进行网络请求
async function fetchUsers() {
  try {
    const response = await fetch("https://api.example.com/users");
    const users = await response.json();
    console.log("用户列表:", users);
    return users;
  } catch (error) {
    console.error("获取用户失败:", error);
    throw error;
  }
}

fetchUsers();

2. 文件操作

javascript
// 使用 FileReader 读取文件
function readFile(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = function () {
      resolve(reader.result);
    };
    reader.onerror = function () {
      reject(new Error("文件读取失败"));
    };
    reader.readAsText(file);
  });
}

// 使用示例
const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", async function (e) {
  const file = e.target.files[0];
  if (file) {
    try {
      const content = await readFile(file);
      console.log("文件内容:", content);
    } catch (error) {
      console.error("错误:", error);
    }
  }
});

3. 定时器

javascript
// 封装定时器为 Promise
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// 使用示例
async function countdown() {
  for (let i = 5; i > 0; i--) {
    console.log(i);
    await delay(1000);
  }
  console.log("开始!");
}

countdown();

4. 数据库操作

javascript
// 模拟数据库操作
function queryDatabase(sql) {
  return new Promise((resolve, reject) => {
    // 模拟异步查询
    setTimeout(() => {
      if (sql.includes("SELECT")) {
        resolve([
          { id: 1, name: "John" },
          { id: 2, name: "Jane" },
        ]);
      } else {
        reject(new Error("查询失败"));
      }
    }, 500);
  });
}

// 使用示例
async function getUsers() {
  try {
    const users = await queryDatabase("SELECT * FROM users");
    console.log("用户:", users);
  } catch (error) {
    console.error("错误:", error);
  }
}

getUsers();

5. 动画效果

javascript
// 封装动画为 Promise
function animate(element, properties, duration) {
  return new Promise((resolve) => {
    const start = performance.now();
    const originalProperties = {};

    // 保存原始属性
    for (const prop in properties) {
      originalProperties[prop] = parseFloat(getComputedStyle(element)[prop]);
    }

    function updateAnimation(currentTime) {
      const elapsed = currentTime - start;
      const progress = Math.min(elapsed / duration, 1);

      // 应用动画
      for (const prop in properties) {
        const startValue = originalProperties[prop];
        const endValue = properties[prop];
        const currentValue = startValue + (endValue - startValue) * progress;
        element.style[prop] = currentValue + (prop === "opacity" ? "" : "px");
      }

      if (progress < 1) {
        requestAnimationFrame(updateAnimation);
      } else {
        resolve();
      }
    }

    requestAnimationFrame(updateAnimation);
  });
}

// 使用示例
async function runAnimation() {
  const box = document.getElementById("box");

  await animate(box, { left: 200, opacity: 0.5 }, 1000);
  console.log("第一个动画完成");

  await animate(box, { top: 100, opacity: 1 }, 1000);
  console.log("第二个动画完成");

  await animate(box, { left: 0, top: 0 }, 1000);
  console.log("所有动画完成");
}

runAnimation();

异步编程的最佳实践

1. 错误处理

  • 回调函数:使用错误优先回调模式
  • Promise:使用 .catch() 捕获错误
  • async/await:使用 try/catch 捕获错误
javascript
// 错误处理示例
// 回调函数
function fetchDataCallback(url, callback) {
  setTimeout(() => {
    if (url.includes("error")) {
      callback(new Error("请求失败"));
    } else {
      callback(null, { data: "成功" });
    }
  }, 1000);
}

fetchDataCallback("https://api.example.com/data", (error, data) => {
  if (error) {
    console.error("错误:", error);
  } else {
    console.log("数据:", data);
  }
});

// Promise
function fetchDataPromise(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes("error")) {
        reject(new Error("请求失败"));
      } else {
        resolve({ data: "成功" });
      }
    }, 1000);
  });
}

fetchDataPromise("https://api.example.com/data")
  .then((data) => {
    console.log("数据:", data);
  })
  .catch((error) => {
    console.error("错误:", error);
  });

// async/await
async function fetchDataAsync(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url.includes("error")) {
        reject(new Error("请求失败"));
      } else {
        resolve({ data: "成功" });
      }
    }, 1000);
  });
}

async function getData() {
  try {
    const data = await fetchDataAsync("https://api.example.com/data");
    console.log("数据:", data);
  } catch (error) {
    console.error("错误:", error);
  }
}

getData();

2. 避免阻塞

  • 避免在主线程上执行长时间运行的同步操作
  • 使用 Web Workers 处理密集计算
  • 使用异步 API 进行 I/O 操作

3. 并行执行

  • 使用 Promise.all() 并行执行多个独立的异步操作
  • 避免不必要的串行执行,提高性能
javascript
// 并行执行示例
async function getMultipleData() {
  // 串行执行(较慢)
  const start1 = performance.now();
  const data1 = await fetchData("https://api.example.com/data1");
  const data2 = await fetchData("https://api.example.com/data2");
  const data3 = await fetchData("https://api.example.com/data3");
  const end1 = performance.now();
  console.log("串行执行时间:", end1 - start1);

  // 并行执行(较快)
  const start2 = performance.now();
  const [data4, data5, data6] = await Promise.all([
    fetchData("https://api.example.com/data1"),
    fetchData("https://api.example.com/data2"),
    fetchData("https://api.example.com/data3"),
  ]);
  const end2 = performance.now();
  console.log("并行执行时间:", end2 - start2);
}

getMultipleData();

4. 取消异步操作

  • 使用 AbortController 取消 fetch 请求
  • 使用标志变量取消长时间运行的操作
javascript
// 取消 fetch 请求
const controller = new AbortController();
const signal = controller.signal;

fetch("https://api.example.com/data", { signal })
  .then((response) => response.json())
  .then((data) => console.log("数据:", data))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("请求被取消");
    } else {
      console.error("错误:", error);
    }
  });

// 取消请求
setTimeout(() => {
  controller.abort();
}, 1000);

// 使用标志变量取消操作
let isCancelled = false;

async function longRunningTask() {
  for (let i = 0; i < 1000000000; i++) {
    if (isCancelled) {
      console.log("操作被取消");
      return;
    }
    // 执行计算
  }
  console.log("操作完成");
}

longRunningTask();

// 取消操作
setTimeout(() => {
  isCancelled = true;
}, 1000);

5. 内存管理

  • 避免创建不必要的闭包
  • 及时清理定时器和事件监听器
  • 避免循环引用

6. 代码组织

  • 将异步操作封装为独立的函数
  • 使用模块化组织代码
  • 保持函数的单一职责

异步编程的常见问题

1. 回调地狱

问题:多个嵌套的回调函数导致代码可读性差。

解决方案:使用 Promise 或 async/await 替代嵌套回调。

2. 未处理的 Promise 拒绝

问题:Promise 被拒绝但没有被捕获,导致控制台错误。

解决方案:总是使用 .catch() 捕获 Promise 错误,或在 async 函数中使用 try/catch

3. 竞态条件

问题:多个异步操作的执行顺序不确定,导致结果不一致。

解决方案:使用适当的同步机制,如 Promise.all() 或 async/await 的顺序执行。

4. 内存泄漏

问题:异步操作导致的内存泄漏,如未清理的定时器或事件监听器。

解决方案:及时清理定时器、事件监听器和其他资源。

5. 超时处理

问题:异步操作没有设置超时,可能导致无限等待。

解决方案:为异步操作设置超时,使用 Promise.race() 实现。

javascript
// 设置超时
function withTimeout(promise, ms) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error("操作超时")), ms);
  });
  return Promise.race([promise, timeoutPromise]);
}

// 使用示例
async function fetchWithTimeout(url, timeout = 5000) {
  try {
    const response = await withTimeout(fetch(url), timeout);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("错误:", error);
    throw error;
  }
}

fetchWithTimeout("https://api.example.com/data", 3000);

6. 错误传播

问题:异步操作的错误没有正确传播,导致难以调试。

解决方案:确保错误在 Promise 链中正确传播,或在 async 函数中使用 try/catch

总结

异步编程是 JavaScript 中非常重要的一部分,它允许我们处理耗时操作而不阻塞主线程。JavaScript 提供了多种异步编程方式:

  1. 回调函数:最基本的异步编程方式,但容易导致回调地狱
  2. Promise:ES6 引入的异步编程解决方案,解决了回调地狱问题
  3. async/await:ES2017 引入的语法糖,基于 Promise,使异步代码更像同步代码
  4. 事件监听:通过监听事件触发来执行回调
  5. Generator 函数:ES6 引入的特殊函数,可以暂停和恢复执行

在实际开发中,我们应该根据具体场景选择合适的异步编程方式:

  • 简单的异步操作可以使用回调函数
  • 复杂的异步操作链应该使用 Promise 或 async/await
  • 用户交互相关的异步操作可以使用事件监听

同时,我们应该遵循异步编程的最佳实践:

  • 正确处理错误
  • 避免阻塞主线程
  • 合理使用并行执行
  • 注意内存管理
  • 保持代码组织清晰

通过掌握异步编程,我们可以创建更响应、更高效的 JavaScript 应用。