promise

186 阅读11分钟

本质上 Promise 是一个函数返回的对象

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation.

Promise是一个对象,它被用作延迟(可能是异步)计算的最终结果的占位符。

Any Promise object is in one of three mutually exclusive states: fulfilled, rejected, and pending:

任何Promise对象都处于以下三种互斥状态之一:已完成、已拒绝和待处理:

什么是回调地狱

  • 多层嵌套
  • 每种问题存在成功和失败的两条路径,错误处理的路径非常难受

Promise 通过什么解决回调地狱

  • 回调函数延迟绑定:在 .then 中传入回调函数,而不是直接在参数里面占个位置声明回调函数
  • 返回值穿透:.then 返回的还是 promise,可以继续 .then
  • 错误冒泡:只需要在最后面 .catch ,前面产生的错误会自动向后传递

Promise 为什么要引入微任务?

Promise解决回调可以通过三种方式,但是采用的是最后一种:

  1. 使用同步回调,直到异步任务进行完,再进行后面的任务。阻塞脚步,浪费CPU资源
  2. 使用异步回调,将回调函数放在进行宏任务队列的队尾。任务队列非常长时,回调函数迟迟无法执行,实时性不好
  3. 使用异步回调,将回调函数放到当前宏任务中的最后面。

Promise 如何实现链式调用?

首先,promise 有状态state,有下面三种取值:

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

然后我们需要 value 来记录应该返回的值,peomise里面是有return 结果的,需要记录下来。

我们还需要 error 来记录遇到错误时的错误信息,用以返回。

再加上成功的回调函数(.then里面调用),和失败的回调函数(.catch里面调用)。

上面这些值都是以闭包的形式记录的

  • 成功和失败的回调函数记录是以数组的形式记录的,因为可以 const p1 = new Promise(); ,然后 p1.then(...);p1.then(...);,即可以绑定多个回调函数。
function CustomPromise(executor){
  let self = this
  self.value = null
  self.error = null
  self.status = PENDING
  self.onFulfilledCallbacks = []
  self.onRejectedCallbacks = []

  const resolve = (value) => {
    if (self.status !== PENDING) return // 只会执行第一个 resolve 或者第一个 reject
    setTimeout(() => {
      self.status = FULFILLED
      self.value = value
      self.onFulfilledCallbacks.map(cb => cb(self.value))
    })
  }

  const reject = (error) => {
    if (self.status !== PENDING) return
    setTimeout(() => {
      self.status = REJECTED
      self.error = error
      self.onRejectedCallbacks.map(cb => cb(self.error))
    })
  }

  executor(resolve, reject) // 我们把 定义好的resolve和reject放进去,在执行后会被自动调用。
}

.then 函数是最难以理解的地方,先写一版最简单的:

  • 对于传入的成功回调和失败回调,需要判断是否是函数
  • 参数放入 onFulfilledCallbacks 和 onRejectedCallbacks 数组中
  • 如果当前 promise 的状态已经是完成时,则不需要放入,直接执行即可
CustomPromise.prototype.then = function(onFulfilled, onRejected) {
  // 成功回调不传,给它一个默认函数
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
  // 对于失败回调直接抛错
  onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };

  if (this.status === PENDING) {
    this.onFulfilledCallbacks.push(onFulfilled);
    this.onRejectedCallbacks.push(onRejected);
  } else if (this.status === FULFILLED) {
    onFulfilled(this.value);
  } else {
    onRejected(this.error);
  }
  return this;
}

这样存在一个问题,因为返回的是 this,而且是直接返回的,其实这个时候是无法做到链式调用的

const res = {
  aaa: {
    name: 'aaa'
  },
  bbb: {
    name: 'bbb'
  }
}

const generatePromise = (str) => {
  return new CustomPromise((resolve, reject) => {
    setTimeout(() => {
      console.log(str)
      resolve(res[str])
    }, 1000)
  })
}

generatePromise('aaa').then((res1) => {
  console.log(res1)
  return generatePromise('bbb')
}).then((res2) => {
  console.log(res2)
})

