手把手教你实现 Promise的使用方法


Posted in Javascript onSeptember 02, 2020

前言

很多 JavaScript 的初学者都曾感受过被回调地狱支配的恐惧,直至掌握了 Promise 语法才算解脱。虽然很多语言都早已内置了 Promise ,但是 JavaScript 中真正将其发扬光大的还是 jQuery 1.5 对 $.ajax 的重构,支持了 Promise,而且用法也和 jQuery 推崇的链式调用不谋而合。后来 ES6 出世,大家才开始进入全民 Promise 的时代,再后来 ES8 又引入了 async 语法,让 JavaScript 的异步写法更加优雅。

今天我们就一步一步来实现一个 Promise,如果你还没有用过 Promise,建议先熟悉一下 Promise 语法再来阅读本文。

构造函数

在已有的 Promise/A+ 规范 中并没有规定 promise 对象从何而来,在 jQuery 中通过调用 $.Deferred() 得到 promise 对象,ES6 中通过实例化 Promise 类得到 promise 对象。这里我们使用 ES 的语法,构造一个类,通过实例化的方式返回 promise 对象,由于 Promise 已经存在,我们暂时给这个类取名为 Deferred

class Deferred {
 constructor(callback) {
 const resolve = () => {
  // TODO
 }
 const reject = () => {
  // TODO
 }
 try {
  callback(resolve, reject)
 } catch (error) {
  reject(error)
 }
 }
}

构造函数接受一个 callback,调用 callback 的时候需传入 resolve、reject 两个方法。

Promise 的状态

Promise 一共分为三个状态:

手把手教你实现 Promise的使用方法

 pending :等待中,这是 Promise 的初始状态;

手把手教你实现 Promise的使用方法 

fulfilled :已结束,正常调用 resolve 的状态;

手把手教你实现 Promise的使用方法 

 rejected :已拒绝,内部出现错误,或者是调用 reject 之后的状态;

手把手教你实现 Promise的使用方法

我们可以看到 Promise 在运行期间有一个状态,存储在 [[PromiseState]] 中。下面我们为 Deferred 添加一个状态。

//基础变量的定义
const STATUS = {
 PENDING: 'PENDING',
 FULFILLED: 'FULFILLED',
 REJECTED: 'REJECTED'
}

class Deferred {
 constructor(callback) {
 this.status = STATUS.PENDING

 const resolve = () => {
  // TODO
 }
 const reject = () => {
  // TODO
 }
 try {
  callback(resolve, reject)
 } catch (error) {
  // 出现异常直接进行 reject
  reject(error)
 }
 }
}

这里还有个有意思的事情,早期浏览器的实现中 fulfilled 状态是 resolved,明显与 Promise 规范不符。当然,现在已经修复了。

手把手教你实现 Promise的使用方法

内部结果

除开状态,Promise 内部还有个结果 [[PromiseResult]] ,用来暂存 resolve/reject 接受的值。

手把手教你实现 Promise的使用方法

手把手教你实现 Promise的使用方法

继续在构造函数中添加一个内部结果。

class Deferred {
 constructor(callback) {
 this.value = undefined
 this.status = STATUS.PENDING

 const resolve = value => {
  this.value = value
  // TODO
 }
 const reject = reason => {
  this.value = reason
  // TODO
 }
 try {
  callback(resolve, reject)
 } catch (error) {
  // 出现异常直接进行 reject
  reject(error)
 }
 }
}

储存回调

使用 Promise 的时候,我们一般都会调用 promise 对象的 .then 方法,在 promise 状态转为 fulfilledrejected 的时候,拿到内部结果,然后做后续的处理。所以构造函数中,还需要构造两个数组,用来存储 .then 方法传入的回调。

class Deferred {
 constructor(callback) {
 this.value = undefined
 this.status = STATUS.PENDING

 this.rejectQueue = []
 this.resolveQueue = []

 const resolve = value => {
  this.value = value
  // TODO
 }
 const reject = reason => {
  this.value = reason
  // TODO
 }
 try {
  callback(resolve, reject)
 } catch (error) {
  // 出现异常直接进行 reject
  reject(error)
 }
 }
}

