JavaScript基础回顾(五):异步

18 阅读7分钟

异步编程

JavaScript 的事件循环与调用栈

1. JavaScript 的单线程模型

  • JavaScript 运行在浏览器或 Node.js 环境中,它是一个单线程的语言,这意味着它一次只能执行一个任务。
  • 单线程意味着不会有多个线程同时执行 JavaScript 代码,这有助于简化编程模型,但也导致了对异步编程的需求。

2. 调用栈(Call Stack)

  • 调用栈是一个特殊的数据结构,用于存储函数的上下文。
  • 当一个函数被调用时,它会被推入调用栈;当函数执行完毕后,它会从调用栈中弹出。
  • 调用栈遵循后进先出(LIFO)的原则。

3. 事件循环(Event Loop)

  • 事件循环是 JavaScript 的运行机制,它允许 JavaScript 引擎在单线程环境中处理异步操作。
  • 事件循环不断的检查调用栈是否为空,如果调用栈为空,它将从任务队列中取出任务并推入调用栈执行。
  • 任务队列中的任务通常是异步操作的结果,如定时器、网络请求的响应等。

4. 宏任务与微任务

  • 宏任务(Macro Tasks)与微任务(Micro Tasks)是事件循环处理任务的两种类型。
  • 宏任务包括:setTimeOut、setInterval、I/O、UI 渲染等。
  • 微任务包括:Promise.then、Promise.catch、async/await 等。
  • 微任务总是优先于宏任务执行。

回调函数与回调地狱

1. 回调函数(Callback)

  • 回调函数是一种将函数作为参数传递给另一个函数的技术,以便在某个事件或条件满足时执行。
  • 在异步编程中,回调函数常用语处理异步操作的完成。

2. 理解回调地狱(Callback Hell)

  • 回调地狱是指多层嵌套的回调函数,这使得代码难以阅读和维护。
  • 回调地狱通常发生在多个异步操作需要按顺序执行时。

3. 避免回调地狱

  • 使用现代 JavaScript 的 Promise 和 async/await 可以避免回调地狱。
  • 通过将异步代码转换为同步代码风格,可以提高代码的可读性和维护性。

通过 I/O 理解异步与非阻塞

在 JavaScript 中,当涉及到 I/O 操作,如网络请求或文件读写,这些操作通常涉及到与操作系统的交互。由于这些操作可能需要等待外部资源(如从服务器获取数据或从硬盘读取文件),他们可能会花费比 CPU 计算更长的时间。在传统的多线程编程中,如果这些操作使用是同步的,那么执行他们的线程可能会被阻塞,直到操作完成。但在 JavaScript 中,这些 I/O 操作被设计为异步和非阻塞的,这意味着:

  1. 操作系统介入:当 JavaScript 代码发起一个 I/O 请求时,这个请求会被发送到操作系统,操作系统负责管理对外部资源的访问。
  2. 非阻塞行为:在 JavaScript 中,一旦请求被发送,主线程(即 JavaScript 的执行线程)不会等待这个请求完成。相反,它会继续执行后续的代码。这是因为 JavaScript 运行时环境将请求委托给操作系统,并注册一个回调函数,该回调函数将在请求完成时被调用。
  3. 操作系统的异步处理:操作系统有能力异步处理请求。这意味着它可以同时处理多个请求,而不需要为每个请求阻塞一个线程。操作系统会在后台执行这些操作,并在操作完成时通知 JavaScript 运行时环境。
  4. 事件通知:当请求完成时,操作系统会通知 JavaScript 运行时环境。这时,请求的回调函数会被放入事件循环的任务队列中,等待执行。
  5. 回调函数执行:当事件循环的调用栈清空,且没有其他更高优先级的任务时,事件循环会从任务队列中取出回调函数并放入调用栈中执行。这时,JavaScript 代码可以处理请求的结果。

通过这种方式,JavaScript 能够在不阻塞主线程的情况下执行 I/O 操作,从而保证了程序的响应性和效率。这种模式允许 JavaScript 程序同时处理多个 I/O 操作,而不需要为每个请求分配一个单独的线程,这在单线程环境中尤为重要。

Promise

  • Promise 是 ES6 新增的特性,是一个代表未来将会完成的异步操作的对象。Promise 有三种状态:Pending(进行中)、Fulfilled(已成功)、Rejected(已失败)。
  • Promise 的实例是通过 new Promise 来创建 。构造函数的参数是一个执行器函数(executor function),函数将在创建后立即执行。执行器函数通常包含异步操作,并在操作完成时调用 resolve 或 reject 函数。
  • Promise 通过 then、catch、finally 添加回调函数:
    • then:.then(resolve, reject);一般用于指定 Promise 成功时的回调函数,返回一个新的 Promise。
    • catch:.catch(reject);用于指定 Promise 失败时的回调函数,返回一个新的 Promise。
    • finally:.finally();无论成功或失败都会调用,返回一个新的 Promise。
  • Promise.all():接收一个 Promise 数组作为参数,只有当所有 Promise 都成功时,返回的 Promise 才会成功,resolve 返回的结果是所有 Promise 的结果的数组。如果传入的 Promise 有任何一个失败了,Promise.all 返回的 Promise 会立即被拒绝,拒绝的原因是第一个失败的 Promise 的拒绝值。
  • Promise.race():同样接受一个 Promise 数组作参数,但只要有一个 Promise 成功或失败,将以次作为结果返回。