// aaa
// {name: 'aaa'}
// {name: 'aaa'} // 立即打印了出来,因为 .then 里面 return this(第一个promise),然后把第二个.then 的回调函数也放到第一个promise的 onFulfilledCallbacks 里面了,所以在 generatePromise('aaa') 执行完后,立即触发了两个回调函数
// bbb

所以我们需要修改返回值,首先修改 PENDING 状态时的返回值

  • 通过返回一个新的 Promise 实现,我们叫他 bridgePromise
  • 第二个 .then 的回调函数会放到这个 bridgePromise 的 onFulfilledCallbacks 里面
  • 那么我们就应该往初始的 promise 里面放入一个回调函数,在这个回调函数里面触发 bridgePromise 的 resolve,修改 bridgePromise 的状态为完成,从而触发 bridgePromise 的 onFulfilledCallbacks,里面就有第二个 .then 的回调函数
CustomPromise.prototype.then = function (onFulfilled, onRejected) {
  let bridgePromise;
  let self = this;
  if (self.status === PENDING) {
    return bridgePromise = new CustomPromise((resolve, reject) => {
      self.onFulfilledCallbacks.push((value) => {
        try {
          // 看到了吗?要拿到 then 中回调返回的结果。
          let x = onFulfilled(value);
          resolve(x);
        } catch (e) {
          reject(e);
        }
      });
      self.onRejectedCallbacks.push((error) => {
        try {
          let x = onRejected(error);
          resolve(x);
        } catch (e) {
          reject(e);
        }
      });
    });
  }
  //...
}

但是还有问题,主要是 then 中回调返回的结果可能又是个 Promise,这个时候我们没有处理,所以不能直接 resolve(x),即 x 是个 promise

要创建一个新的 resolve 来处理他

function resolvePromise(bridgePromise, x, resolve, reject) {
  //如果x是一个promise
  if (x instanceof CustomPromise) {
    // 拆解这个 promise ,直到返回值不为 promise 为止
    if (x.status === PENDING) {
      x.then(y => {
        resolvePromise(bridgePromise, y, resolve, reject);
      }, error => {
        reject(error);
      });
    } else {
      x.then(resolve, reject); // x已经完成,.then 会直接触发执行 resolve 和 reject
    }
  } else {
    // 非 Promise 的话直接 resolve 即可
    resolve(x);
  }
}

其中,resolvePromise 中的参数 resolve 和 reject 始终都是第一个 Promise 的 resolve 和 reject,他们会控制第一个传入的 bridgePromise 的状态。

