拆解实现 Promise 及其周边

973 阅读11分钟

Promise对象用于表示一个异步操作的最终状态(成功/失败)及其结果值。 —— MDN

Promise的出现使我们可以优雅地处理异步操作,脱离回调地狱的痛苦。
有一利必有一弊,它成为了面试必考问题之一 ,转化为另一种痛苦生活在我们身边... 当然,这是句玩笑话~ 🤣

上次看Promise大概一个多月前了,花了两天的空暇时间基于Promise/A+手写了一遍,大多云里雾里。为了巩固,趁着周末又写了一遍,彻底梳理清楚✌。本文主要还是拆解实现Promise丐版及其周边。

基本特点

  1. 新建Promise会立即执行,无法中断。
  2. Promise有三种状态:pengdingfulfilledrejected。只有pengding -> fulfilledpending -> rejected两种状态流,且状态更改后,不可再次更改。
  3. resolve为成功状态;reject为失败状态。

Promise基本使用👇

const p = new Promise((resolve, reject) => {
  resolve('success')
  reject('error')
})
p.then(value => {
 console.log(value)
}, reason => {
 console.log(reason)
})
// success

拆解实现Promise丐版

实现基本逻辑

// promise.js
const PENGDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
  let that = this
  // 初始状态为pengding
  this.status = PENGDING
  // 记录成功返回的结果值
  this.value = null 
  // 记录失败返回的结果值
  this.reason = null
  function resolve(value) {
    if (that.status === PENGDING) {
      that.status = FULFILLED
      that.value = value
    }
  }
  function reject(reason) {
    if (that.status === PENGDING) {
      that.status = REJECTED
      that.reason = reason
    }
  }
  executor(resolve, reject)
}
Promise.prototype.then = function (onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    // 调用成功回调
    onFulfilled(this.value)
  } else if (this.status === REJECTED) {
    // 调用失败回调
    onRejected(this.reason)
  }
}
module.exports = Promise

引入实现的丐版Promise,执行下上述 🌰

// main.js
const Promise = require('./promise')
const p = new Promise((resolve, reject) => {
  resolve('success')
  reject('error')
})
p.then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  }
)

执行打印出success

处理异步情况

// main.js
const Promise = require('./promise')
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  })
})
p.then(
  (value) => {
    console.log(value)
  },
  (reason) => {
    console.log(reason)
  }
)

执行无输出结果,打印then函数的this.status发现状态仍为pengding
这是为什么呢? 知道事件循环的朋友们应该知道setTimeout是异步且属于宏任务,将会在下个事件循环宏任务中执行,而p.then为当前宏任务下的同步代码。因此,that.status仍为pengding,所以无输出结果。

既然这样,我们需要添加两个字段onFulfilledonRejected去缓存成功和失败的回调,在resolvereject时候再去执行缓存函数。

// promise.js
const PENGDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
  let that = this
  // 初始状态为pengding
  this.status = PENGDING
  // 记录成功返回的结果值
  this.value = null
  // 记录失败返回的结果值
  this.reason = null
  this.onFulfilled = null
  this.onRejected = null
  function resolve(value) {
    if (that.status === PENGDING) {
      that.status = FULFILLED
      that.value = value
      // 有缓存成功回调,则执行
      that.onFulfilled && that.onFulfilled(value)
    }
  }
  function reject(reason) {
    if (that.status === PENGDING) {
      that.status = REJECTED
      that.reason = reason
      // 有缓存失败回调,则执行
      that.onRejected && that.onRejected(value)
    }
  }
  executor(resolve, reject)
}
Promise.prototype.then = function (onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    // 调用成功回调
    onFulfilled(this.value)
  } else if (this.status === REJECTED) {
    // 调用失败回调
    onRejected(this.reason)
  } else {
    // 挂载状态下缓存成功和失败回调
    this.onFulfilled = onFulfilled
    this.onRejected = onRejected
  }
}
module.exports = Promise

改进后,执行上述异步情况代码,成功打印success

