简单手写 Promise

Huy大约 8 分钟javascriptjavascript

在刚学 JS 的时候有尝试手写过完整的 Promise,但是代码量太过于庞大了,过了半年就忘记了。 这一次,手写一个简易版的 Promise,以加强理解。

技术要点

  • 能够初始化和异步调用
  • 能够实现 then 和 catch 的链式调用
  • 静态方法: resolve、reject、all 和 race

提示

值得注意的是, 本文实现的 Promise, 并非规范版 Promise。区别在于: 规范中不论 promise 被 reject 还是被 resolve 时, 结果 都会被 resolve,只有出现异常时才会被rejected

测试用例

首先准备测试用例:

// 常规方法/异步调用
const p1 = new MyPromise((resolve, reject) => {
  resolve(100)
})

console.log('p1', p1)

const p2 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(200)
  }, 100)
})

console.log('p2', p2)

const p3 = new MyPromise((resolve, reject) => {
  reject(300)
})

console.log('p3', p3)

const p4 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject(400)
  }, 100)
})

console.log('p4', p4)

// 链式调用 then catch
const p5 = p1.then((res) => {
  return res + 1
})

console.log('p5', p5)

const p6 = p3.catch((data) => {
  return data + 2
})

console.log('p6', p6)

// 静态方法

const p7 = MyPromise.resolve(700)
const p8 = MyPromise.reject('错误信息')
const p9 = MyPromise.all([p1, p2, p7, p8])
const p10 = MyPromise.race([p1, p2, p7, p8])

正式手写 Promise

1. 手写基础构造函数

首先,定义一个名为 MyPromise 的类。该类包含了三个属性:state、value 和 reason。state 表示 Promise 的状态,可能的取值有 'padding'(进行中)、'fulfilled'(已成功)和 'rejected'(已失败)。value 是成功时的返回值,reason 是失败时的原因。

类的构造函数接受一个函数 fn 作为参数,该函数包含两个参数 resolveHandler 和 rejectHandler,分别用于处理成功和失败的情况。

在构造函数内部,定义 resolveHandler 和 rejectHandler 函数。resolveHandler 用于将 Promise 的状态从 'padding' 改变为 'fulfilled',并将返回值保存在 value 属性中。rejectHandler 用于将 Promise 的状态从 'padding' 改变为 'rejected',并将失败原因保存在 reason 属性中。

接下来,即调用传入构造函数的函数 fn,尝试执行 fn(resolveHandler, rejectHandler),并将定义的 resolveHandler 和 rejectHandler 作为参数传递给它。这表示 fn 函数中的代码将决定 Promise 的最终状态和值。如果 fn 执行成功,调用 resolveHandler 函数将使 Promise 变为成功状态,并将返回值保存在 value 属性中。如果 fn 抛出异常,catch 块将捕获异常并调用 rejectHandler 函数,将异常作为失败原因保存在 reason 属性中。

具体代码如下:

class MyPromise {
  state = 'padding' // 状态, 'padding' 'fulfilled' 'rejected'
  value = undefined // 成功之后的值
  reason = undefined // 失败之后的值

  constructor(fn) {
    const resolveHandler = (value) => {
      if (this.state === 'padding') {
        this.state = 'fulfilled'
        this.value = value
      }
    }

    const rejectHandler = (reason) => {
      if (this.state === 'padding') {
        this.state = 'rejected'
        this.reason = reason
      }
    }

    // Promise 只能捕获同步错误, 无法捕获异步错误
    try {
      fn(resolveHandler, rejectHandler)
    } catch (err) {
      rejectHandler(err)
    }
  }
}

关于异步错误的捕获,官方的 Promise 也是无法捕获的:

new Promise(() => {
  setTimeout(() => {
    throw '错误'
  }, 100)
})

// 结果:
// Promise {<pending>}

2. 实现 then 和 catch 的链式调用

首先,在 MyPromise 类中添加了两个属性 resolveCallBacks 和 rejectCallBacks,分别用于存储在状态为 'padding' 时的成功回调和失败回调。这意味着在 Promise 处于进行中状态时,可以通过 then() 方法注册成功和失败的回调函数。

接下来,定义 then() 方法,该方法接受两个参数 fn1 和 fn2,分别表示成功和失败的回调函数。这两个参数可以是函数,也可以是非函数值。如果参数不是函数,将被转换为一个函数,该函数会将传入的值直接返回

