希望有需要的小伙伴静下心来自己多摸索着写几篇,当你完完整整的实现过必定对你面试还是个人技术成长有好处。跟我们平时项目需求开发一样,只要思路屡清楚再开始编码,你就会发现原来 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执行机制等相关知识点后,这种题都是可以用眼睛就能做出来。