渐进加强的 Promise

最近这段时间因为疫情的缘由,在家实在闷得慌,因此看了下 js 的一些基础知识,从前不是很了解的 Promise 忽然豁然开朗了很多,因而就赶忙趁热打铁写下来(这就是温故而知新的感受吗,哈哈哈😁)。数组

确实是待久了,🌸樱花🌸都开了。

为何要用 Promise

  • 一个很显然的缘由就是它的链式调用可以解决回调地狱带来的一些问题(不利于阅读与维护、很差调试等),这点想必你们都清楚。
  • 其实 Promise 和传统的回调还有一个细微的区别,就是回调函数的定义时机,传统的回调咱们是得先有回调函数再执行(由于你要把函数看成参数传,得先写好),而 Promise 则更加灵活,它是先执行函数体,而后在你须要 then 的时候再去写回调函数也不迟,不知道说清楚没有,你们能够细品一下😂。

基础用法

要想知道某个东西怎么写的,就要先学会用,因此读者大大们若是还有没用过的话,赶忙去学下再来看吧,由于本文的目标是手写一个 Promise 以及 catch、finally、all、race、resolve 等附加方法,下面是最简单的用法展现👇:promise

let p = new Promise((resolve, reject) => { // 这里面的代码是同步执行的
  resolve(1)
}).then(res => { // 这里面的代码是异步的
  console.log('成功', res)
}, err => {
  console.log('错误', err)
})
// 成功 1
复制代码

知识前置

事实上 Promise 是有个 Promises/A+ 规范的(这东西至关于一个需求文档),这里我会先罗列几个必知必会的点,毕竟要手写嘛✍,肚子里得有点墨水:并发

  • 咱们要知道一个 Promise 有三种状态:pending,success,fail,而且状态只能从 pending 到 success 或者从 pending 到 fail,而且只能改变一次,是不可逆的(正经点的状态名称:pending、fulfilled、rejected,固然这不重要)
  • resolve 和 reject 是用来改变 Promise 自身状态和值的,并触发后面 then 里面的回调
  • then 里面的参数若是不是函数将被忽略,具备穿透效果,这个后面会细说
  • new Promise() 返回一个 promise 对象,每一个 then 返回一个新的 Promise,由于 Promise 的状态只容许被改变一次,因此每次都得返回新的

下面的描述我也没写的规范那么正经,由于我有时候总以为那样不利于理解,一堆英文名字就把你给愣住了😯。框架

开始手写

首先咱们把前面提到的基础用法简化下:dom

let p = new Promise(fn).then(fn1, fn2)
复制代码

这样一来上面的结构就明了许多,既然须要 new,那么它首先是个构造函数,接收一个函数参数 fn,而后实例有个 then 方法,接收两个参数 fn1 和 fn2,也都是函数。此外由知识前置里面的内容可知 Promise 里面自身得维护一个状态,因此咱们能够先写出一个大致框架:异步

class MyPromise {
  constructor(fn) {
    this.status = 'pending' // 保存状态
    this.successValue = null // 保存成功的值
    this.failValue = null // 保存失败的值
    fn() // 由于 Promise 里面的代码是同步执行的,因此直接进来须要直接调用
  }
  then(successFn, failFn) {}
}
复制代码

咱们知道在执行 fn 的时候其实还有两个参数,就是 fn(resolve, reject),因此咱们须要完善它,在 MyPromise 里面定义 resolve 和 reject 这两个函数,当外界调用 resolve 和 reject 时就是改变 MyPromise 里面的状态和值,并触发相应的回调函数,就像下面这样:函数

constructor(fn) {
    this.status = 'pending' // 保存状态
    this.successValue = null // 保存成功的值
    this.failValue = null // 保存失败的值

    let resolve = (successValue) => { // 这个 successValue 是外部调用传进来的值
      this.status = 'success'
      this.successValue = successValue
    }
    let reject = (failValue) => { // 这个 failValue 是外部调用传进来的值
      this.status = 'fail'
      this.failValue = failValue
    }

    fn(resolve, reject)
}
复制代码