resolve 与 reject

 修改状态

接下来,我们需要实现 resolve 和 reject 两个方法,这两个方法在被调用的时候,会改变 promise 对象的状态。而且任意一个方法在被调用之后,另外的方法是无法被调用的。

new Promise((resolve, reject) => {
 setTimeout(() => {
 resolve('?‍♂️')
 }, 500)
 setTimeout(() => {
 reject('?‍♂️')
 }, 800)
}).then(
 () => {
 console.log('fulfilled')
 },
 () => {
 console.log('rejected')
 }
)

手把手教你实现 Promise的使用方法

此时,控制台只会打印出 fulfilled ,并不会出现 rejected

class Deferred {
 constructor(callback) {
 this.value = undefined
 this.status = STATUS.PENDING

 this.rejectQueue = []
 this.resolveQueue = []

 let called // 用于判断状态是否被修改
 const resolve = value => {
   if (called) return
  called = true
  this.value = value
  // 修改状态
  this.status = STATUS.FULFILLED
 }
 const reject = reason => {
   if (called) return
  called = true
  this.value = reason
  // 修改状态
  this.status = STATUS.REJECTED
 }
 try {
  callback(resolve, reject)
 } catch (error) {
  // 出现异常直接进行 reject
  reject(error)
 }
 }
}

调用回调

修改完状态后,拿到结果的 promise 一般会调用 then 方法传入的回调。

class Deferred {
 constructor(callback) {
 this.value = undefined
 this.status = STATUS.PENDING

 this.rejectQueue = []
 this.resolveQueue = []

 let called // 用于判断状态是否被修改
 const resolve = value => {
   if (called) return
  called = true
  this.value = value
  // 修改状态
  this.status = STATUS.FULFILLED
  // 调用回调
  for (const fn of this.resolveQueue) {
  fn(this.value)
  }
 }
 const reject = reason => {
   if (called) return
  called = true
  this.value = reason
  // 修改状态
  this.status = STATUS.REJECTED
  // 调用回调
  for (const fn of this.rejectQueue) {
  fn(this.value)
  }
 }
 try {
  callback(resolve, reject)
 } catch (error) {
  // 出现异常直接进行 reject
  reject(error)
 }
 }
}

熟悉 JavaScript 事件系统的同学应该知道, promise.then 方法中的回调会被放置到微任务队列中,然后异步调用。

手把手教你实现 Promise的使用方法

所以,我们需要将回调的调用放入异步队列,这里我们可以放到 setTimeout 中进行延迟调用,虽然不太符合规范,但是将就将就。

class Deferred {
 constructor(callback) {
 this.value = undefined
 this.status = STATUS.PENDING

 this.rejectQueue = []
 this.resolveQueue = []

 let called // 用于判断状态是否被修改
 const resolve = value => {
   if (called) return
  called = true
  // 异步调用
  setTimeout(() => {
   this.value = value
  // 修改状态
  this.status = STATUS.FULFILLED
  // 调用回调
  for (const fn of this.resolveQueue) {
   fn(this.value)
  }
  })
 }
 const reject = reason => {
   if (called) return
  called = true
  // 异步调用
  setTimeout(() =>{
  this.value = reason
  // 修改状态
  this.status = STATUS.REJECTED
  // 调用回调
  for (const fn of this.rejectQueue) {
   fn(this.value)
  }
  })
 }
 try {
  callback(resolve, reject)
 } catch (error) {
  // 出现异常直接进行 reject
  reject(error)
 }
 }
}

then 方法

接下来我们需要实现 then 方法,用过 Promise 的同学肯定知道,then 方法是能够继续进行链式调用的,所以 then 必须要返回一个 promise 对象。但是在 Promise/A+ 规范中,有明确的规定,then 方法返回的是一个新的 promise 对象,而不是直接返回 this,这一点我们可以通过下面代码验证一下。

手把手教你实现 Promise的使用方法