在 then() 方法内部,根据 Promise 的当前状态进行不同的处理:

  1. 如果状态为 'padding',说明 Promise 仍处于进行中状态,无法立即转换状态。此时,将返回一个新的 MyPromise 对象,并将成功和失败的回调函数存储到对应的回调数组中(resolveCallBacks 和 rejectCallBacks)。在异步操作完成后,通过执行这些回调函数来转换状态和传递值。
  2. 如果状态为 'fulfilled',说明 Promise 已经成功。此时,将立即执行成功回调函数 fn1,并返回一个新的 MyPromise 对象,该对象的状态由 fn1 的执行结果决定。
  3. 如果状态为 'rejected',说明 Promise 已经失败。此时,将立即执行失败回调函数 fn2,并返回一个新的 MyPromise 对象,该对象的状态由 fn2 的执行结果决定。

最后,依据 Promise 的定义可以知道 catch() 方法实际上就是 then() 方法的语法糖。catch() 方法接受一个参数 fn,表示失败的回调函数。实际上,它相当于调用 then(null, fn),将 null 作为成功回调函数,将 fn 作为失败回调函数。

在下述代码中,resolveCallBacks 和 rejectCallBacks 是使用数组保存的,目的在于连续调用 then 时以确保 then 都能得到正确执行:

const p = new Promise((resolve, > reject)=>{
    setTimeout(()=>{
        resolve(100)
    }, 0)
})

p.then((res)=>{
    // 第一次调用: 100 res1
    console.log(res, 'res1')
})
p.then((res2)=>{
    // 第二次调用: 100 res2
    console.log(res2, 'res2')
})

因此,可以写出如下的代码:

class MyPromise {
  state = 'padding' // 状态, 'padding' 'fulfilled' 'rejected'
  value = undefined // 成功之后的值
  reason = undefined // 失败之后的值

  resolveCallBacks = [] // padding 状态下, 存储成功的回调
  rejectCallBacks = [] // padding 状态下, 存储失败的回调

  constructor(fn) {
    const resolveHandler = (value) => {
      if (this.state === 'padding') {
        this.state = 'fulfilled'
        this.value = value
        this.resolveCallBacks.forEach((fn) => fn(this.value))
      }
    }

    const rejectHandler = (reason) => {
      if (this.state === 'padding') {
        this.state = 'rejected'
        this.reason = reason
        this.rejectCallBacks.forEach((fn) => fn(this.reason))
      }
    }

    try {
      fn(resolveHandler, rejectHandler)
    } catch (err) {
      rejectHandler(err)
    }
  }

  then(fn1, fn2) {
    // 传入非函数值, 则会无效化
    fn1 = typeof fn1 === 'function' ? fn1 : (v) => v
    fn2 = typeof fn2 === 'function' ? fn2 : (e) => e

    if (this.state === 'padding') {
      // 当 padding 状态下, 则是异步调用无法立即装换状态,fn1 和 fn2 会存储到 CallBacks 中
      return new MyPromise((resolve, reject) => {
        this.resolveCallBacks.push(() => {
          try {
            const newValue = fn1(this.value)
            resolve(newValue) // 异步执行完毕得到 newValue 时,才转换 state 状态
          } catch (error) {
            reject(error)
          }
        })

        this.rejectCallBacks.push((resolve, reject) => {
          try {
            const newReason = fn2(this.reason)
            reject(newReason) // 异步执行完毕得到 newReason 时,才转换 state 状态
          } catch (error) {
            reject(error)
          }
        })
      })
    }

    if (this.state === 'fulfilled') {
      return new MyPromise((resolve, reject) => {
        try {
          const newValue = fn1(this.value)
          resolve(newValue)
        } catch (error) {
          reject(error)
        }
      })
    }

    if (this.state === 'rejected') {
      return new MyPromise((resolve, rejected) => {
        try {
          const newReason = fn2(this.reason)
          rejected(newReason)
        } catch (error) {
          rejected(error)
        }
      })
    }
  }

  // catch 是 then 的语法糖, resolve 回调函数为 null
  catch(fn) {
    return this.then(null, fn)
  }
}

3. 完善 Promise 的静态方法

Promise 的静态方法有很多,这里着重实现几个:

  1. MyPromise.resolve(value) 方法接受一个值 value,返回一个已经被解析为成功状态的 Promise 对象。它实际上是创建一个新的 MyPromise 对象,并立即调用 resolve 方法将 value 作为成功值传递。
  2. MyPromise.reject(reason) 方法接受一个原因 reason,返回一个已经被解析为失败状态的 Promise 对象。它实际上是创建一个新的 MyPromise 对象,并立即调用 reject 方法将 reason 作为失败原因传递。
  3. MyPromise.all(promiseList) 方法接受一个 Promise 对象数组 promiseList,返回一个新的 Promise 对象。这个方法将等待所有的 Promise 对象都解析为成功状态,然后将所有 Promise 对象的结果组成一个数组作为新 Promise 对象的成功值。如果其中任何一个 Promise 对象失败,新的 Promise 对象将立即被解析为失败状态,并传递失败原因。
  4. MyPromise.race(promiseList) 方法接受一个 Promise 对象数组 promiseList,返回一个新的 Promise 对象。这个方法将等待任何一个 Promise 对象首先解析为成功或失败状态,然后将该 Promise 对象的结果作为新 Promise 对象的结果。无论是成功还是失败,新的 Promise 对象将立即解析。
