跳到主要内容

异步编程✅

js 中异步编程有哪些模式 ?

答案
实现方式简介优缺点与适用场景
事件监听通过事件触发异步操作,常用于浏览器事件处理简单易用,但难以控制执行顺序
回调函数异步操作完成后调用回调函数处理结果,最基础的异步模式简单直观,易陷入回调地狱,难维护
Promise用对象封装异步结果,支持链式调用和错误处理解决回调嵌套,语法更清晰
Async/Await基于Promise的语法糖,使异步代码像同步代码一样书写可读性强,异常处理方便
Generator可暂停/恢复执行的函数,配合yield实现异步流程控制灵活但实现复杂,实际用得较少

说一下 promise ?

答案

Promise 是 ES6 引入的用于处理异步操作的对象。主要解决了异步编程中 "回调地狱"(Callback Hell)的问题。通过链式调用的方式,Promise 可以让异步代码的结构更加清晰、易于理解和维护。

  • 使用 new Promise((resolve, reject) => { ... }) 构造函数。在执行器函数中,异步操作成功时调用 resolve(value),失败时调用 reject(error)
  • 返回的 Promise 实例
    • then(onFulfilled, onRejected) 方法用于注册成功和失败的回调函数。
    • catch(onRejected) 方法用于注册失败的回调函数,等同于 then(null, onRejected)
    • finally(onFinally) 方法用于注册无论成功或失败都会执行的回调函数。
function asyncFunction () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Async operation completed')
}, 1000)
})
}

asyncFunction()
.then(result => {
console.log(result) // 'Async operation completed'
})
.catch(error => {
console.error('Error:', error)
}).finally(() => {
console.log('Operation finished')
})

Promise 的状态

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。 Promise 的状态一旦从 pending 变为 fulfilledrejected,就不会再改变。

Promise 内部是一个状态机,状态只能从 pending 迁移到 fulfilledrejected,且状态一旦改变不可逆。可以基于 Promise A+ 规范: 实现 Promise 模拟准,ES6 Promise 遵循此规范,并扩充了方法

Promise 对象和原型包含的方法属性如下

方法/属性描述
Promise.all(iterable)接收一个可迭代对象(如数组),返回一个 Promise,只有当所有 Promise 都成功时,才会成功,并返回一个包含所有成功值的数组。
Promise.allSettled(iterable)接收一个可迭代对象(如数组),返回一个 Promise,只有当所有 Promise 都已完成时,才会成功,并返回一个包含所有结果的数组。
Promise.any(iterable)接收一个可迭代对象(如数组),返回一个 Promise,只有当其中一个 Promise 成功时,才会成功,并返回该 Promise 的成功值。全部失败时返回一个 AggregateError
Promise.race(iterable)接收一个可迭代对象(如数组),返回第一个改变状态的 Promise,无论是成功还是失败。
Promise.reject(reason)返回一个被拒绝的 Promise,拒绝原因为 reason。
Promise.resolve(value)返回一个被解决的 Promise,解决原因为 value。
Promise.try(fn)尝试执行一个函数并返回一个 Promise
Promise.withResolvers返回一个 Promise 和两个函数 resolve 和 reject,用于手动控制 Promise 的状态
Promise.prototype.then(onFulfilled, onRejected)注册一个成功的回调函数和一个失败的回调函数,返回一个新的 Promise
Promise.prototype.catch()注册一个失败的回调函数,等同于 then(null, onRejected)
Promise.prototype.finally(onFinally)注册一个无论成功或失败都会执行的回调函数

示例代码