可以看到 p1 对象和 p2 是两个不同的对象,并且 then 方法返回的 p2 对象也是 Promise 的实例。

除此之外,then 方法还需要判断当前状态,如果当前状态不是 pending 状态,则可以直接调用传入的回调,而不用再放入队列进行等待。

class Deferred {
 then(onResolve, onReject) {
 if (this.status === STATUS.PENDING) {
  // 将回调放入队列中
  const rejectQueue = this.rejectQueue
  const resolveQueue = this.resolveQueue
  return new Deferred((resolve, reject) => {
  // 暂存到成功回调等待调用
  resolveQueue.push(function (innerValue) {
   try {
   const value = onResolve(innerValue)
   // 改变当前 promise 的状态
   resolve(value)
   } catch (error) {
   reject(error)
   }
  })
  // 暂存到失败回调等待调用
  rejectQueue.push(function (innerValue) {
   try {
   const value = onReject(innerValue)
   // 改变当前 promise 的状态
   resolve(value)
   } catch (error) {
   reject(error)
   }
  })
  })
 } else {
  const innerValue = this.value
  const isFulfilled = this.status === STATUS.FULFILLED
  return new Deferred((resolve, reject) => {
  try {
   const value = isFulfilled
   ? onResolve(innerValue) // 成功状态调用 onResolve
   : onReject(innerValue) // 失败状态调用 onReject
   resolve(value) // 返回结果给后面的 then
  } catch (error) {
   reject(error)
  }
  })
 }
 }
}

现在我们的逻辑已经可以基本跑通,我们先试运行一段代码:

new Deferred(resolve => {
 setTimeout(() => {
 resolve(1)
 }, 3000)
}).then(val1 => {
 console.log('val1', val1)
 return val1 * 2
}).then(val2 => {
 console.log('val2', val2)
 return val2
})

3 秒后,控制台出现如下结果:

手把手教你实现 Promise的使用方法

可以看到,这基本符合我们的预期。

值穿透

如果我们在调用 then 的时候,如果没有传入任何的参数,按照规范,当前 promise 的值是可以透传到下一个 then 方法的。例如,如下代码:

new Deferred(resolve => {
 resolve(1)
})
 .then()
 .then()
 .then(val => {
 console.log(val)
 })

手把手教你实现 Promise的使用方法

在控制台并没有看到任何输出,而切换到 Promise 是可以看到正确结果的。

手把手教你实现 Promise的使用方法

要解决这个方法很简单,只需要在 then 调用的时候判断参数是否为一个函数,如果不是则需要给一个默认值。

const isFunction = fn => typeof fn === 'function'

class Deferred {
 then(onResolve, onReject) {
 // 解决值穿透
 onReject = isFunction(onReject) ? onReject : reason => { throw reason }
 onResolve = isFunction(onResolve) ? onResolve : value => { return value }
 if (this.status === STATUS.PENDING) {
  // ...
 } else {
  // ...
 }
 }
}

手把手教你实现 Promise的使用方法

现在我们已经可以拿到正确结果了。

一步之遥

现在我们距离完美实现 then 方法只差一步之遥,那就是我们在调用 then 方法传入的 onResolve/onReject 回调时,还需要判断他们的返回值。如果回调的内部返回的就是一个 promise 对象,我们应该如何处理?或者出现了循环引用,我们又该怎么处理?

前面我们在拿到 onResolve/onReject 的返回值后,直接就调用了 resolve 或者 resolve ,现在我们需要把他们的返回值进行一些处理。