如果状态不是 PENDING,也需要做响应的处理

    CustomPromise.prototype.then = function (onFulfilled, onRejected) {
      // 防止不传参数
      onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
      onRejected = typeof onRejected === "function" ? onRejected : error => { throw error }

      let bridgePromise
      let self = this
      if (this.status === PENDING) {
        return bridgePromise = new CustomPromise((resolve, reject) => {
          self.onFulfilledCallbacks.push((value) => {
            try {
              let x = onFulfilled(value)
              resolvePromise(bridgePromise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          })

          self.onRejectedCallbacks.push((error) => {
            try {
              let x = onRejected(error)
              resolve(x)
            } catch (e) {
              reject(e)
            }
          })
        })
      } else if (this.status === FULFILLED) {
        return bridgePromise = new CustomPromise((resolve, reject) => {
          try {
            // 状态变为成功,会有相应的 self.value
            let x = onFulfilled(self.value)
            // 暂时可以理解为 resolve(x),后面具体实现中有拆解的过程
            resolvePromise(bridgePromise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      } else if (this.status === REJECTED) {
        return bridgePromise = new CustomPromise((resolve, reject) => {
          try {
            // 状态变为失败,会有相应的 self.error
            let x = onRejected(self.error)
            resolvePromise(bridgePromise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
    }

这个时候,上面的例子就能正常执行了

const res = {
  aaa: {
    name: 'aaa'
  },
  bbb: {
    name: 'bbb'
  }
}

const generatePromise = (str) => {
  return new CustomPromise((resolve, reject) => {
    setTimeout(() => {
      console.log(str)
      resolve(res[str])
    }, 1000)
  })
}

generatePromise('aaa').then((res1) => {
  console.log(res1)
  return generatePromise('bbb')
}).then((res2) => {
  console.log(res2)
})

// 1秒钟后打印 aaa,然后返回 res1 {name: 'aaa'} 并打印
// aaa
// {name: 'aaa'}
// 再过一秒钟后打印 bbb,然后返回 res2 {name: 'bbb'} 并打印
// bbb
// {name: 'bbb'}

如果 我们在 .then 方法里面打印当前的this,会发现在一开始就打印出来两个 CustomPromise,这两个分别是 generatePromise('aaa').then 以及再 .then 中生成的新的 Promise(匿名的 return bridgePromise = new CustomPromise... )

如果我们给 CustomPromise 编号,并在生成时打印出来,上面的代码一共会生成 5 个 Promise

  1. 调用 generatePromise('aaa') 生成了 0 号 Promise
  2. 调用 generatePromise('aaa').then 生成了 1 号 Promise(bridgePromise),把打印 res1 的回调函数放入其中
  3. 调用 generatePromise('aaa').then().then 生成了 2 号 Promise(bridgePromise),把打印 res2 的回调函数放入其中
  4. 开始执行 generatePromise('aaa') 里面的异步函数,执行完毕,触发 resolve(),执行打印 res1 的回调函数
  5. 开始 return generatePromise('bbb'),此时调用了 generatePromise('bbb'),又生成了 3 号 Promise
  6. 因为返回的是 Promise,在 1 号 Promise 中,x 是Promise,就需要使用 .then,再生成 4 号 Promise (也是 bridgePromise)来处理
let x = onFulfilled(value) // return generatePromise('bbb') 新生成的 Promise
resolvePromise(bridgePromise, x, resolve, reject)

function resolvePromise(bridgePromise, x, resolve, reject) {
      console.log('调用了 resolvePromise', bridgePromise, x.status)
      //如果x是一个promise
      if (x instanceof CustomPromise) {
        // 拆解这个 promise ,直到返回值不为 promise 为止
        if (x.status === PENDING) {
          x.then(y => {
            resolvePromise(bridgePromise, y, resolve, reject)
          }, error => {
            reject(error)
          })
        } else {
          x.then(resolve, reject)
        }

catch

catch 方法其实是 then 方法的语法糖

CustomPromise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

因为 then 方法中,

onRejected = typeof onRejected === "function" ? onRejected : error => { throw error };

所以,一旦其中有一个PENDING状态的 Promise 出现错误后状态必然会变为失败, 然后执行 onRejected函数,而这个 onRejected 执行又会抛错,把新的 Promise 状态变为失败,新的 Promise 状态变为失败后又会执行onRejected......就这样一直抛下去,直到用catch 捕获到这个错误,才停止往下抛。

Promise.resolve

主要看传参

  • 传参为一个 Promise, 直接返回它。
  • 传参为一个 thenable 对象,返回的 Promise 会跟随这个对象,采用它的最终状态作为自己的状态
  • 其他情况,直接返回以该值为成功状态的 promise 对象。
CustomPromise.resolve = (param) => {
  if(param instanceof CustomPromise) return param;
  return new CustomPromise((resolve, reject) => {
    if(param && param.then && typeof param.then === 'function') {
      // param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然
      param.then(resolve, reject);
    }else {
      resolve(param);
    }
  })
}

Promise.reject

Promise.reject 中传入的参数会作为一个 reason 原封不动地往下传, 实现如下:

CustomPromise.reject = function (reason) {
  return new CustomPromise((resolve, reject) => {
    reject(reason);
  });
}

Promise.finally

CustomPromise.prototype.finally = function(callback) {
  this.then(value => {
    return CustomPromise.resolve(callback()).then(() => {
      return value;
    })
  }, error => {
    return CustomPromise.resolve(callback()).then(() => {
      throw error;
    })
  })
}

Promise.all

对于 all 方法而言,需要完成下面的核心功能:

  • 传入参数为一个空的可迭代对象,则直接进行 resolve。
  • 如果参数中有一个 promise 失败,那么 Promise.all 返回的 promise 对象失败。
  • 在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
CustomPromise.all = function(promises) {
  return new CustomPromise((resolve, reject) => {
    let result = [];
    let index = 0;
    let len = promises.length;
    if(len === 0) {
      resolve(result);
      return;
    }
   
    for(let i = 0; i < len; i++) {
      // 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise
      CustomPromise.resolve(promise[i]).then(data => {
        result[i] = data;
        index++;
        if(index === len) resolve(result); 
      }).catch(err => {
        reject(err);
      })
    }
  })
}

Promise.race

只要有一个 promise 执行完,直接 resolve 并停止执行。

CustomPromise.race = function(promises) {
  return new CustomPromise((resolve, reject) => {
    let len = promises.length;
    if(len === 0) return;
    for(let i = 0; i < len; i++) {
      CustomPromise.resolve(promise[i]).then(data => {
        resolve(data); // 只有最先的 resolve 会改变上面 new Promise 的状态,后续的尝试性变更会因为 status !== PENDING,直接 return
        return;
      }).catch(err => {
        reject(err);
        return;
      })
    }
  })
}

小结

以上是 ES6 的 promise 的 api 实现,在后续发展中

  • ES8 引入了 async、await
  • ES11 引入了 allSettled、any
  • ES12 改进了 Promise.resolve() 和 Promise.reject(),引入了 AggregateError 类(一次操作中处理多个错误)

然后我继续来实现这些效果

Promise.any 只要其中的一个 Promise 成功解决,就返回那个已经成功的 Promise,否则返回全部错误结果

分析需求:

  • 接受参数是数组,不是数组要报错
  • 返回一个新的 Promise,遍历处理参数数组
  • 处理时先通过 Promise.resolve 包装一下,以免参数不是 promise
  • 在遇到一个成功的情况,直接 resolve
  • 计数错误数量,如果达到参数数组的长度,说明全部失败,返回全部错误信息
  • 如果不支持表示多个错误信息的 AggregateError,需要自己实现这个内置的错误对象
CustomPromise.any = function (promises) {
  return new CustomPromise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      throw new TypeError('Argument must be an array') // 校验参数
    }

    let rejectedCount = 0 // 统计错误个数
    let errors = [] // 记录错误信息

    promises.forEach((promise, index) => {
      CustomPromise.resolve(promise).then(
        value => {
          resolve(value) // 一旦有一个 promise 解决,立即返回此 promise
        },
        error => {
          rejectedCount++
          errors[index] = error
          if (rejectedCount === promises.length) {
            // 全部失败时,返回全部失败的错误信息
            reject(new AggregateError(errors, 'All promises were rejected'))
          }
        })
    })
  })
}

还是拿上面的做例子

const res = {
  aaa: {
    name: 'aaa'
  },
  bbb: {
    name: 'bbb'
  }
}

const generatePromise = (str, time) => {
  return new CustomPromise((resolve, reject) => {
    setTimeout(() => {
      console.log(str)
      if (res[str]) {
        resolve(res[str])
      } else {
        reject(new Error('can not find param: ', str))
      }
    }, time)
  })
}

CustomPromise.any([generatePromise('aaa', 1000), generatePromise('bbb', 2000), generatePromise('ccc', 500)]).then(res => {
  console.log('promise.any excute success, result is ', res)
}).catch(error => {
  console.log('promise.any excute fail, error is ', error)
})

// ccc // 500毫秒后打印 ccc,放入错误数组
// aaa // 1秒后打印 aaa,执行成功,触发 resolve
// promise.any excute success, result is  {name: 'aaa'} // 执行 any 的 then 回调
// bbb // 2秒后打印 bbb,执行成功,但 resolve不会再触发

修改测试用例,打印错误信息

CustomPromise.any([generatePromise('aaaa', 1000), generatePromise('bbbb', 2000), generatePromise('ccc', 500)]).then(res => {
  console.log('promise.any excute success, result is ', res)
}).catch(error => {
  console.log('promise.any excute fail, error is ', error)
  error.errors.forEach((err, index) => {
    console.error(`Error ${index + 1}:`, err)
  })
})

// ccc   // 500毫秒后打印 ccc,放入错误数组
// aaaa  // 1000毫秒后打印 aaaa,放入错误数组
// bbbb  // 2000毫秒后打印 bbbb,放入错误数组
// promise.any excute fail, error is  AggregateError: All promises were rejected // 全部执行错误
//   at this.html:153:24
//   at this.html:87:23
//   at this.html:41:46
//   at Array.map (<anonymous>)
//   at this.html:41:36
// 打印三个错误

简单的 AggregateError polyfill

class AggregateErrorPolyfill extends Error {
  constructor(errors, message) {
    super(message);
    this.errors = errors;
    this.name = 'AggregateError';
  }
}

使用

try {
  throw new AggregateErrorPolyfill([
    new Error('Error 1'),
    new Error('Error 2'),
    new Error('Error 3')
  ], 'All promises were rejected');
} catch (error) {
  if (error instanceof AggregateErrorPolyfill) {
    console.error('Caught an AggregateError:', error.message);
    error.errors.forEach((err, index) => {
      console.error(`Error ${index + 1}:`, err.message);
    });
  } else {
    console.error('Caught an unexpected error:', error);
  }
}

Promise.allSettled

在所有输入的 Promise 都被解决后返回一个 Promise,无论这些 Promise 是成功解决还是被拒绝。

分析需求:

  • 参数校验,需要是数组
  • 完成个数记录,在成功回调和失败回调中,都记录结果
  • 如果个数等于参数数组个数,返回结果,只有 resolve,不会 reject
  • 结果是个对象,status表明状态,成功就是value的值,失败就是reason的错误原因
  • 其余的和 any 很像
CustomPromise.allSettled = function (promises) {
  return new CustomPromise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      throw new TypeError('Argument must be an array') // 校验参数
    }

    let completedCount = 0 // 统计个数
    let results = [] // 记录结果

    promises.forEach((promise, index) => {
      CustomPromise.resolve(promise).then(
        value => {
          completedCount++
          results[index] = { status: 'fulfilled', value }
          if (completedCount === promises.length) {
            resolve(results)
          }
        },
        reason => {
          completedCount++
          results[index] = { status: 'rejected', reason }
          if (completedCount === promises.length) {
            resolve(results)
          }
        })
    })
  })
}

面试题汇总

  • 如果要你实现一个 Promise.async 方法,参数是一个 promise 数组,然后其中的 promise 会同步执行,该怎么实现?

这题我想了很久,然后发现很奇怪,因为参数是 promise 数组,而我们的 all,race 等,其实在参数里面已经开始运行了,只不过我们在 all 方法里面通过同步执行的代码,立即生成了很多的回调函数而已,然后这些 promise 执行完之后,才会一个个触发回调函数,所以根本不可能实现什么同步运行,promise 是同步启动的,但是执行是异步执行的,我们的 .then 再 .then ,也只是在第一个 promise 执行完的回调函数里面再生成第二个 promise 才实现的。

我找了一个类似的可能符合面试官要求的答案,他的参数是生成 promise 的参数,然后在内部再生成 promise,但是我认为这个题目是有歧义的,无意义的

CustomPromise.async = async (arr) => {
  if (!Array.isArray(arr)) {
    throw new TypeError('Argument must be an array') // 校验参数
  }

  let p = CustomPromise.resolve()

  arr.forEach((params, index) => {
    p = p.then(_ => new CustomPromise((resolve) => {
      resolve(generatePromise(...params)) // 在回调函数里面再生成 promise,否则立即就会执行
    }))
  })

  return CustomPromise.resolve(p)
}

CustomPromise.async([['aaa', 1000], ['bbb', 2000], ['ccc', 500]]).then(res => {
  console.log(res)
}).catch(error => {
  console.log(error)
})

参考

作者:神三元 链接:juejin.cn/post/684490… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。