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:打印任务执行的详细日志。