再在异步情况下加点料🍕,多次执行不同的p.then函数。

// main.js
const Promise = require('./promise')

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  })
})
p.then(
  (value) => {
    console.log('第一次', value)
  },
  (reason) => {
    console.log(reason)
  }
)

p.then(
  (value) => {
    console.log('第二次', value)
  },
  (reason) => {
    console.log(reason)
  }
)
// 第二次 success

发现我们第一个p.then代码没有被执行。
来分析下原因:目前我们只用一个变量去存储成功和失败的回调,当我setTimeout执行之前,我的两个p.then已按顺序执行完毕。那么,第二个p.then就将覆盖第一个p.then所赋值的回调函数,所以执行结果为第二次 success

改进下代码,我们用数组的方式去存储所有的回调函数。

const PENGDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
  let that = this
  // 初始状态为pengding
  this.status = PENGDING
  // 记录成功返回的结果值
  this.value = null
  // 记录失败返回的结果值
  this.reason = null
  // 数组记录所有的成功回调
  this.onFulfilled = []
  // 数组记录所有的失败回调
  this.onRejected = []
  function resolve(value) {
    if (that.status === PENGDING) {
      that.status = FULFILLED
      that.value = value
      // 有缓存成功回调,则执行
      that.onFulfilled.forEach((fn) => fn(value))
    }
  }
  function reject(reason) {
    if (that.status === PENGDING) {
      that.status = REJECTED
      that.reason = reason
      // 有缓存失败回调,则执行
      that.REJECTED.forEach((fn) => fn(reason))
    }
  }
  executor(resolve, reject)
}
Promise.prototype.then = function (onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    // 调用成功回调
    onFulfilled(this.value)
  } else if (this.status === REJECTED) {
    // 调用失败回调
    onRejected(this.reason)
  } else {
    // 挂载状态下缓存所有的成功和失败回调
    this.onFulfilled.push(onFulfilled)
    this.onRejected.push(onRejected)
  }
}
module.exports = Promise

处理链式调用及值穿透(同步)

Promise的重中之重就是链式调用,主要处理的就是下述三种情况。根据Promise/A+的思想,每次执行完promise.then就创建新的promise,并把上一个then的返回值传递给下个promisethen方法,就可达到链式调用及值穿透的效果。resolvereject同理。

// main.js
const Promise = require('./promise')

const p = new Promise((resolve, reject) => {
  // 目前只处理同步链式调用
  resolve('success')
})

// 返回普通值
p.then((value) => {
  return value
}).then((value) => {
  console.log(value)
})
// 报错 Cannot read property 'then' of undefined

// 返回promise
p.then((value) => {
  return new Promise((resolve, reject) => {
     resolve(value)
  })
}).then((value) => {
  console.log(value)
})
// 报错 Cannot read property 'then' of undefined

// 值穿透
p.then().then((value) => {
  console.log(value)
})
// 报错 Cannot read property 'then' of undefined

改进代码主要部分如下。

...
Promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  // 值穿透问题
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  onRejected = typeof onRejected === 'function' ? onRejected : (reason) => { throw reason }

  const p2 = new Promise((resolve, reject) => {
    if (that.status === FULFILLED) {
      // 调用成功回调
      const x = onFulfilled(that.value)
      resolvePromise(x, resolve, reject)
    } else if (that.status === REJECTED) {
      // 调用失败回调
      const x = onRejected(that.reason)
      resolvePromise(x, resolve, reject)
    } else {
      // 挂载状态下缓存所有的成功和失败回调
      that.onFulfilled.push(onFulfilled)
      that.onRejected.push(onRejected)
    }
  })
  return p2
}
function resolvePromise(x, resolve, reject) {
  // 如果 x 是 promise 对象执行其 then 函数(参数为p2的回调函数,而非返回的x的回调函数)
  if (x instanceof Promise) {
    x.then(
      (value) => resolve(value),
      (reason) => reject(reason)
    )
  } else {
    // 如果 x 是 普通值
    resolve(x)
  }
}
module.exports = Promise

