为了面试,还是踏上了这条路,手写Promise

243 阅读5分钟

手写Promise

一、创建基础框架

/** 这是Promise写法 */
let promise = new Promise((resolve, reject) => {
    resolve('返回数据')
    reject('错误信息') // Promise中,仅会执行resolve/reject中一个
})

promise.then(
    res => { console.log(res) }, // 仅有这里输出 '返回数据'
    err => { console.log(err) }
)

模仿到Promise的例子,我们来创建自己手写的Promise。

class Commitment {
    // 三个基础状态 并且PENDING可变一次到FULFILLED/REJECTED,不可再次改变状态
    static PENDING = 'pending'
    static FULFILLED = 'fulfilled'
    static REJECTED = 'rejected'
    constructor(func) {
        // 实例化后,调用时status初始值为PENDING
        this.status = Commitment.PENDING
        this.result = null;
        // 因为实例化后,在外部调用的resolve/reject的this指向并不是Commitment构造函数,
        // 所以需要在调用resolve/reject时用.bind把this指向修改正确
        func(this.resolve.bind(this), this.reject.bind(this))
    }
    resolve(result) {
        if (this.status !== Commitment.PENDING) return // 仅可在PENDING状态下执行
        // 把状态修改成对应值
        this.status = Commitment.FULFILLED
        // 保存结果
        this.result = result
    }
    reject(result) {
        if (this.status !== Commitment.PENDING) return // 仅可在PENDING状态下执行
        // 把状态修改成对应值
        this.status = Commitment.REJECTED
        // 保存结果
        this.result = result
    }
}

let commitment = new Commitment((resolve, reject) => {
    resolve('返回数据')
})

在上面的代码中,已经实现了一个基础的、同步的Promise使用示例,接下来我们进行.then()调用

二、实现.then方法

我们知道 promise.then(func1, func2) 中可以接收两个参数作为回调,一个接收成功resolve回调,第二个接收失败reject回调。

class Commitment {
    ...
    then(onFULFILLED, onREJECTED) {
        if (this.status === Commitment.FULFILLED) {
            onFULFILLED(this.result)
        }
        
        if (this.status === Commitment.REJECTED) {
            onREJECTED(this.result)
        }
    }
}

let commitment = new Commitment((resolve, reject) => {
    resolve('返回数据')
})

commitment.then(
    res => console.log(res), // 拿到 '返回数据'
    err => console.log(err)
)

看起来我们已经实现了Promise的部分逻辑,这里会遇到两个问题。

1. 当resolve阶段发生错误信息时

在原生Promise中,当调用回调函数的函数块内发生错误,Promise.then可以正确捕获到错误,并在err中返回错误信息

let promise = new Promise((resolve, reject) => {
    throw new Error('错误信息')
}) 

promise.then( 
    res => console.log(res),
    err => console.log(err.message) // '错误信息'
)

目前我们写的代码中,还没有这一步的逻辑处理,如果按照以上方法调用的话,会直接在throw那行直接抛出异常,后续的 .then 并不会捕获到错误信息。那我们就需要在最初执行函数的时候,进行try catch捕获异常,如果捕获到异常情况,则直接调用this.reject(error)并把错误信息传进去。这样就实现了跟原生Promise的效果一样了。

class Commitment {
    ...
    constructor(func) {
        try {
            func(this.resolve.bind(this), this.reject.bind(this))
        } catch (error) {
            // 这里不需要再次用bind去绑定this,因为这里是直接去调用构造函数的方法
            this.reject(error)
        }
    }
}

2. 当.then传入的不是函数时

Promise.then 是可以传不是函数的代码作为参数,传入其他值,可以得到正常的调用结果。

let promise = new Promise((resolve, reject) => {
    resolve('返回数据')
})

promise.then(
    undefined,
    err => { console.log(err) }
)
// 以上代码实际输出的还是调用成功的逻辑,具体返回内容为Promise对象
// Promise {<fulfilled>: '返回数据'}

那我们手写的Commitment中,.then 方法传入undefined的话,会出现什么结果,没错onFULFILLED is not a function,会提示这个玩意儿不是个方法,所以在调用前,我们需要对 .then 中的传入值做一个简单的验证。

class Commitment {
    ...
    then(onFULFILLED, onREJECTED) {
        onFULFILLED = typeof onFULFILLED === 'function' ? onFULFILLED : () => {}
        onREJECTED = typeof onREJECTED === 'function' ? onREJECTED : () => {}
        ...
    }
}

三、实现异步操作

目前Commitment的整体流程还是同步的,并没有达到Promise的异步效果

console.log('第一步')

let commitment = new Commitment((resolve, reject) => {
  console.log('第二步')
  resolve('返回数据')
})

commitment.then(
  res => console.log(res),
  err => console.log(err)
)

console.log('第三步')

// 第一步
// 第二步
// 返回数据
// 第三步

那如果是原生的Promise是什么效果呢?

console.log('第一步')

let promise = new Promise((resolve, reject) => {
    console.log('第二步')
    resolve('返回数据')
})

promise.then(
    res => console.log(res),
    err => console.log(err)
)

console.log('第三步')

// 第一步
// 第二步
// 第三步
// 返回数据

这时候我们发现,原生的Promise中,调用resolve方法后的回调函数是异步执行的,那我们就直接简单粗暴的给Commitment中的 .then 里面,添加上setTimeout用来实现异步。

class Commitment {
    ...
    then(onFULFILLED, onREJECTED) {
        ...
        setTimeout(()=> {
            if (this.status === Commitment.FULFILLED) {
                onFULFILLED(this.result)
            }
            if (this.status === Commitment.REJECTED) {
                onREJECTED(this.result)
            }
        })
    }
}

这时候再按上面的调用方式去执行,会发现console.log打印值的顺序跟Promise一样了。

四、 链式调用

Promise是可以支持链式调用的,多个promise.then().then()的执行,但我们目前的代码还是不能实现链式调用,那么接下来我们就来实现这个功能。

promise.then().then()其实就是让前一个then方法再返回一个新的Promise,这个新的Promise就有then方法了,这样就实现了链式调用。

class Commitment {
    ...
    then(onFULFILLED, onREJECTED) {
        // 直接包裹手写的Promise肯定不行,
        // 还需要在下方调用onFULFILLED/onREJECTED时改为传给resolve/reject
        return new Commitment((resolve, reject) => {
            ...
            if (this.status === Commitment.FULFILLED) {
                resolve(onFULFILLED(this.result))
            }
            if (this.status === Commitment.REJECTED) {
                reject(onREJECTED(this.result))
            }
            ...
        })
    }
}

let commitment = new Commitment((resolve, reject) => {
    resolve('返回数据')
})

commitment
    .then(
        res => { 
            console.log(res)
            return '1'
        },
        err => console.error(err)
    )
    .then(
        res => {
            console.log(res)
            return '2'
        },
        err => console.error(err)
    )
    .then(res => console.log(res))

// 返回数据
// '1'
// '2'

五、 结语

现在满大街都是手写Promise的技术文章,自己跟着流程走一遍还是会熟悉很多,终于理解了Promise的一点点知识,后续会继续学习。

感谢 技术蛋老师 的视频和 achens 同学的笔记

六、 源码

class Commitment {
  static PENDING = '待定'
  static FULFILLED = '成功'
  static REJECTED = '拒绝'
  constructor(func) {
    this.status = Commitment.PENDING
    this.result = null

    try {
      func(this.resolve.bind(this), this.reject.bind(this))
    } catch (error) {
      this.reject(error)
    }
  }
  resolve(result) {
    if (this.status !== Commitment.PENDING) return
    this.status = Commitment.FULFILLED
    this.result = result
  }
  reject(result) {
    if (this.status !== Commitment.PENDING) return
    this.status = Commitment.REJECTED
    this.result = result
  }
  then(onFULFILLED, onREJECTED) {
    return new Commitment((resolve, reject) => {
      // 如果使用默认参数 会被传入进来的值覆盖掉,主要目的是为了验证传入进来的参数是否是函数
      onFULFILLED = typeof onFULFILLED === 'function' ? onFULFILLED : () => {}
      onREJECTED = typeof onREJECTED === 'function' ? onREJECTED : () => {}

      setTimeout(() => {
        if (this.status === Commitment.FULFILLED) {
          resolve(onFULFILLED(this.result))
        }

        if (this.status === Commitment.REJECTED) {
          reject(onREJECTED(this.result))
        }
      })
    })
  }
}

let commitment = new Commitment((resolve, reject) => {
  resolve('返回数据')
})

commitment
  .then(
    res => {
      console.log(res)
      return '1'
    },
    err => console.error(err)
  )
  .then(
    res => {
      console.log(res)
      return '2'
    },
    err => console.error(err)
  )
  .then(res => console.log(res))