Published on

JavaScript 异步编程详解

Authors
  • avatar
    Name
    青雲
    Twitter

引言

想象你在一家咖啡店,你下了订单,而后静静地坐着等咖啡制作。这个等待过程,你可以选择读书、刷手机,亦或静静地发呆。而这,便是异步编程在现实生活中的一个缩影:你的订单已经提交给了"后台"(咖啡师),而你可以继续你的"任务"(任何其他活动),不必同步地站在柜台前等待咖啡做好。(耐心和等待一种美德,🐶)

在JavaScript开发中,异步编程是处理耗时任务的高效方式,它让我们的代码在等待某个任务完成时可以去执行其他任务,不必像同步编程一样闲置等待。

异步编程的演进

JavaScript 的异步编程经历了一系列的发展和演进,从早期的回调函数 (Callback) 到现代的Async/Await。每一个阶段的新特性都解决了之前方法的某些缺点和局限性,使编写异步代码变得更加干净和直观。

回调函数 (Callback)

简介

回调函数是最早的异步编程解决方案。使用回调函数,我们可以在异步操作完成后,调用指定的函数来处理结果。

function fetchData(callback) {
  setTimeout(() => {
    callback("Data fetched");
  }, 1000);
}

function processData(data) {
  console.log(data);
}

// 调用示例
fetchData(processData);

缺点

  1. 回调地狱 (Callback Hell):当多个异步操作需要顺序执行时,嵌套的回调函数会导致代码变得难以阅读和维护。
function step1(callback) {
  setTimeout(() => {
    console.log("Step 1 complete");
    callback();
  }, 1000);
}

function step2(callback) {
  setTimeout(() => {
    console.log("Step 2 complete");
    callback();
  }, 1000);
}

function step3(callback) {
  setTimeout(() => {
    console.log("Step 3 complete");
    callback();
  }, 1000);
}

step1(() => {
  step2(() => {
    step3(() => {
      console.log("All steps complete");
    });
  });
});
  1. 错误处理复杂:每个回调函数都需要处理可能发生的错误,使代码变得更加杂乱。

Promise 对象

简介

为了解决回调函数的嵌套问题,ECMAScript 2015(ES6)引入了Promise。它是一种更优雅的异步编程模型,链式调用使得代码更加清晰和可读。Promise 表示一个将来可能完成或失败的事件及其结果值。

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
}

function processData(data) {
  console.log(data);
}

// 调用示例
fetchData()
  .then(processData)
  .catch(error => console.error(error));

优点

  1. 链式调用:Promise 可以通过 .then.catch 方法进行链式调用,代码更加线性化、易读。
  2. 统一的错误处理:Promise 提供了 .catch 方法来进行统一的错误处理,简化了错误处理逻辑。

Generator 函数

简介

Generator 函数是ES6引入的一种异步编程模型。Generator函数可以通过 yield 暂停执行,并通过 next 方法恢复执行,是异步编程的一种更细粒度的控件方式。Generator函数配合Promiseco库,可以写出类似同步的异步代码。

function* fetchData() {
  const data = yield new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
  console.log(data);
}

// 调用示例
const generator = fetchData();
const promise = generator.next().value;
promise.then(data => generator.next(data));

优点

  1. 暂停和恢复执行:Generator 函数可以在yield语法处暂停执行,并在任意时刻恢复执行,使得流程控制更加灵活。
  2. 配合Promise:Generator函数和Promise结合,能够写出结构化更加清晰的异步代码。

使用 co 库简化 Generator 函数调用

const co = require('co');

function* fetchData() {
  const data = yield new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
  console.log(data);
}

// 调用示例
co(fetchData);

Async/Await 函数

简介

async/await 是ES2017 (ES8) 引入的语法糖,它建立在Promise之上,使异步代码更加简洁和像同步代码。async 声明函数返回一个Promise,await 关键字暂停函数执行,直到Promise处理完成。

async function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
}

async function processData() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

// 调用示例
processData();

处理错误

在使用 async/await 时,可以使用 try/catch 语句来捕获和处理错误。这使得错误处理更加直观和集成。

async function fetchData() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject("Error occurred"), 1000);
  });

  try {
    let result = await promise;
    console.log(result); // 不会执行,因为 promise 已被 reject
  } catch (error) {
    console.error(error); // 输出: Error occurred
  }
}

fetchData();

优点

  1. 代码更加简洁:async/await 让异步代码看起来像同步代码,清晰而易读。
  2. 统一的错误处理:try/catch 可以用来处理所有的异步错误,逻辑更加清晰。

总结

JavaScript 异步编程经历了回调函数、Promise、Generator 函数和 Async/Await 函数的演进,每一个阶段的新特性都解决了一些之前方法的缺点。

  1. 回调函数 (Callback) 是最基础的异步编程方式,但容易导致回调地狱和复杂的错误处理。
  2. Promise 提供了一种更优雅的方式处理异步操作,通过链式调用和统一的错误处理,提高了代码的可读性。
  3. Generator 函数 通过 yield 语法实现异步控制,使得流程控制更加灵活,配合 co 库,可以写出类似同步的异步代码。
  4. Async/Await 函数 是现代异步编程的标准,建立在 Promise 之上,使异步代码更加简洁、清晰、易读。

Promise详解

Promise 是一个对象,用于表示一个异步操作的最终完成(或失败)及其结果值。Promise 可以处于以下三种状态之一:

  1. 待定(Pending):初始状态,既不是成功也不是失败。
  2. 已完成(Fulfilled):操作成功完成。
  3. 已拒绝(Rejected):操作失败。

Promise 构造函数接受一个执行器函数作为参数,执行器函数接收两个参数:

  1. resolve 函数:在操作成功时调用,将 Promise 的状态置为“已完成”。
  2. reject 函数:在操作失败时调用,将 Promise 的状态置为“已拒绝”。

示例

const myPromise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("Operation was successful!");
    } else {
      reject("Operation failed!");
    }
  }, 1000);
});

在这个例子中,myPromise 是一个 Promise 对象,模拟了一个异步操作。这个操作等待 1 秒后,根据成功或失败,调用 resolvereject

为了处理 Promise 解析后的结果,可以使用 .then() 方法,它接受两个回调函数:一个用于处理成功情况,另一个用于处理失败情况。可以链式调用 .then().catch() 方法进行连续处理。

myPromise
  .then((result) => {
    console.log(result); // 输出: Operation was successful!
  })
  .catch((error) => {
    console.error(error); // 输出: Operation failed!
  });

Promise 链

一个重要的特性是可以链式调用 .then() 方法。这使得多个异步操作可以串联起来,形成一个异步操作的执行序列。

const fetchData = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Data fetched");
  }, 1000);
});

fetchData
  .then((result) => {
    console.log(result); // 输出: Data fetched
    return "Processing data";
  })
  .then((result) => {
    console.log(result); // 输出: Processing data
    return "Data processed";
  })
  .catch((error) => {
    console.error(error);
  });
  1. fetchData Promise 被解析并输出 “Data fetched”。
  2. 第一个 .then() 返回新的字符串,传递给下一个 .then()
  3. 第二个 .then() 输出 “Processing data” 。

处理多个异步操作

Promise.all()

Promise.all() 接受一个 Promise 数组,当这个数组中的所有 Promise 都已完成时,返回一个 Promise。这个新的 Promise 的结果是一个包含所有已完成 Promise 结果的数组。如果任意一个 Promise 被拒绝,新 Promise 就会立即被拒绝。

const promise1 = new Promise((resolve) => {
  setTimeout(() => resolve("Promise 1 resolved"), 1000);
});
const promise2 = new Promise((resolve) => {
  setTimeout(() => resolve("Promise 2 resolved"), 2000);
});

Promise.all([promise1, promise2])
  .then((results) => {
    console.log(results); // 输出: [ 'Promise 1 resolved', 'Promise 2 resolved' ]
  })
  .catch((error) => console.error(error));

在这个例子中,promise1promise2 是两个异步操作。Promise.all 等待所有操作完成,并返回一个包含两个操作结果的数组。

Promise.race()

Promise.race() 也是接受一个 Promise 数组,但其返回的 Promise 等待数组中的任意一个 Promise 完成或被拒绝,以先发生者为准。

const fastPromise = new Promise((resolve) => {
  setTimeout(() => resolve("Fast Promise resolved"), 1000);
});
const slowPromise = new Promise((resolve) => {
  setTimeout(() => resolve("Slow Promise resolved"), 3000);
});

Promise.race([fastPromise, slowPromise])
  .then((result) => {
    console.log(result); // 输出: Fast Promise resolved
  })
  .catch((error) => console.error(error));

实现简化版的 Promise

步骤 1:设置初始状态

首先,我们定义 Promise 的初始状态为 "pending"。

class MyPromise {
  constructor(executor) {
    this.state = "pending";
    this.value = undefined;
    this.reason = undefined;

    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state === "pending") {
        this.state = "fulfilled";
        this.value = value;
        this.onFulfilledCallbacks.forEach((cb) => cb(this.value));
      }
    };

    const reject = (reason) => {
      if (this.state === "pending") {
        this.state = "rejected";
        this.reason = reason;
        this.onRejectedCallbacks.forEach((cb) => cb(this.reason));
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === "fulfilled") {
      onFulfilled(this.value);
    }

    if (this.state === "rejected") {
      onRejected(this.reason);
    }

    if (this.state === "pending") {
      this.onFulfilledCallbacks.push(onFulfilled);
      this.onRejectedCallbacks.push(onRejected);
    }
  }
}
  1. 状态管理:我们使用 this.statethis.valuethis.reason 来表示 Promise 的当前状态、成功的值和失败的原因。
  2. 回调队列:this.onFulfilledCallbacksthis.onRejectedCallbacks 用来存储成功和失败的回调函数。
  3. resolve 和 reject 函数:通过改变 this.state 来更新 Promise 的状态,并调用对应的回调函数队列。
  4. 执行器函数:executor 函数会立即执行,并传入 resolvereject 函数。在执行过程中,如果抛出错误,则直接调用 reject

步骤 2:完善链式调用

为了支持链式调用,我们需要在 then 方法中返回一个新的 Promise。

class MyPromise {
  // ...前面的代码...

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (value) => value;
    onRejected = typeof onRejected === "function" ? onRejected : (reason) => { throw reason; };

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === "fulfilled") {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      if (this.state === "rejected") {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      if (this.state === "pending") {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
      }
    });

    return promise2;
  }
}

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError("Chaining cycle detected for promise"));
  }

  let called = false;
  if ((x !== null && typeof x === "object") || typeof x === "function") {
    try {
      const then = x.then;
      if (typeof then === "function") {
        then.call(x,
          (y) => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          });
      } else {
        resolve(x);
      }
    } catch (error) {
      if (called) return;
      called = true;
      reject(error);
    }
  } else {
    resolve(x);
  }
}

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success");
  }, 1000);
});

p1.then((data) => {
  console.log(data);
  return new MyPromise((resolve, reject) => {
    setTimeout(() => {
      resolve("Another success");
    }, 1000);
  });
})
.then((data) => {
  console.log(data);
})
.catch((error) => {
  console.error(error);
});
  1. 返回新 Promise:在 then() 方法中,我们返回一个新的 Promise。
  2. 处理回调:根据当前状态,调用相应的回调函数,并传入 resolvePromise 函数处理返回值。
  3. resolvePromise 函数:这个函数非常关键,它处理 thenable(即有 then 方法的对象)或返回值,确保 Promise 遵循规范。

完整 Promise 处理

为了使我们的 Promise 更加完整,我们还需要处理:

  1. .catch() 方法:JavaScript 的 Promise 提供了 .catch() 方法,用于捕获错误。
  2. 静态方法:例如 Promise.resolvePromise.reject
  3. 多态并发:例如 Promise.allPromise.race

.catch() 方法

class MyPromise {
  // ...前面的代码...

  catch(onRejected) {
    return this.then(null, onRejected);
  }
}

Promise.resolve 和 Promise.reject

class MyPromise {
  // ...前面的代码...

  static resolve(value) {
    return new MyPromise((resolve) => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

Promise.all 和 Promise.race

  class MyPromise {
  // ...前面的代码...

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      let resultArray = [];
      let count = 0;
      promises.forEach((p, index) => {
        MyPromise.resolve(p).then((value) => {
          resultArray[index] = value;
          count += 1;
          if (count === promises.length) {
            resolve(resultArray);
          }
        }).catch(reject);
      });
    });
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      promises.forEach((p) => {
        MyPromise.resolve(p).then(resolve).catch(reject);
      });
    });
  }
}

面试实战

1. 什么是异步编程,为什么在 JavaScript 中需要异步编程?

问题描述: 解释什么是异步编程,以及为什么在 JavaScript 中需要使用异步编程。