MyPromise.resolve = function (value) {
  return new MyPromise((resolve, rejected) => resolve(value))
}

MyPromise.reject = function (reason) {
  return new MyPromise((resolve, rejected) => rejected(reason))
}

MyPromise.all = function (promiseList = []) {
  return new MyPromise((resolve, rejected) => {
    const result = [] // 储存所有的 Promise
    const length = promiseList.length
    let resolvedCount = 0

    promiseList.forEach((p) => {
      p.then((data) => {
        result.push(data)
        if (++resolvedCount === length) {
          resolve(result)
        }
      }).catch((err) => {
        rejected(err)
      })
    })
  })
}

MyPromise.race = function (promiseList = []) {
  let resolved = false
  return new MyPromise((resolve, reject) => {
    promiseList.forEach((p) => {
      p.then((data) => {
        if (!resolved) {
          resolve(data)
          resolved = true
        }
      }).catch((err) => {
        reject(err)
      })
    })
  })
}

完整 Promise 代码

class MyPromise {
  state = 'padding' // 状态, 'padding' 'fulfilled' 'rejected'
  value = undefined // 成功之后的值
  reason = undefined // 失败之后的值

  resolveCallBacks = [] // padding 状态下, 存储成功的回调
  rejectCallBacks = [] // padding 状态下, 存储失败的回调

  constructor(fn) {
    const resolveHandler = (value) => {
      if (this.state === 'padding') {
        this.state = 'fulfilled'
        this.value = value
        this.resolveCallBacks.forEach((fn) => fn(this.value))
      }
    }

    const rejectHandler = (reason) => {
      if (this.state === 'padding') {
        this.state = 'rejected'
        this.reason = reason
        this.rejectCallBacks.forEach((fn) => fn(this.reason))
      }
    }

    try {
      fn(resolveHandler, rejectHandler)
    } catch (err) {
      rejectHandler(err)
    }
  }

  then(fn1, fn2) {
    // 传入非函数值, 则会无效化
    fn1 = typeof fn1 === 'function' ? fn1 : (v) => v
    fn2 = typeof fn2 === 'function' ? fn2 : (e) => e

    if (this.state === 'padding') {
      // 当 padding 状态下, 则是异步调用无法立即装换状态,fn1 和 fn2 会存储到 CallBacks 中
      return new MyPromise((resolve, reject) => {
        this.resolveCallBacks.push(() => {
          try {
            const newValue = fn1(this.value)
            resolve(newValue) // 异步执行完毕得到 newValue 时,才转换 state 状态
          } catch (error) {
            reject(error)
          }
        })

        this.rejectCallBacks.push((resolve, reject) => {
          try {
            const newReason = fn2(this.reason)
            reject(newReason) // 异步执行完毕得到 newReason 时,才转换 state 状态
          } catch (error) {
            reject(error)
          }
        })
      })
    }

    if (this.state === 'fulfilled') {
      return new MyPromise((resolve, reject) => {
        try {
          const newValue = fn1(this.value)
          resolve(newValue)
        } catch (error) {
          reject(error)
        }
      })
    }

    if (this.state === 'rejected') {
      return new MyPromise((resolve, rejected) => {
        try {
          const newReason = fn2(this.reason)
          rejected(newReason)
        } catch (error) {
          rejected(error)
        }
      })
    }
  }

  // catch 是 then 的语法糖 resolve 回调函数为 null
  catch(fn) {
    return this.then(null, fn)
  }
}

MyPromise.resolve = function (value) {
  return new MyPromise((resolve, rejected) => resolve(value))
}

MyPromise.reject = function (reason) {
  return new MyPromise((resolve, rejected) => rejected(reason))
}

MyPromise.all = function (promiseList = []) {
  return new MyPromise((resolve, rejected) => {
    const result = [] // 储存所有的 Promise
    const length = promiseList.length
    let resolvedCount = 0

    promiseList.forEach((p) => {
      p.then((data) => {
        result.push(data)
        if (++resolvedCount === length) {
          resolve(result)
        }
      }).catch((err) => {
        rejected(err)
      })
    })
  })
}

MyPromise.race = function (promiseList = []) {
  let resolved = false
  return new MyPromise((resolve, reject) => {
    promiseList.forEach((p) => {
      p.then((data) => {
        if (!resolved) {
          resolve(data)
          resolved = true
        }
      }).catch((err) => {
        reject(err)
      })
    })
  })
}
Loading...