手写 Promise

  • queueMicrotask 是一个 Web API,用途是将一个函数排入微任务队列,内置 Promise 是由次实现的。
  • 手写版没有做类型检查、循环引用等处理,方法也没有全部实现,仅为学习。
class CustomPromise {
  constructor(excutor) {
    // 状态
    this.state = 'pending'
    // 值
    this.value = undefined
    // onFullfilled 回调数组
    this.onFulfilledCallbacks = []
    // onRejected 回调数组
    this.onRejectedCallbacks = []

    try {
      // 定义时立即执行执行器函数
      excutor(this._resolve, this._reject)
    } catch (err) {
      this._reject(err)
    }
  }

  _resolve = (value) => {
    // 模拟真实的 Promise 行为,使用 queueMicrotask 包装 _resolve 逻辑
    if (this.state === 'pending') {
      queueMicrotask(() => {
        this.state = 'fulfilled'
        this.value = value
        this.onFulfilledCallbacks.forEach((fn) => fn())
      })
    }
  }

  _reject = (error) => {
    if (this.state === 'pending') {
      // 模拟真实的 Promise 行为,使用 queueMicrotask 包装 _reject 逻辑
      queueMicrotask(() => {
        this.state = 'rejected'
        this.value = error
        this.onRejectedCallbacks.forEach((fn) => fn())
      })
    }
  }

  then(onFulfilled, onRejected) {
    // 处理默认
    if (typeof onFulfilled !== 'function') {
      onFulfilled = (value) => value
    }
    if (typeof onRejected !== 'function') {
      onRejected = (error) => {
        throw error
      }
    }

    let promise = new CustomPromise((resolve, reject) => {
      if (this.state === 'pending') {
        // 如果是 pending,加入队列
        this.onFulfilledCallbacks.push(() => {
          queueMicrotask(() => {
            try {
              let result = onFulfilled(this.value)
              resolve(result)
            } catch (err) {
              reject(err)
            }
          })
        })
        this.onRejectedCallbacks.push(() => {
          queueMicrotask(() => {
            try {
              let result = onRejected(this.value)
              resolve(result)
            } catch (err) {
              reject(err)
            }
          })
        })
      } else if (this.state === 'fulfilled') {
        // fulfilled 同步结果,向后传
        queueMicrotask(() => {
          try {
            let result = onFulfilled(this.value)
            resolve(result)
          } catch (err) {
            reject(err)
          }
        })
      } else if (this.state === 'rejected') {
        // rejected 同步结果,向后传
        queueMicrotask(() => {
          try {
            let result = onRejected(this.value)
            resolve(result)
          } catch (err) {
            reject(err)
          }
        })
      }
    })

    return promise
  }

  catch(onRejected) {
    // 只有 onRejected 的语法糖
    return this.then(null, onRejected)
  }

  finally(onFinally) {
    return this.then(
      (value) => CustomPromise.resolve(onFinally()).then(() => value),
      (error) =>
        CustomPromise.resolve(onFinally()).then(() => {
          throw error
        })
    )
  }

  static resolve(value) {
    if (value instanceof CustomPromise) {
      return value
    }
    return new CustomPromise((resolve) => {
      resolve(value)
    })
  }

  static reject(error) {
    return new CustomPromise((resolve, reject) => {
      reject(error)
    })
  }

  static all(promises) {
    return new CustomPromise((resolve, reject) => {
      const result = []
      let resolvedCount = 0

      if (promises.length === 0) {
        resolve(result)
        return
      }

      promises.forEach((promise, index) => {
        promise.then((value) => {
          result[index] = value
          resolvedCount++
          if (resolvedCount === promises.length) {
            resolve(result)
          }
        }),
          (error) => {
            reject(error)
          }
      })
    })
  }

  static race(promises) {
    return new CustomPromise((resolve, reject) => {
      let isResolved = false
      promises.forEach((promise) => {
        promise.then(
          (value) => {
            if (!isResolved) {
              resolve(value)
              isResolved = true
            }
          },
          (error) => {
            if (!isResolved) {
              reject(error)
              isResolved = true
            }
          }
        )
      })
    })
  }
}

封装可取消的 Promise

function makeCancelable(promise) {
  let isCanceled = false
  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then((val) => (isCanceled ? reject({ isCanceled, value: val }) : resolve(val)))
    promise.catch((error) => (isCanceled ? reject({ isCanceled, error }) : reject(error)))
  })
  return {
    promise: wrappedPromise,
    cancel() {
      isCanceled = true
    }
  }
}

async/await

  • async 函数是使用 async 关键字声明的函数,它会返回一个 Promise 对象。任何在 async 函数中执行的一步操作都会返回一个 Promise。
  • await 关键字只能在 async 函数内部使用。await 后面通常会跟着一个 Promise,它会暂停 async 函数的执行,直到 Promise 解决或拒绝。
  • 在 async 函数中,可以使用 try...catch 来处理 await 调用的 Promise 可能产生的错误。
  • 如果 async 函数正常执行结束,会返回一个解决状态的 Promise;如果 async 函数中抛出错误,它会返回一个拒绝状态的 Promise。

在 useEffect 中使用 async 函数

useEffect(() => {
  async function fetchData() {
    try {
      const data = await fetchSomeData();
      // 使用数据进行一些操作
    } catch (error) {
      console.error('Failed to fetch data:', error);
    }
  }

  fetchData();
}, [dependency]); // 确保将依赖项数组传递给 useEffect

参考