then

constructor 里面的东西大概写完了,接下来咱们简要写下 then 方法,then 方法里面不是有两个函数参数吗,根据 status 的状态执行其中一个便可,就像下面这样:测试

then(successFn, failFn) {
    if (this.status === 'success') {
        successFn(this.successValue)
    } else if (this.status === 'fail') {
        failFn(this.failValue)
    }
}
复制代码

ok,咱们来测试一下:this

let p = new MyPromise((resolve, reject) => {
  console.log('1')
  resolve(100)
}).then(res => {
  console.log('2')
  console.log('成功', res)
}, err => {
  console.log('错误', err)
})
console.log('3')
// 1
// 2
// 成功 100
// 3
复制代码

不错不错,能够成功打印出 100,可是有个问题,console.log 的顺序应该是 一、三、2,由于 then 里面的内容是异步执行的,因此咱们须要 setTimeout 来简单模拟下,把 then 操做延后,就像下面这样:spa

then(successFn, failFn) {
    if (this.status === 'success') {
      setTimeout(() => {
        successFn(this.successValue)
      })
    } else if (this.status === 'fail') {
      setTimeout(() => {
        failFn(this.failValue)
      })
    }
}
复制代码

不错不错,看起来好像能够了,可是若是我这样写呢:

let p = new MyPromise((resolve, reject) => { // 连续调用
    resolve(100)
    reject(-1)
}).then(res => {
  console.log('成功', res)
}, err => {
  console.log('错误', err)
})
// 错误 -1
复制代码

上面结果输出 -1 固然是错的,由于连续调用 resolve 或 reject 是无效的,Promise 只容许被改变一次,因此咱们须要加个限制条件:

let resolve = (successValue) => {
  if (this.status !== 'pending') return // 状态已经改变过就不往下执行了
  this.status = 'success'
  this.successValue = successValue
}
let reject = (failValue) => {
  if (this.status !== 'pending') return // 状态已经改变过就不往下执行了
  this.status = 'fail'
  this.failValue = failValue
}
复制代码

固然还没完,一个新的问题诞生了,若是我这样写呢:

let p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(100)
  }, 2000);
}).then(res => {
  console.log(res)
}, err => {
  console.log(err)
})
复制代码

没有输出任何东西,这是由于当我调用 then 的时候,MyPromise 里面的东西还没执行完,状态未被改变,因此 then 里面的成功和失败都不会调用,所以咱们须要在 then 中加上一种状况,就是若是 MyPromise 里面还没执行完就先把 then 中的 fn1 和 fn2 放进数组中存起来,等到 MyPromise 里面执行完再把数组拿出来遍历执行(固然也要放进 setTimeout 中),就像下面这样(接下来都用图了😁):

事实上你能够把 resolve 或 reject 当成触发 then 里面函数的开关,只要碰到 resolve 或 reject,与之对应的 then 里面的函数也该执行了(放入微任务执行,咱们用的是宏任务替代,关于事件循环也是个挺有意思的知识点,你们能够本身去了解一下)。一句话说就是 resolve 和 reject 的做用就是在合适的时间点执行 then 里面的回调函数,有点观察者模式的意思(你品,你细品🤔)。

then 增强

写到这里,其实还有个大问题,前面说过了,then 是有返回的,而且是个新的 Promise,还支持链式调用,显然咱们的并不具有这样的功能,因此如今咱们须要完善 then,主要思想就是在 then 里面套一层 Promise,此外你要知道若是 then(fn1, fn2) 继续向下传递的话,传递的值是 fn1 或 fn2 的返回值,看下面这张图你可能会清晰点:

👌,理清了这个东西以后咱们就来看下具体是怎么改的:

细心点你会发现这里 then 里面函数的返回值还多是个 Promise,因此咱们须要对 then 里面函数的返回值作个判断,若是返回值是个 Promise 就须要等这个 Promise 执行完再调用 resolve 和 reject,就像下面这样:

执行一下下面的测试代码:

let p = new MyPromise((resolve, reject) => {
  resolve(100)
}).then(res => {
  console.log('成功', res)
  // return 0
  return new MyPromise((resolve2, reject2) => {
    setTimeout(() => {
      resolve2(0)
    }, 1000)
  })
}, err => {
  console.log('错误', err)
}).then(res2 => {
  console.log('成功2', res2)
}, err2 => {
  console.log('错误2', err2)
})
// 成功   100
// 成功2  0 (1s后打印出来)
复制代码

写到这里,then 里面的东西已经写的差很少了,但其实仍是有问题的,咱们这里只是解决了一层 Promise 的嵌套,若是你多嵌套几个 Promise 就不行了,这个须要咱们把上面公共的部分提取出来而后递归调用,写起来不复杂,但会有点绕容易晕,此外咱们也没有对循环调用同一个 Promsie 作判断以及一些异常捕获,由于咱们理解到这里就差很少了👏。固然了,我会在文末附上完整的代码😬,里面也有详细的注释。

穿透

什么是穿透呢?让咱们来看下面的代码:

let p = new Promise((resolve, reject) => {
  resolve(100)
}).then()
.then(1)
.then(res => {
  console.log('成功', res)
}, err => {
  console.log('错误', err)
})
// 成功 100
复制代码

简单来讲就是,若是 then 中的参数不是函数或为空,then 以前的值还可以继续向下传递,其实这个写起来很简单,就是在 then 里面的一开始判断下参数是否为函数,不是的话就包装成函数,并把以前的值看成返回值,具体操做以下:

注意咱们这里抛出错误 throw 不能写成这样 return new Error('xxx'),由于这不是抛出错误,是返回一个错误对象,等价于 return {},它是个正常的返回值,而不是错误,好好体会一下。

catch

catch 这东西其实和 then 一毛同样,只不过不须要成功回调,promise.catch(fn) 至关于 promise.then(null, fn),也就是说若是 catch 后面若是还有 then 也是能够继续执行的,咱们直接看下面的代码就了解了:

Promise.resolve 和 Promise.reject

这里咱们先简要看一下 Promise.resolve 的用法:

Promise.resolve(1).then(res => console.log(res))
Promise.resolve(
    new Promise((resolve, reject) => resolve(2))
).then(res => console.log(res))
// 1
// 2
复制代码

首先 Promise.resolve 是个静态方法(就是只能用类来调用,比如 Math.random()),它能够进行链式调用,因此它返回的也是个 Promise,只不过要注意的是 resolve 里面接收的参数能够是 Promise 和通常值(数字、字符串、对象等),若是是 Promise 则须要等这个参数 Promise 执行完再返回,让咱们看下下面的代码:

Promise.reject 也是同样的写法,它们都算是语法糖,这里就略过了。

finally

finally 的特色是不论正确与否,它总会执行,接收一个函数参数,而且返回 Promise,不过要注意的是它向下传递的值是上一次的值而不是 finally 中的值,具体以下:

Promise.all

仍是同样咱们先来看下具体用法:

MyPromise.all([new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 1000);
}), 2, 3]).then(res => {
  console.log(res)
})
// [1, 2, 3]    (1s后打印)
复制代码

首先 all 方法接收一个数组参数,数组的每一项能够是 Promise,也能够是常量等有返回值的东西;其次使用 all 方法以后也能够用 then 进行链式调用,因此 all 方法返回的也是个 Promise;最后 all 是并发执行,其实就是写个循环,不过只有所有成功才算是成功,不然就算失败,直接看下面的代码:

Promise.race

race 其实和上面的 all 差很少,可是规则有点不同,它也是接收一个数组,只不过返回的就一项,最早返回成功就成功,最早失败就失败,代码也是和上面雷同,具体以下:

相关文章
相关标签/搜索