手把手教你实现 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 相关文章推荐
关于图片验证码设计的思考
Jan 29 Javascript
浏览器脚本兼容 文本框中,回车键触发事件的兼容
Jun 21 Javascript
js编写trim()函数及正则表达式的运用
Oct 24 Javascript
textarea 控制输入字符字节数(示例代码)
Dec 27 Javascript
页面按钮禁用与解除禁用的方法
Feb 19 Javascript
JQuery中使用ajax传输超大数据的解决方法
Jul 14 Javascript
AngularJs 终极购物车(实例讲解)
Nov 08 Javascript
vue和webpack打包项目相对路径修改的方法
Jun 15 Javascript
vue中promise的使用及异步请求数据的方法
Nov 08 Javascript
详解vue-cli 脚手架 安装
Apr 16 Javascript
解决layui轮播图有数据不显示的情况
Sep 16 Javascript
浅谈Webpack4 Tree Shaking 终极优化指南
Nov 18 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
剧场版动画《PSYCHO-PASS 3 FIRST INSPECTOR》3月27日日本上映!
2020/03/06 日漫
深入php-fpm的两种进程管理模式详解
2013/06/03 PHP
php-fpm服务启动脚本的方法
2018/04/27 PHP
ThinkPHP5.0框架结合Swoole开发实现WebSocket在线聊天案例详解
2019/04/02 PHP
javascript自定义startWith()和endWith()的两种方法
2013/11/11 Javascript
javascript写的异步加载js文件函数(支持数组传参)
2014/06/07 Javascript
js判断浏览器类型为ie6时不执行
2014/06/15 Javascript
jQuery固定浮动侧边栏实现思路及代码
2014/09/28 Javascript
jQuery焦点控制图层展示延迟隐藏的方法
2015/03/09 Javascript
JavaScript中操作字符串之localeCompare()方法的使用
2015/06/06 Javascript
JavaScript去除数组里重复值的方法
2015/07/13 Javascript
JavaScript中各种引用类型的常用操作方法小结
2016/05/05 Javascript
原生JS实现的放大镜效果实例代码
2016/10/15 Javascript
利用jquery禁止外层滚动条的滚动
2017/01/05 Javascript
使用Bootstrap + Vue.js实现表格的动态展示、新增和删除功能
2017/11/27 Javascript
js canvas画布实现高斯模糊效果
2018/11/27 Javascript
详解在React项目中安装并使用Less(用法总结)
2019/03/18 Javascript
如何区分vue中的v-show 与 v-if
2020/09/08 Javascript
[01:09:13]DOTA2-DPC中国联赛 正赛 CDEC vs XG BO3 第三场 1月19日
2021/03/11 DOTA
python实现文件分组复制到不同目录的例子
2014/06/04 Python
简单理解Python中基于生成器的状态机
2015/04/13 Python
KMP算法精解及其Python版的代码示例
2016/06/01 Python
python异常和文件处理机制详解
2016/07/19 Python
基于python爬虫数据处理(详解)
2017/06/10 Python
python编程实现随机生成多个椭圆实例代码
2018/01/03 Python
Python堆排序原理与实现方法详解
2018/05/11 Python
将python依赖包打包成window下可执行文件bat方式
2019/12/26 Python
Python装饰器用法与知识点小结
2020/03/09 Python
python logging.info在终端没输出的解决
2020/05/12 Python
利用Python实现斐波那契数列的方法实例
2020/07/26 Python
Html5页面二次分享的实现
2018/07/30 HTML / CSS
阿提哈德航空官方网站:Etihad Airways
2017/01/06 全球购物
大学生实习证明范本
2014/01/15 职场文书
普通党员个人对照检查材料
2014/09/18 职场文书
2014年国庆晚会主持词
2014/09/19 职场文书
小程序后台PHP版本部署运行 LNMP+WNMP
2021/04/01 Servers