青铜到铂金的手写Promise修炼

214 阅读12分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

一、什么是promise

我们知道,在promise出现之前,我们采用回调的方式处理异步问题;这样当嵌套太深时容易陷入回调地狱,代码维护也会变的困难;promise的出现解决了此问题。

那什么是promise?

阮一峰老师在es6入门中说道:所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

二、手写实现

1. 青铜篇

Promises/A+中说道“promise”是一个对象或函数;下面代码 我们采用构造函数的方式实现。

我们先看一个promise简单使用

const promise = new Promise(function (resolve, reject) {
    console.log('promise开始执行')
    // throw new Error ('出错了')
    // setTimeout(() => {
    // 	resolve('成功')
    // }, 0)
    resolve('成功')
    // reject('失败')
})
promise.then(
    function (res) {
        console.log('成功', res)
    },
    function (err) {
        console.log('失败', err)
    }
)
console.log('代码执行')

可以看到代码的运行结果是

  1. promise开始执行
  2. 代码执行
  3. 成功 成功
  • 我们看出,首先promise 接受一个函数作为参数,我们叫他executor执行器,它是立即执行,里面有两个函数做参数,一个resolve(成功),一个reject(失败)。然后看到他有两个函数,一个去执行成功状态的函数,一个去执行失败状态的函数。

  • 同时也注意到我们在执行器throw error时,他也执行了reject。 我们再看Promises/A+规范中说到, promise有三种状态 pending(等待), fulfilled(成功), rejected(失败)

  • 等待态 可以转换到已完成或拒绝状态

  • 成功态 不得过渡到任何其他状态;必须有一个不能改变的值。

  • 失败态 不得过渡到任何其他状态;一定有理由,不能改变。

再看说的then方法:

  • then方法的行为符合本规范
  • promise必须提供一种then方法来访问其当前或最终的值或原因
  • then方法接受两个参数
promise.then(onFulfilled, onRejected)

我们再会看上面的例子,我们在promise执行器执行resolve(成功),reject(失败)改变状态后,会在.then方法里面对应执行onFulfilled, onRejected,并且把成功的值和失败的理由作为参数进行了传递。

至此,我们可以实现最初级版的promise

const PENDING = 'pending' // 等待
const FULFILLED = 'fulfilled' // 成功态
const REJECTED = 'rejected' // 失败态

function MyPromise(excuter) {
    this.status = PENDING
    this.value = undefined // 成功的结果
    this.reason = undefined // 失败的原因

    const resolve = (value) => {
        if (this.status === PENDING) { // 状态一经改变  不能再改变
          this.value = value
          this.status = FULFILLED
        }
    }
    const reject = (reason) => {
        if (this.status === PENDING) { // 状态一经改变  不能再改变
          this.reason = reason
          this.status = REJECTED
        }
    }
    try {
        excuter(resolve, reject) // 立即执行
    } catch (error) {
        reject(error)
    }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) { // then方法挂在到原型上
  // console.log(this) // 注意这里的this
  if (this.status === FULFILLED) {
    onFulfilled(this.value) // 成功后执行onFulfilled 并把成功的结果作为参数
  }
  if (this.status === REJECTED) {
    onRejected(this.reason) // onRejected 并把成功的结果作为参数
  }
}

2. 白银篇

现在我们升级打怪到达白银段位,我们首先看一个基于我们青铜段位实现的promise来执行的例子。

const promise = new MyPromise(function (resolve, reject) {
    console.log('promise开始执行')
    setTimeout(() => {
        resolve('成功')
    }, 0)
})
promise.then(
    function (res) {
        console.log('成功', res)
    },
    function (err) {
        console.log('失败', err)
    }
)
console.log('代码执行')

然后我看看运行结果:

image.png

ok,我们发现then方法里面都没有执行成功的回调。同时,我们在then方法里面打印this.status,我们发现此时的状态是pending(等待态)。

现在我们知道了原因就是setTimeout 包裹的resolve是异步执行的,当走到then方法里面时,状态没有改变; 外加可能同时有好几个promise在执行。于是,我们采用类似依赖收集的方式,在pending状态时把成功或者失败的回调收集起来,如何在状态改变的时候去循环执行它们。

