希望有需要的小伙伴静下心来自己多摸索着写几篇,当你完完整整的实现过必定对你面试还是个人技术成长有好处。跟我们平时项目需求开发一样,只要思路屡清楚再开始编码,你就会发现原来 Promise 也不是传说中的那么复杂
在使用 Promise
解决工作中常见异步问题时,首先得知道其有哪些优缺点:
Promise 有哪些优缺点:
- 优点
- 可以解决异步嵌套的问题(回调地狱)
- 可以解决异步并发的问题
- 代码更清晰、链式调用等等
- 缺点
Promise
本身也是基于回调的Promise
无法终止,一创建立即执行,无法取消- 当状态为
Pending
时,无法得知代码进站在那个阶段(刚刚开始还是处理中)等等
注意:正因为有以上一些缺陷,ES7
中引入了异步终极解决方案 asyns + await
!
Promise 常见应用(举一例)
-
解决回调地狱
例:很常见的需求:获取一个文件中的内容(其中,一个文件的内容为另一个文件的文件名)
在不使用
Promise
之前,大多都是通过回调的方式完成的。但如果是多个文件的话,会照成两个很明显的不足:一是回调嵌套层数多、而是回调异常情况不能统一。若使用Promise
可以很容易解决以上两种情况。
简版的Promise
三种状态
Pending
:等待态(初始状态)Fulfilled
:成功态Rejected
:失败态
注意:状态只能从 Pending
等待态 变成 Fulfilled
成功态;或者从 Pending
等待态 变成 Rejected
失败态。并且状态一旦变成成功态或者失败态后就不能改变了。
执行器 - executor
Promise
默认接受一个执行器 executor
作为参数,并且该执行器会默认执行。
执行器 executor
默认接受两个参数:一个 resolve
成功的回调、一个 reject
失败的回调。
接下来,我们从处理 同步、异步两种情况来完善简约版的 Promise
。
核心函数 - then
then
方法会接受两个参数:一个是成功时的 onfulfilled
,一个是失败时的 onrejected
。注意:then
的参数是可选的。
处理同步
注意:在执行器 executor
在执行时,内部有可能报错,为了代码的健全性需要给执行器添加异常处理,报错相当于执行了失败态并将报错信息传递下去。
验证
-
成功情况
-
失败情况
注意:抛错也当成失败处理
以上代码逻辑支持基本功能,但回调函数中存在异步代码,以上的 Promise
就无法实现。请看下小节 处理异步 情况!
处理异步
若回调中存在异步代码,导致 Promise
实例的状态一直在 Pending
。上节只处理了Fulfilled
成功和 Rejected
失败两种状态,故 then
方法还需添加状态为 Pending
的情况。
验证
-
添加异步代码情况
由于
JS执行机制
,当执行then
时,此时Promise
实例的状态还是Pending
,导致代码什么也没有输出。 -
在then方法中添加
Pending
状态的情况注意:在处理实例为
pending
时,运用到了 发布订阅模式。
完整版的Promise
在编写完整版 Promise
前,先看下以下常见问题:
- 为什么要加定时器?
- 为什么在处理异步情况的时候也要加try-catch?
- 返回值
x
有几种情况? - 如何通过返回值
x
去推导promise2
的状态? - ....
接下来,我们就一起揭开以上问题吧~~~
核心方法 - then
then 方法传入成功和失败的回调函数,返回一个新的Promise对象
then 的实现原理
为了实现 then
方法的实现原理,我们从以下两点去考虑:
一、当前
Promise
的状态
Promise
实例调用 then
方法时,此时当前 Promise
的状态有可能是成功态 fulfilled
、有可能是失败态 rejected
、也有可能是等待态 pending
。故,需要从这三个状态去考虑then
的实现原理:
-
状态为成功
fulfilled
由于
then
方法是异步执行,所以需要添加一个setTimout
将成功的回调函数onfulfilled
包裹起来,并且将成功的值value
传入。 -
状态为失败
rejected
由于
then
方法是异步执行,所以需要添加一个setTimout
将成功的回调函数onrejected
包裹起来,并且将失败的原因reason
传入。 -
状态为等待
pending
当状态为
Pending
时,此时需要将成功的回调函数onfulfilled
和异步的回调函数onrejected
分别存放到对应的存放成功回调onResolvedCallbacks
的数组中和存放失败回调onRejectedCallbacks
的数组中。等待状态
变为成功态
或者失败态
时,依次执行数组里的回调函数。
二、执行 then 方法后返回新的 Promise
由于执行 then
方法,要返回一个新的 Promise
。不管当前 Promise
是哪一种状态执行后都会有一个返回值。为了可以实现链式调用,需要将该返回值传递下去。成功就 resolve
让新的 Promise
状态为成功态
,失败就 reject
让新的 Promise
状态为失败态
。
在返回新的 Promise
时,需要考虑以下几点:
- 用户一上来就
resolve
,此时由于JS执行机制
的影响,会出现新返回的Promise
为undefined
。解决此问题,需要添加一个定时器setTimeout
将当前上下文添加到异步队列
中等new Promise
执行完后再执行逻辑。 - 用户一上来就抛错,然而我们自己写的
Promise
中executor
的异常处理只能捕获同步异常情况。此时,需要在处理异步的情况下也添加一个try - catch
捕获异常。如果有异常的话,直接让新返回的promise
直接reject
。
注意:该返回值有几种情况,下小节我们会详解
三、判断返回值
x
去推导新返回的Promise
的状态
若 x
是普通值就直接 resolve
这个值;若 x
是 Promise
类型的就执行.then
看结果是成功还是失败;若 x
是失败的 Promise
就执行 reject
;通过这三种情况,我们编写一个公共的方法 - resolvePromise
来判断 x
和新的 Promise
的关系。
在编写公共 resolvePromise
方法时, Promise A+要求的需要注意一下几点:
-
x
的值不能和返回的promise
一样,否则报错。new TypeError('Chaining cycle detected for promise #<Promise>'
。 -
需要编写一个功能方法
isPromise
来判断x
的值是否是Promise
类型。若是,则调用.then
方法;若不是,则为普通值,调用promise2
的resolve
方法,并把值传递下去。 -
若
x
是Promise
类型时,x.then
有可能报错。此时需要添加一个try-catch
异常。用promise2
的reject
方法将错误信息抛出。 -
若
then
属性是一个函数的话,就默认规定x
是一个Promise
类型;若then
属性不是一个函数的话,就是一个普通对象,直接调用promise2
的resolve
方法并将值传入。 -
通过
then.call(x)
是为了不用再次取then
的值,防止取不到then
。并且将y
作为成功回调的参数,r
作为 失败回调的参数。下一层可以获取到promise2
返回成功的 y和失败的
r。只需要状态成功时调用promise2
的resolve
并将y
传入;状态失败时调用promise2
的reject
并将r
传入。 -
注意
y
有可能还是一个Promise
类型,此时需要递归调用resolvePromise
方法直到y
是一个普通值。
核心方法 - resolvePromise
-
判断对象是否是 Promise
-
完整版 Promise
测试Promise是否符合标准
-
安装测试Promise依赖包
npm install promises-aplus-tests -g
-
编写 - 延迟对象
-
执行验证
promises-aplus-tests promise.js
扩展
Promise.resolve
Promise.resolve 的作用:返回一个
fulfilled
的Promise
实例,或原始Promise
实例。
在实现该功能前,我们需要了解 Promise.resolve
方法中参数的情况:
- 若参数为空,则返回一个状态为
fulfilled
的Promise
实例 - 若参数为
Promise
实例,则返回这个实例,不做修改 - 若参数为普通值,则返回一个状态为
fulfilled
的Promise
实例并且fulfilled
响应函数会得到这个参数 - 若参数为
thenable
对象, 立即执行它的.then()
方法
注意:对象里含有 then
方法就叫 thenable
对象。
resolve 的实现
接下来,我们给不同的参数的例子来验证写的 Promise.resolve
是否符合预期。
Promise.reject
reject:返回一个状态为
rejected
的Promise
实例
reject 的实现
接下来,我们给不同的参数的例子来验证写的 Promise.reject
是否符合预期。
Promise.race
race 的实现
注意:若数组里传的不是一个 Promise
对象,需要将其通过 Promise.resolve
变成 Promise
接下来,我们给不同的参数的例子来验证写的 Promise.race
是否符合预期。
注意:是谁最先完成,并且最先完成的成功就成功,失败就失败。
Promise.all
all:返回一个Promise,只有当所有的promise都成功才成功,否则只有一个失败就失败
注意:若数组里传的不是一个 Promise
对象,需要将其通过 Promise.resolve
变成 Promise
all 的实现
接下来,我们给不同的参数的例子来验证写的 Promise.all
是否符合预期。
Promise.finally
finally:最终的(无论成功或失败都会执行)
finally 的实现
- 高逼格代码实现
finally
接下来,我们给不同的参数的例子来验证写的 Promise.finally
是否符合预期。
Promise.catch
catch:执行失败的回调函数,返回一个新的Promise对象
Promise.prototype.catch
方法是 then(null/undefined, reject)
的别名,两者效果一样。
catch 的实现
接下来,我们给不同的参数的例子来验证写的 Promise.catch
是否符合预期。
Promise常见的题
题目一
输出一下结果?
详细的分析一下这题的解题思路,首先我们要知道宏任务、微任务、任务队列等概念:
- 由于``JS
执行机制,代码从上往下执行,先执行完同步任务再执行异步任务,将异步任务都存放对应的**任务队列**中。故,一轮输出:
1和
8`。下面分析下执行异步时的情况: - 当代码执行到
setTimeout
时,由于setTimeout
是宏任务。故将2
存放在宏任务队列中等待执行。Promise
相关等方法属于微任务,将值依次存放在微任务队列中等待执行。 - 当代码执行到
new Promise
调用.then
方法时,由于回调函数中执行了resolve
方法,代码会走成功的回调,并且resolve
函数没有参数,所以成功的回调参数data
没有值。故,输出:3
和undefined
。 - 当代码执行到
Promise.resolve
时,直接走成功的回调,并且resolve
中有参数,所以将这个参数作为成功的回调参数的data
。故,输出6
和5
。 - 当第二轮微任务队列走完后,再走宏任务队列,故,第三轮输出:
2
。
注意:做此题的技巧:① 先走完同步
、② 再走异步
,其中异步方法
中先走微任务
再走宏任务
。
题目二
输出一下结果?
详细的分析一下这题的解题思路:
- 相信有很多小伙伴由第一道题的经验可知:第一轮输出
1
和7
。这样输出就错了!相信如果实现上方小节Promise
源码小节都知道,Promise
参数里的执行器
是默认立即执行的。故,第一轮输出:1
、3
、7
。下面分析下执行异步时的情况: - 同理,
2
存放到宏任务队列中。由于执行了resolve(4)
,那么第一个.then
方法的成功回调方法中的打印值会存放在微任务队列中等待执行。此时,要注意:第二个.then
方法的成功回调不会执行,一直在等待状态。故,第二轮输出:5
、4
。 - 当第一个
.then
方法执行完后,再执行第二个.then
方法,并且将成功回调方法中的打印值会存放在微任务队列中等待执行。由于第一个then
方法执行没有return
,所以第二个then
参数data
为undefined
。故,第三轮输出:6
和undefined
。 - 当第三轮微任务队列走完后,再走宏任务队列,故,第四轮输出:
2
。
注意:做此题的技巧:同上题技巧。但要明白 Promise
的特点。
题目三
输出一下结果?
详细的分析一下这题的解题思路:
- 由第二题可知,故,第一轮输出:
1
、3
、9
、11
。下面分析下执行异步时的情况: - 同理,
2
存放到宏任务队列中。第一个Promise
执行resolve
方法,所以先执行第一个Promise
中的第一个then
方法。4
和5
被存放在微任务队列中。但又new Promise
并且执行resolve
方法。注意:此时,第二个resolve
执行时,6
和7
的代码还没有存放在微任务队列中。第一个Promise
中的第二个then
方法中的8
也没有存放在微任务队列中。代码执行到三个new Promise
中调用resolve
方法,所以10
被存在微任务队列中。故,第二轮输出:4
、5
、10
。 - 执行上述分析的微任务队列中的
10
,当走第二个Promise
的resolve
方法时,6
被存放在微任务队列中,7
没有存放在微任务队列中。此时,8
被存放在微任务队列中。故,第三轮输出:10
、6
、8
。 - 最后执行第二个
Promise
的 第二个then
方法。故,第四轮输出:7
。 - 当第四轮微任务队列走完后,再走宏任务队列,故,第四轮输出:
2
。
注意:做此题的技巧:同上题技巧。但要知道Promise的状态是否是成功或者失败状态回调函数是否执行。
通过以上三道有关 Promise
的题,当你完全掌握了 Promise
、同步
、异步
、宏任务
、微任务
、任务队列
、JS执行机制
等相关知识点后,这种题都是可以用眼睛就能做出来。