then(onResolve, onReject) {
 // 解决值穿透代码已经省略
 if (this.status === STATUS.PENDING) {
 // 将回调放入队列中
 const rejectQueue = this.rejectQueue
 const resolveQueue = this.resolveQueue
 const promise = new Deferred((resolve, reject) => {
  // 暂存到成功回调等待调用
  resolveQueue.push(function (innerValue) {
  try {
   const value = onResolve(innerValue)
-   resolve(value)
+   doThenFunc(promise, value, resolve, reject)
  } catch (error) {
   reject(error)
  }
  })
  // 暂存到失败回调等待调用
  rejectQueue.push(function (innerValue) {
  try {
   const value = onReject(innerValue)
-   resolve(value)
+   doThenFunc(promise, value, resolve, reject)
  } catch (error) {
   reject(error)
  }
  })
 })
 return promise
 } else {
 const innerValue = this.value
 const isFulfilled = this.status === STATUS.FULFILLED
 const promise = new Deferred((resolve, reject) => {
  try {
  const value = isFulfilled
  ? onResolve(innerValue) // 成功状态调用 onResolve
  : onReject(innerValue) // 失败状态调用 onReject
-  resolve(value)
+  doThenFunc(promise, value, resolve, reject)
  } catch (error) {
  reject(error)
  }
 })
 return promise
 }
}

返回值判断

在我们使用 Promise 的时候,经常会在 then 方法中返回一个新的 Promise,然后把新的 Promise 完成后的内部结果再传递给后面的 then 方法。

fetch('server/login')
 .then(user => {
  // 返回新的 promise 对象
  return fetch(`server/order/${user.id}`)
 })
 .then(order => {
  console.log(order)
 })
function doThenFunc(promise, value, resolve, reject) {
 // 如果 value 是 promise 对象
 if (value instanceof Deferred) {
 // 调用 then 方法,等待结果
 value.then(
  function (val) {
   doThenFunc(promise, value, resolve, reject)
  },
  function (reason) {
  reject(reason)
  }
 )
 return
 }
 // 如果非 promise 对象,则直接返回
 resolve(value)
}

判断循环引用

如果当前 then 方法回调函数返回值是当前 then 方法产生的新的 promise 对象,则被认为是循环引用,具体案例如下:

手把手教你实现 Promise的使用方法

then 方法返回的新的 promise 对象 p1 ,在回调中被当做返回值,此时会抛出一个异常。因为按照之前的逻辑,代码将会一直困在这一段逻辑里。

手把手教你实现 Promise的使用方法

所以,我们需要提前预防,及时抛出错误。

function doThenFunc(promise, value, resolve, reject) {
 // 循环引用
 if (promise === value) {
 reject(
  new TypeError('Chaining cycle detected for promise')
 )
 return
 }
 // 如果 value 是 promise 对象
 if (value instanceof Deferred) {
 // 调用 then 方法,等待结果
 value.then(
  function (val) {
   doThenFunc(promise, value, resolve, reject)
  },
  function (reason) {
  reject(reason)
  }
 )
 return
 }
 // 如果非 promise 对象,则直接返回
 resolve(value)
}

现在我们再试试在 then 中返回一个新的 promise 对象。

const delayDouble = (num, time) => new Deferred((resolve) => {
 console.log(new Date())
 setTimeout(() => {
 resolve(2 * num)
 }, time)
})

new Deferred(resolve => {
 setTimeout(() => {
 resolve(1)
 }, 2000)
})
 .then(val => {
 console.log(new Date(), val)
 return delayDouble(val, 2000)
 })
 .then(val => {
 console.log(new Date(), val)
 })

手把手教你实现 Promise的使用方法

上面的结果也是完美符合我们的预期。

catch 方法

catch 方法其实很简单,相当于 then 方法的一个简写。

class Deferred {
 constructor(callback) {}
 then(onResolve, onReject) {}
 catch(onReject) {
 return this.then(null, onReject)
 }
}

静态方法

resolve/reject

Promise 类还提供了两个静态方法,直接返回状态已经固定的 promise 对象。

class Deferred {
 constructor(callback) {}
 then(onResolve, onReject) {}
 catch(onReject) {}
 
 static resolve(value) {
 return new Deferred((resolve, reject) => {
  resolve(value)
 })
 }

 static reject(reason) {
 return new Deferred((resolve, reject) => {
  reject(reason)
 })
 }
}

all

all 方法接受一个 promise 对象的数组,等数组中所有的 promise 对象的状态变为 fulfilled ,然后返回结果,其结果也是一个数组,数组的每个值对应的是 promise 对象的内部结果。