继续执行上述三个例子的代码,均可得出success的结果。

  1. 如果是普通值,直接resolve返回值x
  2. 如果是Promise,执行x.then。注意!!!then函数传参是p2resolvereject,所以执行const x = onFulfilled(that.value)时,执行的是p2resolve函数,从而将值传递下去。
  3. 值穿透在没有resolvereject参数的前提下,判断入参是否是函数,不是的话,赋值默认函数。

处理链式调用及值穿透(异步)

main.js内逻辑更改为异步的情况。

// main.js
const Promise = require('./promise')

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  })
})

...

来分析下原因:首先p.thenp.then.then都是第一轮事件循环,所以会于setTimeout去执行。其次,异步的情况下,我们是缓存成功和回调函数,等待resolvereject的时候再去执行。但我们缓存的仅仅是回调函数,并不是一个promise对象,所以只要再包装一层即可。

// promise.js
Promise.prototype.then = function (onFulfilled, onRejected) {
  ... 
  const p2 = new Promise((resolve, reject) => {
    if (that.status === FULFILLED) {
      ...
    } else if (that.status === REJECTED) {
      ...
    } else {
      // 挂载状态下缓存所有的成功和失败回调
      that.onFulfilled.push(() => {
        const x = onFulfilled(that.value)
        resolvePromise(x, resolve, reject)
      })
      that.onRejected.push(() => {
        const x = onRejected(that.reason)
        resolvePromise(x, resolve, reject)
      })
    }
  })
  return p2
}

处理返回自身的情况

原生promise会将与返回与自身相等的错误情况抛出Chaining cycle detected for promise #<Promise>

const p = new Promise((resolve, reject) => {
   resolve('success')
})

const p1 = p.then((value) => {
  console.log(value)
  return p1
})

resolvePromise增加p2传参,并添加判断

Promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  // 值穿透问题
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) => {
          throw reason
        }

  const p2 = new Promise((resolve, reject) => {
    if (that.status === FULFILLED) {
      // 调用成功回调
      const x = onFulfilled(that.value)
      resolvePromise(p2, x, resolve, reject)
    } else if (that.status === REJECTED) {
      // 调用失败回调
      const x = onRejected(that.reason)
      resolvePromise(p2, x, resolve, reject)
    } else {
      // 挂载状态下缓存所有的成功和失败回调
      that.onFulfilled.push(() => {
        const x = onFulfilled(that.value)
        resolvePromise(p2, x, resolve, reject)
      })
      that.onRejected.push(() => {
        const x = onRejected(that.reason)
        resolvePromise(p2, x, resolve, reject)
      })
    }
  })
  return p2
}
function resolvePromise(p2, x, resolve, reject) {
  // 如果返回自身抛出错误
  if (x === p2)
    reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  // 如果 x 是 promise 对象
  if (x instanceof Promise) {
    x.then(
      (value) => resolve(value),
      (reason) => reject(reason)
    )
  } else {
    // 如果 x 是 普通值
    resolve(x)
  }
}

执行报错p1 is not defined,查看堆栈报错由于resolvePromise(p2, x, resolve, reject)中的p2未初始化。
image.png 根据promise/A+规范提示,可巧妙利用宏任务/微任务去解决这个问题,这边我选择setTimeout宏任务的形式。 更改代码如下:

Promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  // 值穿透问题
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) => {
          throw reason
        }

  const p2 = new Promise((resolve, reject) => {
    if (that.status === FULFILLED) {
      // 调用成功回调
      setTimeout(() => {
        const x = onFulfilled(that.value)
        resolvePromise(p2, x, resolve, reject)
      })
    } else if (that.status === REJECTED) {
      // 调用失败回调
      setTimeout(() => {
        const x = onRejected(that.reason)
        resolvePromise(p2, x, resolve, reject)
      })
    } else {
      // 挂载状态下缓存所有的成功和失败回调
      that.onFulfilled.push(() => {
        setTimeout(() => {
          const x = onFulfilled(that.value)
          resolvePromise(p2, x, resolve, reject)
        })
      })
      that.onRejected.push(() => {
        setTimeout(() => {
          const x = onRejected(that.reason)
          resolvePromise(p2, x, resolve, reject)
        })
      })
    }
  })
  return p2
}

