最近这段时间因为疫情的缘由,在家实在闷得慌,因此看了下 js 的一些基础知识,从前不是很了解的 Promise 忽然豁然开朗了很多,因而就赶忙趁热打铁写下来(这就是温故而知新的感受吗,哈哈哈😁)。数组
确实是待久了,🌸樱花🌸都开了。
要想知道某个东西怎么写的,就要先学会用,因此读者大大们若是还有没用过的话,赶忙去学下再来看吧,由于本文的目标是手写一个 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+ 规范的(这东西至关于一个需求文档),这里我会先罗列几个必知必会的点,毕竟要手写嘛✍,肚子里得有点墨水:并发
下面的描述我也没写的规范那么正经,由于我有时候总以为那样不利于理解,一堆英文名字就把你给愣住了😯。框架
首先咱们把前面提到的基础用法简化下: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) } 复制代码
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 是有返回的,而且是个新的 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 这东西其实和 then 一毛同样,只不过不须要成功回调,promise.catch(fn) 至关于 promise.then(null, fn),也就是说若是 catch 后面若是还有 then 也是能够继续执行的,咱们直接看下面的代码就了解了:
这里咱们先简要看一下 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 的特色是不论正确与否,它总会执行,接收一个函数参数,而且返回 Promise,不过要注意的是它向下传递的值是上一次的值而不是 finally 中的值,具体以下:
仍是同样咱们先来看下具体用法:
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 是并发执行,其实就是写个循环,不过只有所有成功才算是成功,不然就算失败,直接看下面的代码:
race 其实和上面的 all 差很少,可是规则有点不同,它也是接收一个数组,只不过返回的就一项,最早返回成功就成功,最早失败就失败,代码也是和上面雷同,具体以下: