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

前言

很多 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 一共分为三个状态:

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

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

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

我们可以看到 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 内部还有个结果 [[PromiseResult]] ,用来暂存 resolve/reject 接受的值。

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

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')
 }
)

此时,控制台只会打印出 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 方法中的回调会被放置到微任务队列中,然后异步调用。

所以,我们需要将回调的调用放入异步队列,这里我们可以放到 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,这一点我们可以通过下面代码验证一下。

可以看到 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 秒后,控制台出现如下结果:

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

值穿透

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

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

在控制台并没有看到任何输出,而切换到 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 {
  // ...
 }
 }
}

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

一步之遥

现在我们距离完美实现 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 对象,则被认为是循环引用,具体案例如下:

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

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

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)
 })

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

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)
})

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

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

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

(0)

相关推荐

  • Javascript Promise用法详解

    1.约定 本文的 demo 代码有些是伪代码,不可以直接执行. 没有特殊说明,本文所有 demo 都是基于 ES6 规范. Object.method 代表是静态方法, Object#method 代表的是实例方法.如 Promise#then 代表的是 Promise 的实例方法, Promise.resolve 代表的是 Promise 的静态方法. 2.什么是 Promise? 首先我们来了解 Promise 到底是怎么一回事 Promise 是抽象的异步处理对象,以及对其进行各种操作的组

  • 使用Promise解决多层异步调用的简单学习心得

    前言 第一次接触到Promise这个东西,是2012年微软发布Windows8操作系统后抱着作死好奇的心态研究用html5写Metro应用的时候.当时配合html5提供的WinJS库里面的异步接口全都是Promise形式,这对那时候刚刚毕业一点javascript基础都没有的我而言简直就是天书.我当时想的是,微软又在脑洞大开的瞎捣鼓了. 结果没想到,到了2015年,Promise居然写进ES6标准里面了.而且一项调查显示,js程序员们用这玩意用的还挺high. 讽刺的是,作为早在2012年就在M

  • 理解JavaScript中Promise的使用

    Javascript 采用回调函数(callback)来处理异步编程.从同步编程到异步回调编程有一个适应的过程,但是如果出现多层回调嵌套,也就是我们常说的厄运的回调金字塔(Pyramid of Doom),绝对是一种糟糕的编程体验.于是便有了 CommonJS 的 Promises/A 规范,用于解决回调金字塔问题.本文先介绍 Promises 相关规范,然后再通过解读一个迷你的 Promises 以加深理解. 什么是 Promise 一个 Promise 对象代表一个目前还不可用,但是在未来的

  • NodeJS中利用Promise来封装异步函数

    在写Node.js的过程中,连续的IO操作可能会导致"金字塔噩梦",回调函数的多重嵌套让代码变的难以维护,利用CommonJs的Promise来封装异步函数,使用统一的链式API来摆脱多重回调的噩梦. Node.js提供的非阻塞IO模型允许我们利用回调函数的方式处理IO操作,但是当需要连续的IO操作时,你的回调函数会多重嵌套,代码很不美观,而且不易维护,而且可能会有许多错误处理的重复代码,也就是所谓的"Pyramid of Doom". 复制代码 代码如下: ste

  • promise处理多个相互依赖的异步请求(实例讲解)

    在项目中,经常会遇到多个相互依赖的异步请求.如有a,b,c三个ajax请求,b需要依赖a返回的数据,c又需要a和b请求返回的数据.如果采用请求嵌套请求的方式自然是不可取的.导致代码难以维护,如何请求很多.会出现很多问题. Promise就是解决多个异步请求的问题. Promise是ES6提供的一个对象,用来传递异步操作的消息. Promise有三种状态:Pending(进行中).Resolved(已完成,又称 Fulfilled)和 Rejected(已失败). 直接上代码.有a,b请求,b依赖

  • JavaScript异步编程Promise模式的6个特性

    在我们开始正式介绍之前,我们想看看Javascript Promise的样子: 复制代码 代码如下: var p = new Promise(function(resolve, reject) {  resolve("hello world");}); p.then(function(str) {  alert(str);}); 1. then()返回一个Forked Promise 以下两段代码有什么区别呢? 复制代码 代码如下: // Exhibit Avar p = new Pr

  • JavaScript Promise 用法

    同步编程通常来说易于调试和维护,然而,异步编程通常能获得更好的性能和更大的灵活性.异步的最大特点是无需等待."Promises"渐渐成为JavaScript里最重要的一部分,大量的新API都开始promise原理实现.下面让我们看一下什么是promise,以及它的API和用法! Promises现状 XMLHttpRequest API是异步的,但它没有使用promise API.但有很多原生的 javascript API 使用了promise: *Battery API *fetc

  • 举例详解JavaScript中Promise的使用

    摘录 – Parse JavaScript SDK现在提供了支持大多数异步方法的兼容jquery的Promises模式,那么这意味着什么呢,读完下文你就了解了. "Promises" 代表着在javascript程序里下一个伟大的范式,但是理解他们为什么如此伟大不是件简单的事.它的核心就是一个promise代表一个任务结果,这个任务有可能完成有可能没完成.Promise模式唯一需要的一个接口是调用then方法,它可以用来注册当promise完成或者失败时调用的回调函数,这在Common

  • JS中Promise函数then的奥秘探究

    Promise概述 Promise对象是CommonJS工作组提出的一种规范,目的是为异步操作提供统一接口. 那么,什么是Promises? 首先,它是一个对象,也就是说与其他JavaScript对象的用法,没有什么两样:其次,它起到代理作用(proxy),充当异步操作与回调函数之间的中介.它使得异步操作具备同步操作的接口,使得程序具备正常的同步运行的流程,回调函数不必再一层层嵌套. 简单说,它的思想是,每一个异步任务立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程.这

  • 详细解读JavaScript编程中的Promise使用

    Promise核心说明 尽管Promise已经有自己的规范,但目前的各类Promise库,在Promise的实现细节上是有差异的,部分API甚至在意义上完全不同.但Promise的核心内容,是相通的,它就是then方法.在相关术语中,promise指的就是一个有then方法,且该方法能触发特定行为的对象或函数. Promise可以有不同的实现方式,因此Promise核心说明并不会讨论任何具体的实现代码. 先阅读Promise核心说明的意思是:看,这就是需要写出来的结果,请参照这个结果想一想怎么用

  • 使用Promise链式调用解决多个异步回调的问题

    介绍 所谓Promise,简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果. 缺少场景支撑,对于新手而言,很难理解Promise的意义. 在<你不知道的JavaScript中>有个场景介绍得很形象: 我走到快餐店的柜台,点了一个芝士汉堡.我交给收银员1.47美元.通过下订单并付款,我已经发出了一个对某个值(就是那个汉堡)的请求.我已经启 动了一次交易. 但是,通常我不能马上就得到这个汉堡.收银员会交给我某个东西来代替汉堡:一张带有 订单号的收据.订单号就是一个

随机推荐