function MyPromise(excuter) {
    this.status = PENDING
    this.value = undefined // 成功的结果
    this.reason = undefined // 失败的原因
+   this.onResolvedCallbacks = [] // 存放成功的回调
+   this.onRejectedCallbacks = [] // 存放失败的回调

    const resolve = (value) => {
        if (this.status === PENDING) { // 状态一经改变  不能再改变
          this.value = value
          this.status = FULFILLED
+          this.onResolvedCallbacks.forEach(fn => fn())
        }
    }
    const reject = (reason) => {
        if (this.status === PENDING) { // 状态一经改变  不能再改变
          this.reason = reason
          this.status = REJECTED
+          this.onRejectedCallbacks.forEach(fn => fn())
        }
    }
    try {
        excuter(resolve, reject) // 立即执行
    } catch (error) {
        reject(error)
    }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) { // then方法挂在到原型上
  // console.log(this) // 注意这里的this
+  // console.log(11122, this.status)
  if (this.status === FULFILLED) {
    onFulfilled(this.value) // 成功后执行onFulfilled 并把成功的结果作为参数
  }
  if (this.status === REJECTED) {
    onRejected(this.reason) // onRejected 并把成功的结果作为参数
  }
+  if (this.status === PENDING) {
+    this.onResolvedCallbacks.push(() => onFulfilled(this.value))
+    this.onRejectedCallbacks.push(() => onRejected(this.reason))
+  }
}

接下来,我们来实现then的链式调用。

首先我们看看Promises/A+规范是怎么说的

  1. 这两个onFulfilledonRejected可选的参数:

    • 如果onFulfilled不是函数,则必须忽略它。
    • 如果onRejected不是函数,则必须忽略它。

    这里我们得到一个优化点,就是在我们的then方法里面要判断onFulfilledonRejected的类型,这个优化点我们先记录下来 后面一起实现,我们再往下看看讲了什么。

  2. then 可以在同一个 Promise 上多次调用。

    • 如果promise 成功了,所有相应的onFulfilled回调必须以他们注册时的顺序依次执行.
    • 如果promise被拒绝,所有相应的onRejected回调必须以他们注册时的顺序依次执行.
  3. then必须返回一个promise

promise2 = promise1.then(onFulfilled, onRejected);

这样我们知道在then方法里面我们需要再return一个promise出去,这样我们才能做到链式调用

MyPromise.prototype.then = function (onFulfilled, onRejected) {
  ...
  promise2 = new MyPromise((resolve, reject)=>{
  })
  return promise2
}
  • 3.1 如果其中一个onFulfilledonRejected返回一个值x,则运行 Promise Resolution Procedure [[Resolve]](promise2, x)
  • 3.2 如果其中一个onFulfilledonRejected抛出异常e,则promise2必须以拒绝e为理由。
  • 3.3 如果onFulfilled不是一个函数并且promise1被满足,则promise2必须以与 相同的值来满足promise1
  • 3.4 如果onRejected不是函数并且promise1被拒绝,则promise2必须以与 相同的原因被拒绝promise1

针对于3.1、3.2我们结合一个例子来看

const promise = new Promise(function (resolve, reject) {
    console.log('promise开始执行')
    setTimeout(() => {
            resolve('成功')
    }, 0)
    // resolve('成功')
    // reject('失败')
})
promise.then(
    function (res) {
        console.log('成功', res)
        return 1
    },
    function (err) {
        console.log('失败', err)
        return '失败了哦'
    }
).then((data) => {
  console.log(3333, data)
}, (data) => {
  console.log(2222, data)
})
console.log('代码执行')

运行结果

image.png

注意:这里的promise不是我们自己的

  • 我们可以看到 promise.then里面return的值在第二个then里面的onFulfilled/onRejected里面作为参数拿到了 那它是怎么拿到的呢
  • 我们看第一个.then里面的参数是如何拿到的。我们自然知道,在new Promise里面我们执行了resolve('成功')/reject('失败'),然后我们在第一个.then里面的onFulfilled/onRejected拿到了传递过来的参数,这样我们就知道该如何下手,就是在我们returnpromise2里面执行resolve/reject但是问题又来了,参数呢,参数怎么办

我们再观察,promise第一个.then里面onFulfilled/onRejectedreturn,自然想到我们在MyPromise.prototype.then里面onFulfilled(this.value)/onRejected(this.reason) 可以拿到返回值,我们赋值给一个变量x。那我们怎么拿到这个变量x,我们知道new Promise里面的代码是立即执行的,于是我们把三个if判断放到promise2里面去执行,这样就可以拿到x了,完美~

还没有结束,我们再看看,上面说的3.3,3.4