首先,我们需要先判断传入的参数是否为数组,然后构造一个结果数组以及一个新的 promise 对象。

class Deferred {
 static all(promises) {
 // 非数组参数,抛出异常
 if (!Array.isArray(promises)) {
  return Deferred.reject(new TypeError('args must be an array'))
 }

  // 用于存储每个 promise 对象的结果
 const result = []
 const length = promises.length
 // 如果 remaining 归零,表示所有 promise 对象已经 fulfilled
 let remaining = length 
 const promise = new Deferred(function (resolve, reject) {
  // TODO
 })
  return promise
 }
}

接下来,我们需要进行一下判断,对每个 promise 对象的 resolve 进行拦截,每次 resolve 都需要将 remaining 减一,直到 remaining 归零。

class Deferred {
 static all(promises) {
 // 非数组参数,抛出异常
 if (!Array.isArray(promises)) {
  return Deferred.reject(new TypeError('args must be an array'))
 }

 const result = [] // 用于存储每个 promise 对象的结果
 const length = promises.length

 let remaining = length
 const promise = new Deferred(function (resolve, reject) {
  // 如果数组为空,则返回空结果
  if (promises.length === 0) return resolve(result)

  function done(index, value) {
  doThenFunc(
   promise,
   value,
   (val) => {
   // resolve 的结果放入 result 中
   result[index] = val
   if (--remaining === 0) {
    // 如果所有的 promise 都已经返回结果
    // 然后运行后面的逻辑
    resolve(result)
   }
   },
   reject
  )
  }
  // 放入异步队列
  setTimeout(() => {
  for (let i = 0; i < length; i++) {
   done(i, promises[i])
  }
  })
 })
  return promise
 }
}

下面我们通过如下代码,判断逻辑是否正确。按照预期,代码运行后,在 3 秒之后,控制台会打印一个数组 [2, 4, 6]

const delayDouble = (num, time) => new Deferred((resolve) => {
 setTimeout(() => {
 resolve(2 * num)
 }, time)
})

console.log(new Date())
Deferred.all([
 delayDouble(1, 1000),
 delayDouble(2, 2000),
 delayDouble(3, 3000)
]).then((results) => {
 console.log(new Date(), results)
})

手把手教你实现 Promise的使用方法

上面的运行结果,基本符合我们的预期。

race

race 方法同样接受一个 promise 对象的数组,但是它只需要有一个 promise 变为 fulfilled 状态就会返回结果。

class Deferred {
 static race(promises) {
 if (!Array.isArray(promises)) {
  return Deferred.reject(new TypeError('args must be an array'))
 }

 const length = promises.length
 const promise = new Deferred(function (resolve, reject) {
  if (promises.length === 0) return resolve([])

  function done(value) {
  doThenFunc(promise, value, resolve, reject)
  }

  // 放入异步队列
  setTimeout(() => {
  for (let i = 0; i < length; i++) {
   done(promises[i])
  }
  })
 })
 return promise
 }
}

下面我们将前面验证 all 方法的案例改成 race。按照预期,代码运行后,在 1 秒之后,控制台会打印一个2。

const delayDouble = (num, time) => new Deferred((resolve) => {
 setTimeout(() => {
 resolve(2 * num)
 }, time)
})

console.log(new Date())
Deferred.race([
 delayDouble(1, 1000),
 delayDouble(2, 2000),
 delayDouble(3, 3000)
]).then((results) => {
 console.log(new Date(), results)
})

手把手教你实现 Promise的使用方法

上面的运行结果,基本符合我们的预期。

总结

一个简易版的 Promise 类就已经实现了,这里还是省略了部分细节,完整代码可以访问 github 。Promise 的出现为后期的 async 语法打下了坚实基础,下一篇博客可以好好聊一聊 JavaScript 的异步编程史,不小心又给自己挖坑了。。。