答案: 异步编程是一种编程范式,它允许在等待一个任务完成时继续执行其他任务,而不是等待该任务完成后再开始下一个任务。比如,异步编程允许在等待一个 HTTP 请求完成时继续执行其他代码。 在 JavaScript 中需要异步编程的原因主要有:

  1. 单线程环境:JavaScript 是单线程的,这意味着同一时间只能执行一个任务。通过异步编程,长时间运行的任务不会阻塞主线程,从而保持用户界面的响应性。
  2. IO操作:许多操作(如网络请求、文件读写)是 IO 密集型的,需要等待一定的时间才能完成。异步编程可以高效地处理这些操作。

2. 解释什么是回调函数,并举一个使用回调函数的例子。

问题描述: 解释什么是回调函数,并提供一个使用回调函数的示例。

答案: 回调函数是作为参数传递给另一个函数的函数,这个函数会在未来的某个时间点执行。

示例代码:

function fetchData(callback) {
  setTimeout(() => {
    callback("Data fetched");
  }, 1000);
}

function processData(data) {
  console.log(data);
}

// 调用示例
fetchData(processData);

在这个例子中,fetchData 函数接受一个回调函数参数 callback。在 1 秒延迟之后,callback 函数被调用并输出 "Data fetched"。

3. 解释什么是“回调地狱”(Callback Hell),并展示如何通过 Promise 解决这个问题。

问题描述: 解释什么是回调地狱,并提供一个通过 Promise 解决回调地狱问题的示例。

答案: 回调地狱是指当多个回调函数嵌套时,代码变得难以阅读和维护的状态。它通常出现在需要顺序执行多个异步操作时。

示例代码(回调地狱):

function step1(callback) {
  setTimeout(() => {
    console.log("Step 1 complete");
    callback();
  }, 1000);
}

function step2(callback) {
  setTimeout(() => {
    console.log("Step 2 complete");
    callback();
  }, 1000);
}

function step3(callback) {
  setTimeout(() => {
    console.log("Step 3 complete");
    callback();
  }, 1000);
}

// 回调地狱
step1(() => {
  step2(() => {
    step3(() => {
      console.log("All steps complete");
    });
  });
});

通过 Promise 解决回调地狱:

function step1() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Step 1 complete");
      resolve();
    }, 1000);
  });
}

function step2() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Step 2 complete");
      resolve();
    }, 1000);
  });
}

function step3() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Step 3 complete");
      resolve();
    }, 1000);
  });
}

// 链式调用
step1()
  .then(step2)
  .then(step3)
  .then(() => {
    console.log("All steps complete");
  });

4. Promise 是什么?请解释 Promise 的基本使用方法。

问题描述: 解释什么是 Promise 并展示 Promise 的基本使用方法。

答案: Promise 是一个对象,用于表示一个异步操作的最终完成(或失败)及其结果值。它有三种状态:

  • Pending(待定)
  • Fulfilled(已完成)
  • Rejected(已拒绝)

示例代码:

const myPromise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve("Operation was successful!");
    } else {
      reject("Operation failed!");
    }
  }, 1000);
});

myPromise
  .then((result) => {
    console.log(result); // 输出: "Operation was successful!"
  })
  .catch((error) => {
    console.error(error); // 处理错误
  });

5. 使用 Promise.all 和 Promise.race,分别解释它们的作用并提供代码示例。

问题描述: 解释 Promise.allPromise.race 的作用,并提供代码示例。

答案: Promise.allPromise.race 是用于处理多个 Promise 的方法。

  • Promise.all:等待所有 Promise 完成,并返回一个新的 Promise,该 Promise 的结果是由所有 Promise 结果组成的数组。如果任意一个 Promise 被拒绝,则新的 Promise 被立即拒绝。

示例代码 (Promise.all):

const promise1 = new Promise((resolve) => {
  setTimeout(() => resolve("Promise 1 resolved"), 1000);
});

const promise2 = new Promise((resolve) => {
  setTimeout(() => resolve("Promise 2 resolved"), 2000);
});

Promise.all([promise1, promise2])
  .then((results) => {
    console.log(results); // 输出: [ 'Promise 1 resolved', 'Promise 2 resolved' ]
  })
  .catch((error) => console.error(error));
  • Promise.race:等待第一个完成的 Promise,并返回一个新的 Promise,该 Promise 的结果是第一个完成的 Promise 的结果。如果第一个完成的 Promise 被拒绝,则新的 Promise 被立即拒绝。

示例代码 (Promise.race):

const fastPromise = new Promise((resolve) => {
  setTimeout(() => resolve("Fast Promise resolved"), 1000);
});

const slowPromise = new Promise((resolve) => {
  setTimeout(() => resolve("Slow Promise resolved"), 3000);
});

Promise.race([fastPromise, slowPromise])
  .then((result) => {
    console.log(result); // 输出: "Fast Promise resolved"
  })
  .catch((error) => console.error(error));

6. 解释什么是 async/await,并展示其基本用法。

问题描述: 解释 async/await 的作用,并提供基本用法的代码示例。

答案: async/await 是基于 Promise 的语法糖,使得异步代码看起来像同步代码,从而提高代码的可读性和简洁性。async 用于声明一个异步函数,await 用于暂停异步函数的执行,直到 Promise 处理完成。

示例代码:

// 声明异步函数
async function fetchData() {
  let promise = new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });


  // 使用 await 等待 Promise 处理完成
  let result = await promise;
  console.log(result); // 输出: "Data fetched"
}

// 调用异步函数
fetchData();

7. 如何使用 try/catch 进行 async/await 的错误处理?

问题描述: 解释如何通过 try/catch 来处理 async/await 中的错误。

答案: 通过 try/catch 语句可以捕获和处理 async/await 中的错误。将可能抛出错误的 await 表达式放入 try 块中,如果 Promise 被拒绝,则会抛出异常并执行 catch 块中的代码。

示例代码:

async function fetchData() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("Error occurred");
    }, 1000);
  });

  try {
    let result = await promise;
    console.log(result); // 不会执行,因为 Promise 已被拒绝
  } catch (error) {
    console.error(error); // 输出: "Error occurred"
  }
}

// 调用异步函数
fetchData();

8. 如何并行处理多个 async/await 函数?

问题描述: 解释如何通过 Promise.all 并行处理多个 async/await 函数,并提供代码示例。

答案: 可以结合 Promise.allasync/await 并行处理多个异步操作。这将显著提高性能,因为多个异步操作将同时进行,而不是依次执行。

示例代码:

async function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: "John Doe" });
    }, 1000);
  });
}

async function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { userId, title: "Post 1" },
        { userId, title: "Post 2" },
      ]);
    }, 1000);
  });
}

async function displayData() {
  try {
    const [user, posts] = await Promise.all([fetchUser(), fetchPosts(1)]);
    console.log(`User: ${user.name}`);
    console.log("Posts:", posts);
  } catch (error) {
    console.error("Error fetching data", error);
  }
}

// 调用示例
displayData();

解释

  1. 并行执行:Promise.all([fetchUser(), fetchPosts(1)])同时执行fetchUserfetchPosts两个异步操作。
  2. 等待完成:await将等待所有 Promise 完成,并返回结果数组[user, posts]
  3. 捕获错误:如果任意一个 Promise 被拒绝,控制将进入catch块处理错误。

9. 如何在 async/await 中创建延迟?

问题描述: 解释如何在 async/await 中创建延迟,并提供代码示例。

答案: 可以使用一个返回 Promise 的 setTimeout 函数来实现延迟,然后配合 await 使用。

示例代码:

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function example() {
  console.log("Waiting...");
  await delay(2000); // 延迟 2 秒
  console.log("Done");
}

// 调用示例
example();

10. 如何处理多个并发请求,并在第一个请求完成时取消其余请求?

问题描述: 解释如何使用 Promise.race 处理多个并发请求,并在第一个请求完成时取消其余请求。

答案: 可以使用 Promise.race 来处理多个请求,只要其中一个完成,就立即返回结果,并取消其他请求。

示例代码:

async function fetchWithTimeout(url, timeout) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error("Timeout")), timeout);

    fetch(url)
      .then((response) => response.json())
      .then((data) => {
        clearTimeout(timer);
        resolve(data);
      })
      .catch((err) => {
        clearTimeout(timer);
        reject(err);
      });
  });
}

async function handleRequests() {
  const url1 = "https://jsonplaceholder.typicode.com/posts/1";
  const url2 = "https://jsonplaceholder.typicode.com/posts/2";
  const timeout = 5000; // 超时时间 5 秒

  try {
    const result = await Promise.race([fetchWithTimeout(url1, timeout), fetchWithTimeout(url2, timeout)]);
    console.log("First request completed:", result);
  } catch (error) {
    console.error("Error:", error);
  }
}

// 调用示例
handleRequests();

11. async/await 与 Generator 函数相比有哪些优点?