/* eslint-disable prefer-promise-reject-errors */
/* eslint-disable jest/no-conditional-expect */
describe('Promise API Demonstrations', () => {
  // 辅助函数,用于创建一个在延迟后解析的 promise
  const resolveAfter = (ms, value) =>
    new Promise(resolve => setTimeout(() => resolve(value), ms))

  // 辅助函数,用于创建一个在延迟后拒绝的 promise
  const rejectAfter = (ms, reason) =>
    new Promise((resolve, reject) => setTimeout(() => reject(reason), ms))

  describe('Promise.all(iterable)', () => {
    test('should resolve when all promises resolve', async () => {
      const promises = [
        resolveAfter(10, 'one'),
        resolveAfter(20, 'two'),
        Promise.resolve('three')
      ]
      const results = await Promise.all(promises)
      expect(results).toEqual(['one', 'two', 'three'])
    })

    test('should reject if any promise rejects', async () => {
      const promises = [
        resolveAfter(10, 'one'),
        rejectAfter(20, 'Error: two failed'),
        Promise.resolve('three')
      ]
      try {
        await Promise.all(promises)
      } catch (error) {
        expect(error).toBe('Error: two failed')
      }
    })
  })

  describe('Promise.allSettled(iterable)', () => {
    test('should resolve with an array of settlement objects', async () => {
      const promises = [
        resolveAfter(10, 'one'),
        rejectAfter(20, 'Error: two failed'),
        Promise.resolve('three')
      ]
      const results = await Promise.allSettled(promises)
      expect(results).toEqual([
        { status: 'fulfilled', value: 'one' },
        { status: 'rejected', reason: 'Error: two failed' },
        { status: 'fulfilled', value: 'three' }
      ])
    })
  })

  describe('Promise.any(iterable)', () => {
    test('should resolve with the value of the first promise to fulfill', async () => {
      const promises = [
        rejectAfter(10, 'fail one'),
        resolveAfter(20, 'success two'),
        resolveAfter(5, 'success one')
      ]
      const result = await Promise.any(promises)
      expect(result).toBe('success one')
    })

    test('should reject with an AggregateError if all promises reject', async () => {
      const promises = [
        rejectAfter(10, 'fail one'),
        rejectAfter(20, 'fail two'),
        Promise.reject('fail three')
      ]
      try {
        await Promise.any(promises)
      } catch (error) {
        expect(error).toBeInstanceOf(AggregateError)
        expect(error.errors).toEqual(['fail one', 'fail two', 'fail three'])
      }
    })
  })

  describe('Promise.race(iterable)', () => {
    test('should resolve if the first settled promise resolves', async () => {
      const promises = [
        resolveAfter(10, 'one'),
        rejectAfter(5, 'Error: two failed fast')
      ]
      try {
        await Promise.race(promises)
      } catch (error) {
        expect(error).toBe('Error: two failed fast') // 因为 rejectAfter(5) 更快
      }

      const promises2 = [
        resolveAfter(5, 'one fast'),
        rejectAfter(10, 'Error: two failed slow')
      ]
      const result2 = await Promise.race(promises2)
      expect(result2).toBe('one fast')
    })

    test('should reject if the first settled promise rejects', async () => {
      const promises = [
        rejectAfter(10, 'Error: one failed'),
        resolveAfter(20, 'two')
      ]
      try {
        await Promise.race(promises)
      } catch (error) {
        expect(error).toBe('Error: one failed')
      }
    })
  })

  describe('Promise.reject(reason)', () => {
    test('should return a rejected promise', async () => {
      const reason = 'Test rejection'
      try {
        await Promise.reject(reason)
      } catch (error) {
        expect(error).toBe(reason)
      }
    })
  })

  describe('Promise.resolve(value)', () => {
    test('should return a resolved promise with the given value', async () => {
      const value = 'Test resolution'
      const result = await Promise.resolve(value)
      expect(result).toBe(value)
    })

    test('should return a resolved promise if value is a thenable', async () => {
      const thenable = {
        then: (resolve) => resolve('resolved from thenable')
      }
      const result = await Promise.resolve(thenable)
      expect(result).toBe('resolved from thenable')
    })
  })

  describe('Promise.try(fn)', () => {
    // Promise.try 不是标准的内置方法。
    // 它通常由像 Bluebird 这样的库提供,或者可以被 polyfill。
    // 这是一个用于演示的简单 polyfill:
    const promiseTry = (fn) => {
      return new Promise((resolve) => {
        resolve(fn())
      })
    }

    test('should resolve with the return value of fn if it does not throw', async () => {
      const result = await promiseTry(() => 'success')
      expect(result).toBe('success')
    })

    test('should reject if fn throws an error', async () => {
      const errorMsg = 'Function failed'
      try {
        await promiseTry(() => {
          throw new Error(errorMsg)
        })
      } catch (error) {
        expect(error.message).toBe(errorMsg)
      }
    })

    test('should resolve with the resolved value if fn returns a promise', async () => {
      const result = await promiseTry(() => Promise.resolve('async success'))
      expect(result).toBe('async success')
    })

    test('should reject with the rejected reason if fn returns a rejected promise', async () => {
      const errorMsg = 'Async function failed'
      try {
        await promiseTry(() => Promise.reject(errorMsg))
      } catch (error) {
        expect(error).toBe(errorMsg)
      }
    })
  })

  describe('Promise.withResolvers()', () => {
    // Promise.withResolvers() 是一个较新的补充 (ES2023+)。
    // 在没有 polyfill 或特定 Node/浏览器版本的情况下,它可能并非在所有环境中都可用。
    // 假设它在此测试中可用。
    if (typeof Promise.withResolvers === 'function') {
      test('should allow manual resolution of a promise', async () => {
        const { promise, resolve } = Promise.withResolvers()
        setTimeout(() => resolve('Manually resolved!'), 10)
        const result = await promise
        expect(result).toBe('Manually resolved!')
      })

      test('should allow manual rejection of a promise', async () => {
        const { promise, reject } = Promise.withResolvers()
        setTimeout(() => reject('Manually rejected!'), 10)
        try {
          await promise
        } catch (error) {
          expect(error).toBe('Manually rejected!')
        }
      })
    } else {
      test.skip('Promise.withResolvers() is not available in this environment', () => {})
    }
  })

  describe('Promise.prototype.then(onFulfilled, onRejected)', () => {
    test('should call onFulfilled when promise resolves', async () => {
      const onFulfilled = jest.fn(value => `Fulfilled: ${value}`)
      const onRejected = jest.fn()

      const result = await Promise.resolve('Data')
        .then(onFulfilled, onRejected)

      expect(onFulfilled).toHaveBeenCalledWith('Data')
      expect(onRejected).not.toHaveBeenCalled()
      expect(result).toBe('Fulfilled: Data')
    })

    test('should call onRejected when promise rejects', async () => {
      const onFulfilled = jest.fn()
      const onRejected = jest.fn(reason => `Rejected: ${reason}`)

      const result = await Promise.reject('Error')
        .then(onFulfilled, onRejected)

      expect(onFulfilled).not.toHaveBeenCalled()
      expect(onRejected).toHaveBeenCalledWith('Error')
      expect(result).toBe('Rejected: Error')
    })

    test('should chain promises', async () => {
      const result = await Promise.resolve(10)
        .then(value => value * 2)
        .then(value => value + 5)
      expect(result).toBe(25)
    })

    test('should handle rejection in chain', async () => {
      try {
        await Promise.resolve(10)
          .then(() => { throw new Error('Chain error') })
          .then(value => value + 5)
      } catch (error) {
        expect(error.message).toBe('Chain error')
      }
    })
  })

  describe('Promise.prototype.catch()', () => {
    test('should catch a rejected promise', async () => {
      const errorHandler = jest.fn(reason => `Caught: ${reason}`)
      const result = await Promise.reject('Initial Error')
        .catch(errorHandler)

      expect(errorHandler).toHaveBeenCalledWith('Initial Error')
      expect(result).toBe('Caught: Initial Error')
    })

    test('should not be called if promise resolves', async () => {
      const errorHandler = jest.fn()
      await Promise.resolve('Success')
        .catch(errorHandler)
      expect(errorHandler).not.toHaveBeenCalled()
    })

    test('should catch errors thrown in a preceding then', async () => {
      const errorMessage = 'Error in then'
      const errorHandler = jest.fn()
      await Promise.resolve('data')
        .then(() => {
          throw new Error(errorMessage)
        })
        .catch(errorHandler)
      expect(errorHandler).toHaveBeenCalled()
      expect(errorHandler.mock.calls[0][0].message).toBe(errorMessage)
    })
  })

  describe('Promise.prototype.finally(onFinally)', () => {
    test('should execute onFinally when promise resolves', async () => {
      const onFinally = jest.fn()
      const result = await Promise.resolve('Resolved value')
        .finally(onFinally)

      expect(onFinally).toHaveBeenCalledTimes(1)
      expect(result).toBe('Resolved value') // finally 会传递原始的解决值
    })

    test('should execute onFinally when promise rejects', async () => {
      const onFinally = jest.fn()
      const rejectionReason = 'Rejected reason'
      try {
        await Promise.reject(rejectionReason)
          .finally(onFinally)
      } catch (error) {
        expect(error).toBe(rejectionReason) // finally 会传递原始的拒绝原因
      }
      expect(onFinally).toHaveBeenCalledTimes(1)
    })

    test('should execute onFinally even if it returns a new promise', async () => {
      const onFinally = jest.fn(() => resolveAfter(10, 'finally done'))
      const result = await Promise.resolve('Original Value')
        .finally(onFinally)

      expect(onFinally).toHaveBeenCalledTimes(1)
      expect(result).toBe('Original Value') // 原始值被保留
    })

    test('if onFinally throws, the promise rejects with that error', async () => {
      const finallyError = new Error('Error in finally')
      const onFinally = jest.fn(() => { throw finallyError })

      try {
        await Promise.resolve('Original Value').finally(onFinally)
      } catch (error) {
        expect(error).toBe(finallyError)
      }
      expect(onFinally).toHaveBeenCalledTimes(1)

      try {
        await Promise.reject('Original Rejection').finally(onFinally)
      } catch (error) {
        expect(error).toBe(finallyError) // finally 中抛出的错误会取代原始的拒绝原因
      }
      expect(onFinally).toHaveBeenCalledTimes(2) // 再次调用
    })
  })
})

Open browser consoleTests

async 和 Promise 的区别

答案
区别点PromiseAsync/Await
语法基于 then/catch 链式调用基于同步风格,使用 await 关键字
可读性回调链,易嵌套结构清晰,接近同步代码
错误处理catch 捕获try/catch 捕获
返回值返回 Promise 实例返回 Promise,await 得到解析值
提示

Promise 在 V8 内部实际上还是通过回调模式来实现,本质上 then catch 等方法仍然是注册回调函数,只不过语法上更清晰。而 Async /Await 则是基于 Promise 的语法糖,结合生成器(Generator)实现了暂停和恢复执行的能力,使得异步代码更像同步代码。

延伸阅读

55%