到此这篇关于手把手教你实现 Promise的方法的文章就介绍到这了,更多相关Promise语法内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
你所要知道JS(DHTML)中的一些技巧
Jan 09 Javascript
js中call与apply的用法小结
Dec 28 Javascript
jQuery之选项卡的简单实现
Feb 28 Javascript
javascript 获取元素样式必杀技
May 04 Javascript
IE浏览器不支持getElementsByClassName的解决方法
Aug 27 Javascript
Angular中$compile源码分析
Jan 28 Javascript
bootstrap学习笔记之初识bootstrap
Jun 21 Javascript
Javascript之深入浅出prototype
Feb 06 Javascript
js实现仿购物车加减效果
Mar 01 Javascript
js装饰设计模式学习心得
Feb 17 Javascript
layer弹出层倒计时关闭的实现方法
Sep 27 Javascript
three.js显示中文字体与tween应用详析
Jan 04 Javascript
如何基于jQuery实现五角星评分
Sep 02 #jQuery
在vscode 中设置 vue模板内容的方法
Sep 02 #Javascript
JavaScript array常用方法代码实例详解
Sep 02 #Javascript
Vue前端判断数据对象是否为空的实例
Sep 02 #Javascript
详解JavaScript 事件流
Sep 02 #Javascript
JavaScript判断数据类型有几种方法及区别介绍
Sep 02 #Javascript
jQuery中getJSON跨域原理的深入讲解
Sep 02 #jQuery
You might like
PHP去掉从word直接粘贴过来的没有用格式的函数
2012/10/29 PHP
ThinkPHP页面跳转success与error方法概述
2014/06/25 PHP
PHP从零开始打造自己的MVC框架之类的自动加载实现方法详解
2019/06/03 PHP
php日志函数error_log用法实例分析
2019/09/23 PHP
用javascript实现页面打印的三种方法
2007/03/05 Javascript
javascript算法题 求任意一个1-9位不重复的N位数在该组合中的大小排列序号
2012/07/21 Javascript
javascript全局变量封装模块实现代码
2012/11/28 Javascript
jQuery实现类似淘宝购物车全选状态示例
2013/06/26 Javascript
js实现点小图看大图效果的思路及示例代码
2013/10/28 Javascript
JS页面延迟执行一些方法(整理)
2013/11/11 Javascript
Jquery 实现table样式的设定
2015/01/28 Javascript
javascript实现ecshop搜索框键盘上下键切换控制
2015/03/18 Javascript
JavaScript脚本判断蜘蛛来源的方法
2015/09/22 Javascript
iscroll.js的上拉下拉刷新时无法回弹的解决方法
2016/02/18 Javascript
JS实现六边形3D拖拽翻转效果的方法
2016/09/11 Javascript
Vue动态实现评分效果
2017/05/24 Javascript
js实现HTML中Select二级联动的实例
2018/01/05 Javascript
简单了解node npm cnpm的具体使用方法
2019/02/27 Javascript
vue cli3.0 引入eslint 结合vscode使用
2019/05/27 Javascript
在js文件中引入(调用)另一个js文件的三种方法
2020/09/11 Javascript
python转换摩斯密码示例
2014/02/16 Python
探究数组排序提升Python程序的循环的运行效率的原因
2015/04/01 Python
python单元测试unittest实例详解
2015/05/11 Python
python 装饰器的使用示例
2020/10/10 Python
利用css3制作3D样式按钮实现代码
2013/03/18 HTML / CSS
实例教程 利用html5和css3打造一款创意404页面
2014/10/20 HTML / CSS
构造方法和其他方法的区别?怎么调用父类的构造方法
2013/09/22 面试题
MYSQL基础面试题
2012/05/13 面试题
技术总监岗位职责
2013/12/05 职场文书
校长就职演讲稿
2014/01/06 职场文书
运动会领导邀请函
2014/01/10 职场文书
应届硕士毕业生自荐信
2014/05/26 职场文书
影视广告专业求职信
2014/09/02 职场文书
个人批评与自我批评总结
2014/10/17 职场文书
质量保证书怎么写
2015/02/27 职场文书
内勤岗位职责范本
2015/04/13 职场文书