问题描述: 解释 async/await 与 Generator 函数相比有哪些优点。 答案: async/await 与 Generator 函数都是用于处理异步操作的方案,但 async/await 具有以下优点:

  1. 语法更简洁:async/await更接近同步代码,语法简单、可读性强。
  2. 内建 Promise 支持:async/await内置对 Promise 的支持,不需要外部库(如co)来运行。
  3. 更简单的错误处理:async/await使用try/catch来处理错误,与同步代码的错误处理方式一致。

示例代码 (Generator 函数):

const co = require('co');

function* fetchData() {
  const data = yield new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
  console.log(data);
}

// 调用示例
co(fetchData);

示例代码 (async/await):

async function fetchData() {
  const data = await new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
  console.log(data);
}

// 调用示例
fetchData();

12. 笔试综合题

题目描述

你正在开发一个自动任务调度系统,系统需要执行一系列的异步任务,其中一些任务需要串行执行,另一些任务可以并行执行。每个任务执行都有一定的成功率,任务失败需要进行重试,最多重试三次。任务执行结果需要保存到一个日志中。

具体要求

  1. 实现一个模拟异步任务的函数performTask(taskName, successRate, duration),该函数返回一个 Promise,模拟任务执行。
  2. 实现一个TaskScheduler类,包含以下方法:
    • addTask(taskName, successRate, duration, parallel):添加任务到调度器队列,parallel表示任务是否并行。
    • run():按照顺序执行任务队列,并记录每个任务的执行结果(成功或失败)。
  3. 实现一个日志系统,记录任务执行的开始时间、结束时间以及结果(成功或失败)。

示例如下

任务模拟函数:

function performTask(taskName, successRate, duration) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = Math.random() < successRate;
      if (success) {
        resolve(`${taskName} completed successfully`);
      } else {
        reject(`${taskName} failed`);
      }
    }, duration);
  });
}

任务调度器类:

class TaskScheduler {
  constructor() {
    this.tasks = [];
    this.logger = [];
  }

  addTask(taskName, successRate, duration, parallel = false) {
    this.tasks.push({ taskName, successRate, duration, parallel, retries: 3 });
  }

  async runTask(task) {
    const { taskName, successRate, duration, retries } = task;
    for (let attempt = 1; attempt <= retries; attempt++) {
      const startTime = new Date();
      try {
        const result = await performTask(taskName, successRate, duration);
        this.logger.push({
          taskName,
          startTime,
          endTime: new Date(),
          attempt,
          result: 'success',
        });
        console.log(result);
        return;
      } catch (error) {
        this.logger.push({
          taskName,
          startTime,
          endTime: new Date(),
          attempt,
          result: 'failed',
        });
        console.error(error);
      }
    }
  }

  async run() {
    const serialTasks = this.tasks.filter(task => !task.parallel);
    const parallelTasks = this.tasks.filter(task => task.parallel);

    for (let task of serialTasks) {
      await this.runTask(task);
    }

    await Promise.all(parallelTasks.map(task => this.runTask(task)));

    this.printLog();
  }

  printLog() {
    console.log('Task Execution Log:');
    this.logger.forEach(log => {
      console.log(
        `Task: ${log.taskName} | Attempt: ${log.attempt} | Start: ${log.startTime.toISOString()} | End: ${log.endTime.toISOString()} | Result: ${log.result}`
      );
    });
  }
}

调用示例

const scheduler = new TaskScheduler();

scheduler.addTask('Task 1', 0.5, 1000, false); // 串行
scheduler.addTask('Task 2', 0.8, 500, true);  // 并行
scheduler.addTask('Task 3', 0.7, 1500, true); // 并行
scheduler.addTask('Task 4', 0.9, 1000, false); // 串行

scheduler.run();

预期输出

任务运行并记录日志,每个任务最多重试三次,串行任务按顺序执行,并行任务同时执行。日志系统记录每个任务的开始时间、结束时间和执行结果。

解释

  1. performTask:模拟异步任务的函数,接受任务名称、成功率和持续时间。返回一个 Promise,根据成功率决定任务是否完成。
  2. TaskScheduler:任务调度器类,包含添加任务、运行任务和记录日志的方法。
  3. addTask:添加任务到队列。
  4. runTask:尝试运行单个任务,最多重试三次。
  5. run:按顺序执行串行任务,并行执行并行任务,最终打印日志。
  6. printLog:打印任务执行的详细日志。