不变的承诺 Promise

背景

Promise 是异步编程的一种解决方案,可以解决前端的回调地狱。以下根据 ES6 入门 简单过一下 Promise 的基础及个人的一些见解补充,同时尝试实现一个符合 PromiseA+ 规范的 Promise。

Promise 基础

Promise 的状态

Promise 只有三种状态:pending(进行中),fulfilled(已成功)和 rejected(已失败)。

Promise 的状态改变只有两种情况:

pendingfulfilled 以及 pendingrejected。当状态变成 fulfilledrejected时,我们就称这个 Promise resolved

⚠️ 我们通常说的 resolved 仅指代 fulfilled,不包括 rejected

构造一个 Promise
const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
promise.then

Promise 的实例拥有 then 方法:

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

⚠️ Promise 新建后立即执行,而 then 方法则属于微任务的回调,执行时机为宏任务执行之后,具体的事件循环机制后面开新坑总结。

resolve

构造 Promise 时,第一个参数 resolve 函数所接收的参数,可以是一个普通的值,也可以是一个 promise。如果接收的是一个 promise 实例,那么 promise 的状态取决于 resolve 接收的 promise 的状态:

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// 3s 后输出:Error: fail

这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。

resolve 与 return

调用resolvereject并不会终结 Promise 的参数函数的执行:

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

一般来说,调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。所以,最好在它们前面加上return:

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})
.catch
  1. Promise.prototype.catch()方法是.then(null | undefined, rejection),用于指定发生错误时的回调函数。

  2. Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,即错误总是会被下一个catch语句捕获。

  3. catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()catch() 方法。

  4. promise 内部的错误不会抛到外层,因此 try/catch 无法捕获:

    try {
       const p2 = new Promise((resolve, reject) => {
            throw 'err'
        })
        console.log('out')
    } catch(e) {
        console.log('catch you', e)
    }
    // out
    // 未捕获的异常:Uncaught (in promise) err

    因此最好 promise 后都用 catch 来捕获。

.finally()
  • ES2018 引入,即不管 Promise 对象最后状态如何,都会执行的操作。
  • finally方法的回调函数不接受任何参数

手写一个简易的 Promise

手写一个符合 PromiseA+ 规范的 Promise 并不简单,可以先简单根据上述 promise 的基本功能实现一个简易版本

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class MyPromise {
  constructor(fn) {
    this.state = PENDING
    // 收集 .then 中的 resolve 的回调函数
    this.resolveCallbacks = []
    // 收集 .then 中的 reject 的回调函数
    this.rejectCallbacks = []
    // resolve(result)
    this.result = null
    // reject(reason)
    this.reason = null
    // 由于 fn 有可能异常,需要捕获
    try {
      // Promise 一实例化就执行传入的 fn
      // 注意此处将函数当作参数传入,需要 bind 绑定上下文
      // 或者 resolve / reject 用箭头函数实现
      fn(this.resolve.bind(this), this.reject.bind(this))
    } catch (error) {
      this.reject(error)
    }
  }

  resolve(result) {
    if (this.state === PENDING) {
      // 修改状态
      this.state = RESOLVED
      this.result = result
      // 执行 then 中的 resolve 回调函数
      this.resolveCallbacks.forEach((cb) => cb(result))
    }
  }

  reject(reason) {
    if (this.state === PENDING) {
      // 修改状态
      this.state = REJECTED
      this.reason = reason
      // 执行 then 中的 reject 回调函数
      this.rejectCallbacks.forEach((cb) => cb(reason))
    }
  }

  then(onFulfilled, onRejected) {
    // 调用 .then 时如果还在 pending 则将回调先存起来,等待 resolve 后执行
    if (this.state === PENDING) {
      // 收集回调
      this.resolveCallbacks.push(onFulfilled)
      this.rejectCallbacks.push(onRejected)
    }
    // 如果调用 .then 时已经 resolve / reject, 那么直接执行对应的回调
    if (this.state === RESOLVED) {
      onFulfilled(this.result)
    }
    if (this.state === REJECTED) {
      onRejected(this.reason)
    }
  }
}

测试代码:

const myPromise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('resolved')
  }, 3000)
})

myPromise.then(
  (result) => {
    console.log(result)
  },
  (reason) => {
    console.log(reason)
  }
)

Promise 的设计思路实际上就是观察者模式:收集回调函数,触发通知,依次执行回调。

上述版本还未能达到 Promise / A+ 的规范,比如 链式调用 等。

符合 Promise / A+ 的 Promise

参考 Promise / A+ ,以下还需要补充下面几点:

  1. then 方法的参数可选
  2. onFulfilledonRejected是异步执行的,可以使用如 setTimeoutsetImmediate 之类的“宏任务”机制,或者使用诸如 MutationObserverprocess.nextTick 之类的“微任务”机制来实现。
  3. 链式调用 与 promise 解决步骤
值穿透

针对 then 的参数可选这一点,如果 onFulfilledonRejectd 不是函数,则会被忽略,并且将值传递给下一个 then。即所谓的值穿透,对 then 进行改造:

then(onFulfilled, onRejected) {
  // 如果不是函数,传递 result
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : (result) => result
  // 如果不是函数,抛出 reason
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason
          }
  // ... if (this.state === PENDING) ... 
}

当然当前的简易版 promise 还不支持链式调用,值穿透也就无从谈起。

异步执行

在实现链式调用之前,我们完善代码,使得onFulfilledonRejected是异步执行的。

当前执行以下代码:

var myPromise = new MyPromise((resolve, reject) => {
  resolve(1)
})

myPromise.then(
  (val) => {
    console.log(val)
  },
  (val) => {
    console.log(val)
  }
)

console.log(2)

现输出 1 再输出 2,显然不是异步的。根据 Promise/ A+ 的规范,这里我们可以通过 setTimeout 来改造 resolve 和 reject :

// ...
resolve(result) {
  // 通过 setTimeout 模拟异步
  setTimeout(() => {
    if (this.state === PENDING) {
      // ...
    }
  })
}

reject(reason) {
    // 通过 setTimeout 模拟异步
  setTimeout(() => {
    if (this.state === PENDING) {
       // ...
    }
  })
}
// ...

至此,输出顺序将是 2, 1。 符合预期

链式调用

现在来实现链式调用:

  1. then 需要返回一个 Promise
  2. then 拿到上一次 then 的返回值
  3. 如果 then 返回的是一个 Promise 则需要等待该 promise 状态发生变化后再执行下一个 then

promise2: 我们将 then 返回的 promise 命名为 promise2, 即 promise2 = promise1.then(onFulfilled, onRejected);

x: 将 onFulfilled 或者 onRejected 的返回值命名为 x,这个 x 可以是一个 thenable。

resolutionProcedure: promise 解决步骤。如果 x 具有 thenable 特性,我们就假设 x 的行为至少有点像 promise,它将试图使 promise 接收 x 的状态。否则,它使用值 x 执行 promise。之所以是 thenable 是为了可以兼容别的 promise(例如 fetch)。实际上这个方法在 Promise / A+ 中有很多情况分叉,我们先简化成以下:

// 分类讨论返回值,如果是Promise,那么等待Promise状态变更, 否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)

现在的 Promise 完整代码如下,接近完全体版本

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class MyPromise {
  constructor(fn) {
    this.state = PENDING
    // 收集 .then 中的 resolve 的回调函数
    this.resolveCallbacks = []
    // 收集 .then 中的 reject 的回调函数
    this.rejectCallbacks = []
    // resolve(result)
    this.result = null
    // reject(reason)
    this.reason = null
    // 由于 fn 有可能异常,需要捕获
    try {
      // Promise 一实例化就执行传入的 fn
      // 注意此处将函数当作参数传入,需要 bind 绑定上下文
      // 或者 resolve / reject 用箭头函数实现
      fn(this.resolve.bind(this), this.reject.bind(this))
    } catch (error) {
      this.reject(error)
    }
  }

  resolve(result) {
    setTimeout(() => {
      if (this.state === PENDING) {
        // 修改状态
        this.state = RESOLVED
        this.result = result
        // 执行 then 中的 resolve 回调函数
        this.resolveCallbacks.forEach((cb) => cb(result))
      }
    })
  }

  reject(reason) {
    setTimeout(() => {
      if (this.state === PENDING) {
        // 修改状态
        this.state = REJECTED
        this.reason = reason
        // 执行 then 中的 reject 回调函数
        this.rejectCallbacks.forEach((cb) => cb(reason))
      }
    })
  }

  then(onFulfilled, onRejected) {
    // 如果不是函数,传递 result
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : (result) => result
    // 如果不是函数,抛出 reason
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason
          }
    // 调用 .then 时如果还在 pending 则将回调先存起来,等待 resolve 后执行
    if (this.state === PENDING) {
      const promise2 = new MyPromise((resolve, reject) => {
        const resolvedCallBack = () => {
          try {
            const x = onFulfilled(this.result)
            // 简化逻辑的 resolutionProcedure
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
            // resolutionProcedure(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }

        const rejectCallBack = () => {
          try {
            const x = onRejected(this.result)
            // 简化逻辑的 resolutionProcedure
            x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
            // resolutionProcedure(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }

        // 收集回调
        this.resolveCallbacks.push(resolvedCallBack)
        this.rejectCallbacks.push(onRejected)
      })
      return promise2
    }
    // 如果调用 .then 时已经 resolve / reject, 那么直接执行对应的回调
    if (this.state === RESOLVED) {
      onFulfilled(this.result)
    }
    if (this.state === REJECTED) {
      onRejected(this.reason)
    }
  }
}
resolutionProcedure

最后根据 Promise / A+ 的规范来完善 resolutionProcedure,实际上就是一系列的判断:

const resolutionProcedure = (promise2, x, resolve, reject) => {
  if (promise2 === x) {
    return reject(new TypeError('Error'))
  }
  if (x instanceof MyPromise) {
    x.then(function(value) {
        resolutionProcedure(promise2, value, resolve, reject)
    }, reject)
	}
  let called = false
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then
      if (typeof then === 'function') {
        then.call(
          x,
          y => {
            if (called) return
            called = true
            resolutionProcedure(promise2, y, resolve, reject)
          },
          e => {
            if (called) return
            called = true
            reject(e)
          }
        )
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(x)
  }
}

符合 Promise / A+ 的 Promise

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

const resolutionProcedure = (promise2, x, resolve, reject) => {
  if (promise2 === x) {
    return reject(new TypeError('Error'))
  }
  if (x instanceof MyPromise) {
    x.then(function (value) {
      resolutionProcedure(promise2, value, resolve, reject)
    }, reject)
  }
  let called = false
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then
      if (typeof then === 'function') {
        then.call(
          x,
          (y) => {
            if (called) return
            called = true
            resolutionProcedure(promise2, y, resolve, reject)
          },
          (e) => {
            if (called) return
            called = true
            reject(e)
          }
        )
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(x)
  }
}

class MyPromise {
  constructor(fn) {
    this.state = PENDING
    // 收集 .then 中的 resolve 的回调函数
    this.resolveCallbacks = []
    // 收集 .then 中的 reject 的回调函数
    this.rejectCallbacks = []
    // resolve(result)
    this.result = null
    // reject(reason)
    this.reason = null
    // 由于 fn 有可能异常,需要捕获
    try {
      // Promise 一实例化就执行传入的 fn
      // 注意此处将函数当作参数传入,需要 bind 绑定上下文
      // 或者 resolve / reject 用箭头函数实现
      fn(this.resolve.bind(this), this.reject.bind(this))
    } catch (error) {
      this.reject(error)
    }
  }

  resolve(result) {
    setTimeout(() => {
      if (this.state === PENDING) {
        // 修改状态
        this.state = RESOLVED
        this.result = result
        // 执行 then 中的 resolve 回调函数
        this.resolveCallbacks.forEach((cb) => cb(result))
      }
    })
  }

  reject(reason) {
    setTimeout(() => {
      if (this.state === PENDING) {
        // 修改状态
        this.state = REJECTED
        this.reason = reason
        // 执行 then 中的 reject 回调函数
        this.rejectCallbacks.forEach((cb) => cb(reason))
      }
    })
  }

  then(onFulfilled, onRejected) {
    // 如果不是函数,传递 result
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : (result) => result
    // 如果不是函数,抛出 reason
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason
          }
    // 调用 .then 时如果还在 pending 则将回调先存起来,等待 resolve 后执行
    if (this.state === PENDING) {
      const promise2 = new MyPromise((resolve, reject) => {
        const resolvedCallBack = () => {
          try {
            const x = onFulfilled(this.result)
            resolutionProcedure(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }

        const rejectCallBack = () => {
          try {
            const x = onRejected(this.result)
            resolutionProcedure(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }

        // 收集回调
        this.resolveCallbacks.push(resolvedCallBack)
        this.rejectCallbacks.push(onRejected)
      })
      return promise2
    }
    // 如果调用 .then 时已经 resolve / reject, 那么直接执行对应的回调
    if (this.state === RESOLVED) {
      onFulfilled(this.result)
    }
    if (this.state === REJECTED) {
      onRejected(this.reason)
    }
  }
}

至此我们完成了一个 符合 Promise / A+ 的 Promise,实际上 Promise 还会提供一些如 .all, .race 之类的 api,这里也实现一下。

Promise.all

Promise.all 接收多个 Promise 实例,返回一个新的 Promise 实例。例如:

const p = Promise.all([p1, p2, p3])

上述的 p1, p2, p3 都是 Promise 实例,如果不是则会用 Promise.resolve 转成 promise。此外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

实现如下:

class MyPromise {
  // ...
  all(arr) {
    // 用于存储返回值
    let list = []
    // 记录 resolve 了多少个 promise
    let length = 0
    // 执行 promise 的过程中是否有异常
    let hasError = false
    // 返回一个新的 Promise
    return new MyPromise((resolve, reject) => {
      for (let i = 0; i < arr.length; i++) {
        // 判断元素是否为 MyPromise, 否则 resolve
        const p =
          arr[i] instanceof MyPromise ? arr[i] : MyPromise.resolve(arr[i])
        p.then((data) => {
          list[i] = data
          length++
          if (length === arr.length) {
            // 如果全部 promise 都 resolved
            resolve(list)
          }
        }).catch((error) => {
          if (!hasError) {
            // 第一个被 reject 的实例的返回值,会传递给 p 的回调函数
            reject(error)
          }
          hasErr = true
        })
      }
    })
  }
  // ...
}

Promise.race

Promise.race()`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

class MyPromise {
  // ...
  race(arr) {
    // 是否有结果
    let hasResult = false
    // 返回一个新的 Promise
    return new MyPromise((resolve, reject) => {
      for (let i = 0; i < arr.length; i++) {
        // 判断元素是否为 MyPromise, 否则 resolve
        const p =
          arr[i] instanceof MyPromise ? arr[i] : MyPromise.resolve(arr[i])
        p.then((data) => {
          if (!hasResult) {
            resolve(data)
          }
          hasResult = true
        }).catch((error) => {
          if (!hasResult) {
            reject(error)
          }
          hasResult = true
        })
      }
    })
  }
  // ...
}