我们先结合本来的promise实现的例子来看这两点的意思

const promise = new MyPromise(function (resolve, reject) {
    console.log('promise开始执行')
    resolve('成功')
    // reject('失败')
})
promise.then(
  '成功', '失败'
).then((data) => {
  console.log(3333, data)
}, (data) => {
  console.log(2222, data)
})
console.log('代码执行')

运行结果

image.png

我们看到promise.then里面的参数不是两个函数了,正如规范里面所说的那样,onFulfilled/onRejected不是一个函数,并且promise1的状态被改变fulfilled/rejectedpromise2必改变为和peomise1一样的状态,并且把它的值作为参数。就如同上面执行结果的console.log(2222, '成功')

ok,到这里我们上代码

const PENDING = 'pending' // 等待
const FULFILLED = 'fulfilled' // 成功态
const REJECTED = 'rejected' // 失败态

function MyPromise(excuter) {
    this.status = PENDING
    this.value = undefined // 成功的结果
    this.reason = undefined // 失败的原因
    this.onResolvedCallbacks = [] // 存放成功的回调
    this.onRejectedCallbacks = [] // 存放失败的回调

    const resolve = (value) => {
        if (this.status === PENDING) { // 状态一经改变  不能再改变
          this.value = value
          this.status = FULFILLED
          this.onResolvedCallbacks.forEach(fn => fn())
        }
    }
    const reject = (reason) => {
        if (this.status === PENDING) { // 状态一经改变  不能再改变
          this.reason = reason
          this.status = REJECTED
          this.onRejectedCallbacks.forEach(fn => fn())
        }
    }
  try {
    excuter(resolve, reject) // 立即执行
  } catch (error) {
    reject(error)
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) { // then方法挂在到原型上
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
  onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
  promise2 = new MyPromise((resolve, reject)=>{
    if (this.status === FULFILLED) {
      let x = onFulfilled(this.value) // 成功后执行onFulfilled 并把成功的结果作为参数
      resolve(x)
    }
    if (this.status === REJECTED) {
      let x = onRejected(this.reason) // onRejected 并把成功的结果作为参数
      resolve(x)
    }
    if (this.status === PENDING) {
      this.onResolvedCallbacks.push(() => {
        let x = onFulfilled(this.value)
        resolve(x)
      })
      this.onRejectedCallbacks.push(() => {
        let x = onRejected(this.reason)
        resolve(x)
      })
    }
  })
  return promise2
}

好了,虽然这里面还有不少问题,我们白银阶段先到这里~

3. 黄金篇

在白银篇中,我们对于x的值,只考虑了普通值的处理方式;x是普通值的时候我们直接传递给下一个then。

我们再看规范中说的,如果x是一个promise,采用它的状态:

  • 如果x是pending状态,则promise必须保持待处理状态,直到xpending 状态转为 fulfilled 或 rejected 状态
  • 如果/当 x 状态是 fulfilled,resolve 它,并且传入和 promise1 一样的值 value
  • 如果/当 x 状态是 rejected,reject 它,并且传入和 promise1 一样的值 reason

在此我们知道,x是一个promise的时候,我们必须等到x返回的结果来判断promise2是成功还是失败。

于是我们抽离一个公共的方法去处理

resolvePromise(promise2, x, resolve, reject)

但是对于resolvePromise的调用的地方我们需要用setTimeouttry catch,看下下图中的注释说明

image.png

上面我们看了xpromise的处理方法,我们再看规范中说到:如果promisex引用同一个对象,promise则以 aTypeError为理由拒绝。

我们看下例子

const p1 = new Promise((resolve, reject) => {
  resolve('成功')
})
const p2 = p1.then(data => {
  return p2
})
p2.then(data => {}, err => {
  console.log(2222, err)
})

执行结果 image.png 所以我们知道在resolvePromise中首先要判断x和promise2是不是相等,相等要抛TypeError错。

该阶段我们实现promise规范中2.3 promise解决程序,对于x是函数或对象的实现我们下一篇再说,先上一波代码

const PENDING = 'pending' // 等待
const FULFILLED = 'fulfilled' // 成功态
const REJECTED = 'rejected' // 失败态

function MyPromise(excuter) {
	this.status = PENDING
	this.value = undefined // 成功的结果
	this.reason = undefined // 失败的原因
  this.onResolvedCallbacks = [] // 存放成功的回调
  this.onRejectedCallbacks = [] // 存放失败的回调

	const resolve = (value) => {
    if (this.status === PENDING) { // 状态一经改变  不能再改变
      this.value = value
      this.status = FULFILLED
      this.onResolvedCallbacks.forEach(fn => fn())
    }
  }
	const reject = (reason) => {
    if (this.status === PENDING) { // 状态一经改变  不能再改变
      this.reason = reason
      this.status = REJECTED
      this.onRejectedCallbacks.forEach(fn => fn())
    }
  }
  try {
    excuter(resolve, reject) // 立即执行
  } catch (error) {
    reject(error)
  }
}

const resolvePromise = (promise2, x, resolve, reject) => {
  // 如果promise和x引用同一个对象,promise则以TypeError为理由拒绝。
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // 如果x是一个对象或函数的处理
  } else {
    // 如果 x 即不是函数类型也不是对象类型,直接 resolve x(resolve(x))
    resolve(x)
  }
}

MyPromise.prototype.then = function (onFulfilled, onRejected) { // then方法挂在到原型上
  // console.log(this) // 注意这里的this
  // console.log(11122, this.status)
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
  onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
  let promise2 = new MyPromise((resolve, reject)=>{
    if (this.status === FULFILLED) {
      // 此时变成异步之后我们try包裹的excuter不能catch到错误,所以我们的这里的代码得用try catch去报错,抛错的时候在catch中执行reject
      setTimeout(() => {
        try {
          let x = onFulfilled(this.value) // 成功后执行onFulfilled 并把成功的结果作为参数
          // 此时我们直接拿promise2是拿不到的会报错,我们必须等这个new完之后才能拿到promise2,我们在这里加一个setTimeout来处理
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      }, 0)
    }
    if (this.status === REJECTED) {
      setTimeout(() => {
        try {
          let x = onRejected(this.reason) // onRejected 并把成功的结果作为参数
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      }, 0)
    }
    if (this.status === PENDING) {
      this.onResolvedCallbacks.push(() => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      })
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      })
    }
  })
  return promise2
}

4. 铂金篇

接着上面的,我们考虑x是一个对象或函数的情况。 我们还是看规范中所说的:

  •  设置一个then作为x.then
    • 如果检索属性x.then导致抛出了一个异常e,用e作为原因拒绝promise
    • 如果then是一个函数,用x作为this调用它。then方法的参数为俩个回调函数,第一个参数叫做resolvePromise,第二个参数叫做rejectPromise
      • 如果resolvePromise用一个值y调用,运行[[Resolve]](promise, y)。译者注:这里再次调用[[Resolve]](promise,y),因为y可能还是promise
      • 如果rejectPromise用一个原因r调用,用r拒绝promise。译者注:这里如果rpromise的话,依旧会直接reject拒绝的原因就是promise。并不会等到promise解决拒绝
      • 2.3.3.3.3. 如果resolvePromiserejectPromise都被调用,或者对同一个参数进行多次调用,那么第一次调用优先,以后的调用都会被忽略。译者注:这里主要针对thenablepromise的状态一旦更改就不会再改变。
      • 2.3.3.3.4. 如果调用then抛出了一个异常e,
        • 2.3.3.4.1. 如果resolvePromiserejectPromise已经被调用,忽略它
        • 2.3.3.4.2. 否则,用e作为原因拒绝promise
  • 如果then不是一个函数,用x解决promise

好,我们这里上代码看resolvePromise完整的实现

const resolvePromise = (promise2, x, resolve, reject) => {
  // 如果promise和x引用同一个对象,promise则以TypeError为理由拒绝。
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }

  let called; // 2.3.3.3.3. 如果resolvePromise和rejectPromise都被调用,或者对同一个参数进行多次调用,那么第一次调用优先,以后的调用都会被忽略。
  if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
    // 如果x是一个对象或函数的处理
    try { // 2.3.3.2 如果检索属性x.then导致抛出了一个异常e,用e作为原因拒绝promise
      let then = x.then // 2.3.3.1 让then成为x.then
      if (typeof then === 'function') {
        then.call(x, y => { // 如果then是一个函数,用x作为this调用它
          if (called) return
          called = true
          resolvePromise(promise2, y, resolve, reject)
        }, r => {
          if (called) return
          called = true
          reject(r)
        })
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    // 如果 x 即不是函数类型也不是对象类型,直接 resolve x(resolve(x))
    resolve(x)
  }
}

好了,至此我们的手写简易Promise已经完成了,恭喜大家修炼完成~