增加错误处理

主要捕获构造函数then函数的错误。用try/catch形式捕获

const PENGDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
  let that = this
  // 初始状态为pengding
  this.status = PENGDING
  // 记录成功返回的结果值
  this.value = null
  // 记录失败返回的结果值
  this.reason = null
  // 数组记录所有的成功回调
  this.onFulfilled = []
  // 数组记录所有的失败回调
  this.onRejected = []
  function resolve(value) {
    if (that.status === PENGDING) {
      that.status = FULFILLED
      that.value = value
      // 有缓存成功回调,则执行
      that.onFulfilled.forEach((fn) => fn(value))
    }
  }
  function reject(reason) {
    if (that.status === PENGDING) {
      that.status = REJECTED
      that.reason = reason
      // 有缓存失败回调,则执行
      that.onRejected.forEach((fn) => fn(reason))
    }
  }
  // 构造函数捕获错误
  try {
    executor(resolve, reject)
  } catch (err) {
    reject(err)
  }
}
Promise.prototype.then = function (onFulfilled, onRejected) {
  let that = this
  // 值穿透问题
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) => {
          throw reason
        }

  const p2 = new Promise((resolve, reject) => {
    if (that.status === FULFILLED) {
      // 调用成功回调
      setTimeout(() => {
        try {
          const x = onFulfilled(that.value)
          resolvePromise(p2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      })
    } else if (that.status === REJECTED) {
      // 调用失败回调
      setTimeout(() => {
        try {
          const x = onRejected(that.reason)
          resolvePromise(p2, x, resolve, reject)
        } catch (err) {
          reject(err)
       }
    } else {
      // 挂载状态下缓存所有的成功和失败回调
      that.onFulfilled.push(() => {
        setTimeout(() => {
          try {
            const x = onFulfilled(that.value)
            resolvePromise(p2, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        })
      })
      that.onRejected.push(() => {
        setTimeout(() => {
          try {
            const x = onRejected(that.reason)
            resolvePromise(p2, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        })
      })
    }
  })
  return p2
}
function resolvePromise(p2, x, resolve, reject) {
  // 如果返回自身抛出错误
  if (x === p2)
    reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  // 如果 x 是 promise 对象
  if (x instanceof Promise) {
    x.then(
      (value) => resolve(value),
      (reason) => reject(reason)
    )
  } else {
    // 如果 x 是 普通值
    resolve(x)
  }
}
module.exports = Promise

拿个🌰验证

//main.js
const Promise = require('./promise')

const p = new Promise((resolve, reject) => {
  throw new Error('failed')
})

p.then(
  (value) => {
    console.log(value)
    return p1
  },
  (reason) => {
    console.log(reason)
  }
)
// Error: failed

标准版Promise

丐版Promise已经基本满足所有情况,但是总想能有个证书,那还得符合我们Promise/A+规范。

  1. npm init
  2. npm install promises-aplus-tests --save-dev
  3. 添加包的方法
Promise.deferred = function () {
  var result = {}
  result.promise = new Promise(function (resolve, reject) {
    result.resolve = resolve
    result.reject = reject
  })

  return result
}
  1. package.jsonscripts字段更改命令为test: promises-aplus-tests promise,并执行npm run test

执行,不出所料,一堆报错。根据提示需按照规范需更改resolvePromise函数

function resolvePromise(p2, x, resolve, reject) {
  // 如果返回自身抛出错误
  if (p2 === x) {
    reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  // 总的来说,就是执行x.then()
  if ((x && typeof x === 'object') || typeof x === 'function') {
    // 避免同个回调被多次调用
    let called = false
    try {
      let then = x.then
      if (typeof then === 'function') {
        then.call(
          x,
          (y) => {
            if (called) return
            called = true
            resolvePromise(p2, y, resolve, reject)
          },
          (r) => {
            if (called) return
            called = true
            reject(r)
          }
        )
      } else {
        // 不是函数 resolve(x)
        if (called) return
        called = true
        resolve(x)
      }
    } catch (err) {
      // 上述代码块报错 reject(err)
      if (called) return
      called = true
      reject(err)
    }
  } else {
    // x 不是对象或函数的情况下(包含x为null的情况)
    resolve(x)
  }
}

至此,标准版Promise更改完毕。

Promise周边

Promise.resolve

Promise.resolve(value)

返回一个给定值解析后的promise对象。

  1. 如果当前值是promise对象,返回这个promise
  2. 如果是个带有then函数的对象,采用它的最终状态。
  3. 返回当前值作为完成状态的promise

手写实现

Promise.resolve = function (param) {
  if (param instanceof Promise) {
    return param
  }
  return new Promise((resolve, reject) => {
    if (
      param &&
      typeof param === 'object' &&
      typeof param.then === 'function'
    ) {
      param.then(resolve, reject)
    } else {
      resolve(param)
    }
  })
}

Promise.reject

返回一个带有拒绝原因的Promise对象。

基本实现

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

Promise.all

Promise.all(iterable)

Promise.all接收一个promise的iterable类型(Array、Map、Set)的输入,并且只返回一个promise实例。该实例的resolve是在所有的promiseresolve回调结束后执行。reject是在任一个promise执行reject后就会立即抛出错误。

基本使用

const p1 = 'promise'
const p2 = new Promise((resolve, reject) => {
  resolve('success')
})
Promise.all([p1, p2].then((values) => {
  console.log(values) // ['promise', 'success']
}))

手写实现

Promise.all = function (params) {
  const promises = Array.from(params)
  let values = []
  return new Promise((resolve, reject) => {
    if (!params.length) resolve(values)
    promises.forEach((promise, index) => {
      // 所有执行完 返回values
      if (index === promises.length - 1) resolve(values)
      Promise.resolve(promise).then(
        (value) => {
          console.log(value)
          values[index] = value
        },
        // 任意一个错误,reject
        (reason) => {
          reject(reason)
        }
      )
    })
  })
}

Promise.race

返回一个promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

基本使用

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p1, success')
  }, 100)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2, success')
  }, 200)
})
Promise.race([p1, p2]).then((value) => {
  console.log(value)
})

手写实现

Promise.race = function (params) {
  const promises = Array.from(params)
  return new Promise((resolve, reject) => {
    if (!promises.length) return
    promises.forEach((promise) => {
      Promise.resolve(promise).then(
        (value) => {
          resolve(value)
          return
        },
        (reason) => {
          reject(reason)
          return
        }
      )
    })
  })
}

扩展

超时控制

利用 Promise.race 的特性去完成超时控制。

function sleep(delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('timeout')), delay)
  })
}

function isTimeout(p, delay) {
  return Promise.race([p, sleep(delay)])
}

// 模拟异步请求
let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("done")
  }, 1000)
})

isTimeout(promise, 500).then(value => {
  // 未超时
  console.log(value)
}).catch(error => {
  // 超时处理
  console.log(error)
}) 

限制并发请求数

通过 Promise.all 的特性去并发请求,用变量 max 去限制请求数量。

const urls = [1,2,3,4,5,6,7,8,9]
const limit = 4
function promiseFactory(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('当前请求的url', url)
      resolve(url)
    })
  })
  .finally(() => {
    if(urls.length) return promiseFactory(urls.shift())
  })
}
function controlComplicatingRequest(urls, limit) {  
  let promises = []
  while(limit--) {
    promises.push(promiseFactory(urls.shift()))
  }
  return Promise.all(promises)
}
controlComplicatingRequest(urls, limit).then(() => {
   console.log('all resolved')
})

总结

如果觉得有帮助的,毫不吝啬地点个💕呗。
下一章,会抠一抠事件循环,有兴趣地加个关注,一起学习。👀