Open yuanyuanbyte opened 2 years ago
// 测试代码 console.log(1); let promise1 = new myPromise((resolve, reject) => { console.log(2); setTimeout(() => { console.log('A', promise1.PromiseState); resolve('这次一定'); console.log('B', promise1.PromiseState); console.log(4); }); }) promise1.then( result => { console.log('C', promise1.PromiseState); console.log('fulfilled:', result); }, reason => { console.log('rejected:', reason) } )
setTimeout(()=>{ console.log('setTimeout') }) console.log(3);
本系列的主题是 JavaScript 深入系列,每期讲解一个技术要点。如果你还不了解各系列内容,文末点击查看全部文章,点我跳转到文末。
如果觉得本系列不错,欢迎 Star,你的支持是我创作分享的最大动力。
前言
如果你问我有什么方法可以让自己JS的技术活生生地提升一个等级?🙀
那就是手写Promise了!!!
😺手写Promise 有一个难点就在于有很多地方需要和原生一样严谨,也就是说原生的Promise会考虑很多特殊情况~🧐
我们在实际运用时可能暂时不会碰到这些情况,可是当我们遇到的时候 却不知底层的原理,无法精准定位和解决问题,
这就是为什么我们要知道如何手写Promise
如果你问我为什么看了这么多教程还是不懂如何手写Promise,那就是因为这里头有很多细节难点,很少人有人愿意把这些都讲出来,不过我今天就要把这里头的细节一个个给抠出来,所以请大家务必先收藏再观看 ~ 奥力给😸😸😸
手写Promise包含以下知识点 👇:
不必担心因为上面的知识点不熟练而无法进行"手写Promise"的学习,因为本文附带
包会套餐
👇:🔍 如果你不太熟悉Promise的话,建议先看我之前写的那篇Promise文章: 通俗易懂的Promise知识点总结,检验一下你是否真的完全掌握了promise?
🔍 如果不知道
类 class
是如何使用的,建议参考我写的这篇文章:ES6新特性 Class 类的全方面理解🔍 其他知识点讲解文章,会在文中列出,不用担心,你只需要跟着这篇文中走就完了~
手写之前先简要的复习一下 Promise,现在我们就来一边回忆一边实现Promise吧 🪐~
◾ promise 核心要点
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)一个
Promise
必然处于以下几种状态之一 👇:(pending)
: 初始状态,既没有被兑现,也没有被拒绝。(fulfilled)
: 意味着操作成功完成。(rejected)
: 意味着操作失败。当 promise 被调用后,它会以处理中状态
(pending)
开始。 这意味着调用的函数会继续执行,而 promise 仍处于处理中直到解决为止,从而为调用的函数提供所请求的任何数据。被创建的 promise 最终会以被解决状态
(fulfilled)
或 被拒绝状态(rejected)
结束,并在完成时调用相应的回调函数(传给 then 和 catch)。◾ 为了让读者尽快对promise有一个整体的理解,我们先来看一段promise的例子 🌰:
输出结果为:
这里包含了四个知识点 👇:
resolve()
,Promise状态会变成fulfilled
,即 已完成状态reject()
,Promise状态会变成rejected
,即 被拒绝状态第一次为准
,第一次成功就永久
为fulfilled
,第一次失败就永远状态为rejected
throw
的话,就相当于执行了reject()
◾ 接下来看下面一段代码,学习新的知识点:
输出结果为:
这里包含了三个知识点 👇:
pending
resolve()
、reject()
以及throw
的话,这个promise的状态也是pending
pending
状态下的promise不会执行回调函数then()
◾ 最后一点:
输出结果:
这个里包含了一个知识点:
Promise
对象传入一个执行函数,否则将会报错。一、定义初始结构
原生的promise我们一般都会用new来创建实例 👇 :
所以我们手写的时候可以用构造函数或者class来创建,为了方便代码的整体观看就用class。
🔍 如果不知道
类 class
是如何使用的,建议参考我写的这篇文章:ES6新特性 Class 类的全方面理解把我们手写的Promise命名为myPromise,具体名字可以按自己想法,都可以
首先创建一个
myPromise
类在new一个promise实例的时候肯定是需要传入参数的
不然这个实例用处不大;而这个参数我们知道是一个函数,并且当我们传入这个函数参数的时候,这个函数参数会自动执行。
因此,我们需要在类的
构造函数constructor
里面添加一个参数,这里就用func来做形参,并且执行一下这个参数二、实现 resolve 和 reject
接下来,大家都知道需要为这个函数参数传入它自己的函数,也就是
resolve()
和reject()
原生的promise里面可以传入
resolve
和reject
两个参数那么我们也得允许手写这边可以传入这两个参数:
这里这样写明显有一个问题 🤨,那就是手写这边不知道哪里调用
resolve()
和reject()
这两个参数,毕竟resolve()
和reject()
还没有定义因此就需要创造出这两个对象 😀
有一点我们需要知道的是
resolve()
和reject()
也是以函数的形式来执行的,我们在原生promise
里也是在resolve
和reject
后面加括号()
来执行的,因此我们可以用类方法的形式来创建这两个函数:创建这两个方法以后我们发现
func
里面的两个参数颜色还是原来的颜色,编辑器就是在告诉我们:这两个参数还没有创建噢~😲等下,刚刚不是已经创建了吗?🦁
是的,但是我们需要用
this
来调用自身class
的方法,因此我们需要在构造函数里把两个参数前加上this
:那么这里的
resolve()
和reject()
方法应该如何执行呢?里面应该写什么内容呢?😯这就需要用到状态了 😛
1. 管理状态和结果
promise有三种状态:分别是
pending
,fulfilled
和rejected
pending
pending
可以转为fulfilled
状态,但是不能逆转pending
也可以转为rejected
状态,但是也不能逆转fulfilled
和rejected
也不能互转因此我们需要提前先把这些状态定义好,可以用
const
来创建外部的固定变量,但是这里为了统一就用static
来创建静态属性
:创建了状态属性以后,还需要为每一个实例添加一个
状态属性
,在前面讲到得“Promise 核心要点”
章节,我们已经知道原生Promise用PromiseState
这个字段来保存实例的状态属性,这里就也用this.PromiseState
来保存实例的状态属性,这个状态属性默认就是待定pending
状态,这样在每一个实例被创建以后就会有自身的状态属性可以进行判断和变动了那么在执行
resolve()
的时候就需要判断状态是否为待定 pending
,如果是待定 pending
的话就把状态改为成功 fulfilled
:同样,为给
reject
添加参数,并且把参数赋值给实例的PromiseResult
属性:◾ 执行
resolve()
和reject()
可以传参现在我们再回忆一下原生
Promise
🙂,在执行resolve()
或者reject()
的时候都是可以传入一个参数,这样我们后面就可以使用这个参数了我们可以把这个结果参数命名为
PromiseResult
(和原生Promise保持一致),不管是成功还是拒绝的结果,两者选其一,我们让每个实例都有PromiseResult
属性,并且给他们都赋值null
,这里给空值null
是因为执行resolve()
或者reject()
的时候会给结果赋值:接着我们就可以给
resolve()
添加参数,并且把参数赋值给实例的PromiseResult
属性:同样,为给
reject()
添加参数,并且把参数赋值给实例的PromiseResult
属性:2. this 指向问题
现在的代码看起来风平浪静的,但很多人会在这里犯错~😥
大家觉得这里有什么错误?🧐
我们来
new
一个实例 🌰 执行一下代码就知道有没有问题了运行上面代码,报错 🦁:
Uncaught TypeError: Cannot read property 'PromiseState ' of undefined
可从报错的信息里面我们貌似发现不了有什么错误🤨,因为
PromiseState
属性我们已经创建了,不应该是undefined
~🔍 但我们仔细看看
resolve()
和reject()
方法里调用PromiseState
,前面是有this
关键字的😲那么只有一种可能🧐,调用
this.PromiseState
的时候并没有调用constructor
里的this.PromiseState
,也就是这里的this
已经跟丢了~我们在
new
一个新实例的时候执行的是constructor
里的内容,也就是constructor
里的this
确实是新实例的,但现在我们是在新实例被创建后再在外部环境下执行resolve()
方法的,这里的resolve()
看着像是和实例一起执行的,其实不然,也就相当于不在class
内部使用这个this
,而我们没有在外部定义任何PromiseState
变量,因此这里会报错解决
class
的this
指向问题一般会用箭头函数,bind
或者proxy
,在这里我们就可以使用bind
来绑定this
,只需要在构造函数constructor
中的this.resolve
和this.reject
后加上,.bind(this)
就可以了 😺:🔍 如果这里有点蒙圈,不太懂为什么这样写,可以参考我之前写的关于
this
指向的文章:▪ call、apply和bind方法的用法、区别和使用场景 ▪ 手写 实现call、apply和bind方法 超详细!!!
我们接着往下写~
对于
resolve
来说,这里就是给实例的resolve()
方法绑定这个this
为当前的实例对象,并且执行this.resolve()
方法:对于
reject
来说,这里就是给实例的reject
方法绑定这个this
为当前的实例对象,并且执行this.reject
方法:咱们来测试一下代码吧:
上面是我们手写的
myPromise
的执行情况,看看原生Promise的执行情况:说明执行结果符合我们的预期,是不是觉得离成功又进了一步啦~ 👏👏👏
那么大家觉得下一步我们要做什么?是不是很多同学觉得需要写
then
了?那么我们就先来满足想要写then
的同学们~三、实现 then 方法
因为
then
是在创建实例后再进行调用的,因此我们再创建一个 类方法,可千万不要创建在constructor
里面了~😛我想应该有些同学突然失忆😶,不记得
then
怎么用了,我们就来稍微写一下原生的then
方法:then
方法可以传入两个参数,这两个参数都是函数,一个是当状态为fulfilled 成功
时执行的代码,另一个是当状态为rejected 拒绝
时执行的代码。虽然很多人可能一直只用一个函数参数,但不要忘记这里是两个函数参数🧐。
因此我们就可以先给手写的
then
里面添加 两个参数:onFulfilled
表示“当状态为成功时”
onRejected
表示“当状态为拒绝时”
1. 状态不可变
这里我们先看看
原生 Promise
产生的结果:可以看到控制台只显示了一个
console.log
的结果,证明Promise
只会执行成功状态
或者拒绝状态
的其中一个也就是我们前文讲到的,
Promise
只以第一次为准
,第一次成功就永久
为fulfilled
,第一次失败就永远
状态为rejected
因此我们在手写的时候就必须进行判断 🤖:
◾ 如果当前实例的
PromiseState
状态属性为fulfilled 成功
的话,我们就执行传进来的onFulfilled
函数,并且为onFulfilled
函数传入前面保留的PromiseResult
属性值:◾ 如果当前实例的
PromiseState
状态属性为rejected 拒绝
的话,我们就执行传进来的onRejected
函数,并且为onRejected
函数传入前面保留的PromiseResult
属性值:定义好了判断条件以后,我们就来测试一下代码,也是一样,在实例 🌰 上使用
then
方法:执行上面的测试代码,查看控制台:
可以看到控制台只显示了一个
console.log
的结果:这次一定
😎,证明我们已经实现了promise的状态不可变
👏👏👏写到这里并没有报错,也就是我们
暂时安全
了,为什么说暂时安全
呢?因为这里还有很多没有完善的地方,手写Promise的时候,有一个难点就在于有很多地方需要和原生一样严谨,也就是说原生的Promise会考虑很多特殊情况~
我们在实际运用时可能暂时不会碰到这些情况,可是当我们遇到的时候 却不知底层的原理,
这就是为什么我们要知道如何手写Promise
接着写 💪
2. 执行异常 throw
在
new Promise
的时候,执行函数里面如果抛出错误,是会触发then
方法的第二个参数,即rejected
状态的回调函数也就是在原生的Promise里面,
then
方法的第二个参数,即rejected
状态的回调函数可以把错误的信息作为内容输出出来到这里,有的同学可能会说,执行异常抛错,不是用
catch()
方法去接吗?为什么这里又说是会触发then方法的第二个参数,即rejected状态的回调函数
?😵那我们就说道说道吧🧐:
catch()
方法返回一个Promise
,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected)
相同。事实上, calling
obj.catch(onRejected)
内部callsobj.then(undefined, onRejected)
。(这句话的意思是,我们显式使用obj.catch(onRejected)
,内部实际调用的是obj.then(undefined, onRejected)
)Promise.prototype.catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。◾ 注意看下面的例子 🌰:
上面代码中,promise抛出一个错误,就被
catch()
方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。比较上面两种写法,可以发现
reject()
方法的作用,等同于抛出错误。这一点很重要,因为我们手写Promise就是用try/catch
来处理异常,用的就是上面的思想。◾ 一般来说,不要在
then()
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法。上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面
then
方法执行中的错误,也更接近同步的写法(try/catch
)。因此,建议总是使用catch()
方法,而不使用then()
方法的第二个参数。回到正题
原生Promise在
new Promise
的时候,执行函数里面如果抛出错误,是会触发then
方法的第二个参数(即rejected状态的回调函数)
,把错误的信息作为内容输出出来:但是如果我们在手写这边写上同样道理的测试代码,很多人就会忽略这个细节😥:
我们看看控制台
可以发现报错了😰,没有捕获到错误,没有把内容输出出来
◾ 我们可以在执行
resolve()
和reject()
之前用try/catch
进行判断,在构造函数 constructor
里面完善代码,判断生成实例的时候是否有报错 🔍:resolve()
和reject()
方法reject()
方法,并且直接执行reject()
方法◾ 注意这里不需要给
reject()
方法进行this
的绑定了,因为这里是直接执行,而不是创建实例后再执行。▪
func(this.resolve.bind(this), this.reject.bind(this));
这里的this.reject
意思是:把类方法reject()
作为参数 传到构造函数constructor
里要执行的func()
方法里,只是一个参数,并不执行,只有创建实例后调用reject()
方法的时候才执行,此时this
的指向已经变了,所以想要正确调用myPromise
的reject()
方法就要通过.bind(this))
改变this
指向。▪
this.reject(error)
,这里的this.reject()
,是直接在构造函数里执行类方法,this
指向不变,this.reject()
就是直接调用类方法reject()
,所以不用再进行this
绑定。◾ 这里考察了
this
绑定的一个细节🔍:call
、apply
和bind
都可以改变函数体内部 this 的指向,但是bind
和call/apply
有一个很重要的区别:一个函数被call/apply
的时候,会立即执行函数,但是bind
会创建一个新函数,不会立即执行。这就是前面为什么说,
this.reject.bind(this)
只是作为参数,并没有直接执行的原因了~😀回到正文
结合前面的讲解,刷新一下控制台,我们可以看到手写这边已经没有报错了👏👏👏:
3. 参数校验
大家觉得目前代码是不是没问题了?可以进行下一步了?
如果你觉得是的话就又掉坑了~🦁
原生Promise里规定
then
方法里面的两个参数如果不是函数的话就要被忽略,我们就故意在原生代码这里不传入函数作为参数:运行以后我们发现在这里执行是没有问题的:
我们再以同样类似的不传 函数参数 的代码应用在 手写代码 上面:
大家想想会不会有什么问题?来看看结果会怎样?🧐
结果就是
Uncaught TypeError: onFulfilled is not a function
。浏览器帮你报错了,这不是我们想要的~😥我们只想要自己来抛出错误,再来看看刚刚的手写
then
部分:我们会在里面分别执行成功和拒绝两个参数,可是我们不想修改这里的代码,那么就只能把不是函数的参数改为函数
Promise
规范如果onFulfilled
和onRejected
不是函数,就忽略他们,所谓“忽略”并不是什么都不干,对于onFulfilled
来说“忽略”就是将value
原封不动的返回,对于onRejected
来说就是返回reason
,onRejected
因为是错误分支,我们返回reason
应该throw
一个Error
:这里我们就可以用
条件运算符
,我们在进行if
判断之前进行预先判断:▪ 如果
onFulfilled
参数是一个函数,就把原来的onFulfilled
内容重新赋值给它,如果onFulfilled
参数不是一个函数,就将value
原封不动的返回▪ 如果
onRejected
参数是一个函数,就把原来的onRejected
内容重新赋值给它,如果onRejected
参数不是一个函数,就throw
一个Error
现在我们再来测试一下代码:
查看控制台,发现没有报错了👏👏👏:
当前实现的完整代码:
四、实现异步
1. 添加定时器
在对代码进行一些基本修补以后,我们就可以来进行下一个大功能了,也就是Promise的 异步功能 ✨。
可以说我们在手写的代码里面依旧没有植入异步功能,毕竟最基本的
setTimeout
我们都没有使用,但是我们必须先了解一下原生Promise的一些运行顺序规则
。在这里我为原生代码添加上步骤信息:
我们配合这段原生Promise代码,结合控制台一起看看
输出顺序为:
console.log(1)
,输出1
promise实例
,输出2
,因为这里依旧是同步resolve
的时候,修改结果值promise.then
会进行异步操作,也就是我们 需要先把执行栈的内容清空,于是就执行console.log(3)
,输出3
promise.then
里面的内容,也就是最后输出“fulfilled: 这次一定”
▪ 我们用同样的测试代码应用在 手写代码 上面:
这次我们发现有些不同了😯,输出顺序为:
1
和2
都没有问题,问题就是“fulfilled: 这次一定”
和3
这里的顺序不对◾ 其实问题很简单,就是我们刚刚说的 没有设置异步执行 😶
我们二话不说直接给
then
方法里面添加setTimeout
就可以了😎,需要在进行if
判断以后再添加setTimeout
,要不然状态不符合添加异步也是没有意义的,然后在setTimeout
里执行传入的函数参数:我们使用前面的用例重新测试一下代码:
输出顺序为:
这次的顺序就比较顺眼了~👏👏👏
在这里我们解决异步的方法是给
resolve
和reject
添加setTimeout
,但是为什么要这么做呢?◾ 这就要讲到
Promises/A+
规范 了规范
2.2.4
:译文:
2.2.4
onFulfilled
和onRejected
只有在执行环境
堆栈仅包含平台代码时才可被调用注1
规范对2.2.4做了注释:
译文:
3.1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保
onFulfilled
和onRejected
方法异步执行,且应该在then
方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用“宏任务(macro-task)”机制,比如setTimeout
或者setImmediate
; 也可以采用“微任务(micro-task)”机制来实现, 比如MutationObserver
或者process.nextTick
。 由于 promise 的实施代码本身就是平台代码(译者注: 即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列或『跳板』)。这里我们用的就是规范里讲到的 “宏任务”
setTimeout
。2. 回调保存
异步的问题真的解决了吗?现在又要进入Promise另一个难点了,大家务必竖起耳朵啦😛
我们来给原生的Promise里添加
setTimeout
,使得resolve
也异步执行,那么就会出现一个问题了,resolve
是异步的,then
也是异步的,究竟谁会先被调用呢?输出顺序为:
特别要注意的是当遇到
setTimeout
的时候被异步执行了,而resolve('这次一定')
没有被马上执行,而是先执行console.log(4)
,等到then
的时候再执行resolve
里保存的值。这里涉及到了浏览器的事件循环,
promise.then()
和setTimeout()
都是异步任务,但实际上异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务 (micro task)
和宏任务 (macro task
)。setTimeout()
属于宏任务promise.then()
属于微任务在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。
我们只需记住 当 当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
回到正文
我们用同样的代码应用到手写的部分:
控制台输出:
可以发现
fulfilled: 这次一定
并没有输出我们可以先猜测一下,没有输出的原因很可能是因为
then
方法没有被执行,看看then
方法里面是根据条件判断来执行代码的:也就是说很可能没有符合的条件,再换句话说可能没有符合的状态
那么我们就在三个位置分别输出当前的状态,这样分别来判断哪个位置出了问题:
输出结果为:
发现只有两组状态被输出,这两组都在
console.log(4)
前被输出,证明setTimeout
里面的状态都被输出了,只有then
里面的状态没有被输出这基本就可以确定是因为
then
里的状态判断出了问题这里涉及到事件循环,我们详细解读一下:
▪ 首先,执行
console.log(1)
,输出1
▪ 第二步,创建promise,执行函数体里的
console.log(2)
,输出2
▪ 第三步,遇到
setTimeout
,setTimeout
是宏任务,将setTimeout
加入宏任务队列,等待执行▪ 第四步,遇到
promise.then()
,promise.then()
是微任务,将promise.then()
加入微任务队列,等待执行▪ 第五步,执行
console.log(3)
,输出3
,此时当前执行栈已经清空▪ 第六步,当前执行栈已经清空,先执行微任务队列的任务
promise.then()
,发现promise的状态并没有改变,还是pending
,所以没有输出。状态并没有改变的原因是:resolve('这次一定')
是在setTimeout
里的,但此时还没开始执行setTimeout
,因为setTimeout
是宏任务,宏任务在微任务后面执行▪ 第七步,微任务队列已经清空,开始执行宏任务
setTimeout
:▪ 第八步,执行
console.log('A',promise1.PromiseState)
,此时promise状态还没发生变化,还是pending
,所以输出A pending
▪ 第九步,执行
resolve('这次一定');
,改变promise的状态为fulfilled
▪ 第十步,执行
console.log('B',promise1.PromiseState)
,输出B fulfilled
▪ 第十一步,执行
console.log(4)
,输出4
◾ 分析完上面的代码,我们知道了,因为先执行了
then
方法,但发现这个时候状态依旧是pending
,而我们手写部分没有定义pending
待定状态的时候应该做什么,因此就少了fulfilled: 这次一定
这句话的输出所以我们就 直接给
then
方法里面添加待定状态的情况就可以了,也就是用if
进行判断:◾ 但是问题来了,当
then
里面判断到pending
待定状态时我们要干什么?因为这个时候
resolve
或者reject
还没获取到任何值,因此我们必须让then
里的函数稍后再执行,等resolve
执行了以后,再执行then
为了保留
then
里的函数,我们可以创建数组
来 保存函数。为什么用
数组
来保存这些回调呢?因为一个promise实例可能会多次then
,也就是经典的链式调用
,而且数组是先入先出的顺序在实例化对象的时候就让每个实例都有这两个数组:
onFulfilledCallbacks
:用来 保存成功回调onRejectedCallbacks
:用来 保存失败回调◾ 接着就完善
then
里面的代码,也就是当判断到状态为pending
待定时,暂时保存两个回调,也就是说暂且把then
里的两个函数参数分别放在两个数组里面:◾ 数组里面放完函数以后,就可以完善
resolve
和reject
的代码了在执行
resolve
或者reject
的时候,遍历自身的callbacks
数组,看看数组里面有没有then
那边 保留 过来的 待执行函数,然后逐个执行数组里面的函数,执行的时候会传入相应的参数:完善好代码后,让我们再来测试以下刚才的实例:
输出结果:
从上面的结果我们可以看到
fulfilled: 这次一定
打印出来啦,promise1.then()
方法也正常执行,打印出了当前的状态:B fulfilled
但是
细心的同学可能已经发现了,代码输出顺序还是不太对,原生Promise中,
fulfilled: 这次一定
是最后输出的◾ 这里有一个很多人忽略的小细节,要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。因此,在保存成功和失败回调时也要添加
setTimeout
我们在判断完
promise
状态后再加setTimeout
:细节补充好了,当前实现的完整代码:
检验一下这次是否能行:
输出顺序:
可以看到最后输出
fulfilled: 这次一定
,和原生Promise顺序一致!到这里我们已经完成了 promise的回调保存,已经越来越接近胜利了😺
3. 验证 then 方法多次调用
Promise 的 then 方法可以被多次调用。
用一个 🌰 ,来验证一下我们写的promise
then
方法是否可以多次调用:运行上面 🌰,输出结果👇
所有
then
中的回调函数都已经执行 😎说明我们当前的代码,已经可以实现
then
方法的多次调用✨👏👏👏 完美,继续
五、实现 then 方法的链式调用
我们常常用到
new Promise().then().then()
,这就是链式调用,用来解决回调地狱举个例子 🌰
输出👇:
再举一个例子 🌰 :
输出👇:
我们先试一下当前的
myPromise
是否可以实现链式调用:毫无疑问在控制台里面是会报错的,提示
then
方法没有定义:Uncaught TypeError: Cannot read property 'then' of undefined
Promise.prototype.then()
方法返回一个新的Promise实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。1. Promises/A+ 规范的理解
◾ 想要实现
then
方法的链式调用,就必须彻底搞懂then
方法,这里我们参考 [Promises/A+
规范](https://promisesaplus.com/#notes) 👇规范在
2.2.7
中这样描述 👇:◾ 2.2.7 then 方法必须返回一个 promise 对象
onFulfilled
或者onRejected
返回一个值x
,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x)
onFulfilled
或者onRejected
抛出一个异常e
,则promise2
必须拒绝执行,并返回拒因e
onFulfilled
不是函数且promise1
成功执行,promise2
必须成功执行并返回相同的值onRejected
不是函数且promise1
拒绝执行,promise2
必须拒绝执行并返回相同的据因理解上面的
“返回”
部分非常重要,即:不论 promise1 被 reject 还是被 resolve 时 promise2 都会执行 Promise 解决过程:[[Resolve]](promise2, x)
,只有出现异常时才会被 rejected。注意 2.2.7.1 :
即:如果
onFulfilled
或者onRejected
返回一个值x
,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x)
规范在 2.3 中详细描述 Promise 解决过程
The Promise Resolution Procedure
👇译过来 👇:
◾ 2.3 Promise 解决过程
Promise 解决过程 是一个抽象的操作,其需输入一个
promise
和一个值,我们表示为[[Resolve]](promise, x)
,如果x
有then
方法且看上去像一个Promise
,解决程序即尝试使promise
接受x
的状态;否则其用x
的值来执行promise
。这种
thenable
的特性使得Promise
的实现更具有通用性:只要其暴露出一个遵循Promises/A+
协议的then
方法即可;这同时也使遵循Promises/A+
规范的实现可以与那些不太规范但可用的实现能良好共存。运行
[[Resolve]](promise, x)
需遵循以下步骤:▪ 2.3.1
x
与 promise 相等如果
promise
和x
指向同一对象,以TypeError
为据因拒绝执行promise
▪ 2.3.2
x
为 Promise如果
x
为 Promise ,则使promise
接受x
的状态x
处于等待态,promise
需保持为等待态直至x
被执行或拒绝x
处于执行态,用相同的值执行promise
x
处于拒绝态,用相同的据因拒绝promise
▪ 2.3.3
x
为对象或函数如果 x 为对象或者函数:
2.3.3.1 把
x.then
赋值给then
2.3.3.2 如果取
x.then
的值时抛出错误e
,则以e
为据因拒绝promise
2.3.3.3 如果
then
是函数,将x
作为函数的作用域this
调用之。传递两个回调函数作为参数,第一个参数叫做resolvePromise
,第二个参数叫做rejectPromise
:2.3.3.3.1 如果
resolvePromise
以值y
为参数被调用,则运行[[Resolve]](promise, y)
2.3.3.3.2 如果
rejectPromise
以据因r
为参数被调用,则以据因r
拒绝promise
2.3.3.3.3 如果
resolvePromise
和rejectPromise
均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用2.3.3.3.4 如果调用
then
方法抛出了异常e
:resolvePromise
或rejectPromise
已经被调用,则忽略之e
为据因拒绝promise
2.3.3.4 如果
then
不是函数,以x
为参数执行promise
▪ 2.3.4 如果
x
不为对象或者函数,以x
为参数执行promise
如果一个
promise
被一个循环的thenable
链中的对象解决,而[[Resolve]](promise, thenable)
的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的TypeError
为据因来拒绝promise
。2. Promises/A+ 规范的总结
基于规范的描述,我们得到以下几点:
◾ 1.
then
方法本身会返回一个新的Promise
对象,返回一个新的Promise以后它就有自己的then
方法,这样就能实现无限的链式◾ 2. 不论
promise1
被resolve()
还是被reject()
时promise2
都会执行Promise 解决过程:[[Resolve]](promise2, x)
在手写这里我们把这个
Promise 解决过程:[[Resolve]](promise2, x)
命名为resolvePromise()
方法,参数为(promise2, x, resolve, reject)
即:resolvePromise()
各参数的意义:其实,这个
resolvePromise(promise2, x, resolve, reject)
即Promise 解决过程:[[Resolve]](promise2, x)
就是对resolve()、reject()
进行改造增强, 针对resolve()
和reject()
中不同值情况 进行处理。resolve()
和reject()
返回的x
值的几种情况:下面我们就根据总结的两点,结合
Promises/A+ 规范
来实现then
方法的链式调用 💪💪💪3. then 方法返回一个新的Promise
◾ 2.2.7规范 then 方法必须返回一个 promise 对象
我们在
then
方法里面返回一个新的手写Promise实例
**,再把原来的代码复制上去:◾ 2.2.7.1规范 如果
onFulfilled
或者onRejected
返回一个值x
,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x)
我们在
myPromise
类外面声明了一个 Promise 解决过程:resolvePromise()
具体方法我们后面会补充~◾ 2.2.7.2 如果
onFulfilled
或者onRejected
抛出一个异常e
,则promise2
必须拒绝执行,并返回拒因e
◾
fulfilled
和rejected
状态处理完,不要忘了pending
状态的情况我们在
pending
状态保存的resolve()
和reject()
回调也要符合2.2.7.1 和 2.2.7.2 规范
:◾ 2.2.7.3 如果
onFulfilled
不是函数且promise1
成功执行,promise2
必须成功执行并返回相同的值◾ 2.2.7.4 如果
onRejected
不是函数且promise1
拒绝执行,promise2
必须拒绝执行并返回相同的据因规范 2.2.7.3 和 2.2.7.4 对
onFulfilled
和onRejected
不是函数的情况做了更详细的描述,根据描述我们对onFulfilled
和onRejected
引入了新的参数校验,所以之前的参数校验就可以退役了:搞定
then
方法 😎下面我们开始着手写 promise 解决过程
resolvePromise(promise2, x, resolve, reject)
六、实现 resolvePromise 方法
◾ 2.3.1 如果
promise
和x
指向同一对象,以TypeError
为据因拒绝执行promise
在这里我们只需要抛出一个
TypeError
的异常即可,因为调用resolvePromise
方法外层的try...catch
会抓住这个异常,然后 以 TypeError 为据因拒绝执行 promise。如果从
onFulfilled
或onRejected
中返回的 x 就是 promise2,会导致 循环引用报错,这部分的处理就是要解决这个问题。举一个 循环引用 的例子🌰:
使用原生 Promise 执行这个代码,会报类型错误:
◾ 2.3.2 如果
x
为 Promise ,则使promise
接受x
的状态马上就要成功啦😸,还有最后一条😎
◾ 2.3.3 如果
x
为对象或者函数 在判断x
是对象或函数时,x
不能是null
,因为typeof null
的值也为object
我们应该显式的声明
x != null
,这样 当x
为null
时,直接执行resolve(x)
,否则,如果不这样不声明,x
为null
时就会走到catch
然后reject
,这不是我们要的,所以需要检测下null
:◾ 2.3.4 如果
x
不为对象或者函数,以x
为参数执行promise
打完收工✨✨✨✨
resolvePromise()
方法 完整代码:七、完整的 Promises/A+ 实现
到这里我们的
myPromsie
已经完成了 Promises/A+ 规范 😸这里为大家提供了两个完整的 Promises/A+ 实现版本:
1. 清爽简洁 无注释版
完整的 Promises/A+ 实现
(清爽简洁 无注释版)
:完整版的代码较长,这里如果看不清楚的可以去我的GitHub上看,我专门维护了一个 手写 Promsie 的仓库:https://github.com/yuanyuanbyte/Promise
2. 按步分析 注释加持版
完整的 Promises/A+ 实现
(按步分析 注释加持版)
:完整版的代码较长,这里如果看不清楚的可以去我的GitHub上看,我专门维护了一个 手写 Promsie 的仓库:https://github.com/yuanyuanbyte/Promise
八、Promises/A+ 测试
如何证明我们写的
myPromise
就符合 Promises/A+ 规范呢?跑一下 Promise A+ 测试 就好啦~
1. 安装官方测试工具
我们使用Promises/A+官方的测试工具 promises-aplus-tests 来对我们的
myPromise
进行测试安装
promises-aplus-tests
:安装完测试工具后的项目目录:
2. 使用 CommonJS 对外暴露 myPromise 类
3. 实现静态方法 deferred
要使用
promises-aplus-tests
这个工具测试,必须实现一个静态方法deferred()
,官方对这个方法的定义如下:意思就是:
我们要给自己手写的
myPromise
上实现一个静态方法deferred()
,该方法要返回一个包含{ promise, resolve, reject }
的对象:promise
是一个处于pending
状态的 Promsie。resolve(value)
用value
解决上面那个promise
reject(reason)
用reason
拒绝上面那个promise
deferred()
的实现如下:4. 配置 package.json
我们实现了
deferred
方法,也通过 CommonJS 对外暴露了myPromise
,最后配置一下package.json
就可以跑测试啦~😺新建一个
package.json
,配置如下:项目目录:
准备工作已就绪👏👏👏
激动人心的时刻马上就要到啦,嘿嘿😸
5. 完美通过官方872个测试用例
执行测试命令:
肯定都等不及了吧~😜 快来看看我们的测试结果吧,走起 🚀
Promises/A+ 官方测试总共872用例,我们手写的Promise完美通过了所有用例 🎉🎉🎉:
九、其他方法
在 ES6 的官方 Promise 还有很多API,比如:
虽然这些都不在 Promises/A+ 规范里面,但是我们也来实现一下吧,加深理解。其实我们前面我们用了很大功夫实现了 Promises/A+ ,现在再来实现这些已经是小菜一碟了,因为这些API全部是前面的封装而已。
1. 实现 Promise.resolve
2. 实现 Promise.reject
3. 实现 Promise.prototype.catch
4. 实现 Promise.prototype.finally
5. 实现 Promise.all
6. 实现 Promise.allSettled
7. 实现 Promise.any
8. 实现 Promise.race()
因文章字数限制,Promise 其他方法的手写实现已放在下篇
❤️ 结尾
◾ 更多更全更详细 的 优质内容, 猛戳这里查看
声明
本文 “第四节:实现异步” 之前的内容 都是学习自B站 up主 技术蛋老师:https://www.bilibili.com/video/BV1RR4y1p7my
没有 蛋老师 的视频,这篇文章可能要很晚才能跟大家见面~
蛋老师 https://space.bilibili.com/327247876 的视频质量都很高,节流和防抖我就是通过蛋老师的视频学习的,里面还有很多视频内容,通俗易懂,我自己都后悔没有早点接触蛋老师,少走多少弯路啊,欢迎大家支持关注蛋老师💝~
参考
查看原文
查看全部文章
博文系列目录
交流
各系列文章汇总:https://github.com/yuanyuanbyte/Blog
我是圆圆,一名深耕于前端开发的攻城狮。