Promise系列

2,110 阅读12分钟

希望有需要的小伙伴静下心来自己多摸索着写几篇,当你完完整整的实现过必定对你面试还是个人技术成长有好处。跟我们平时项目需求开发一样,只要思路屡清楚再开始编码,你就会发现原来 Promise 也不是传说中的那么复杂

在使用 Promise 解决工作中常见异步问题时,首先得知道其有哪些优缺点:

Promise 有哪些优缺点:

  • 优点
    1. 可以解决异步嵌套的问题(回调地狱)
    2. 可以解决异步并发的问题
    3. 代码更清晰、链式调用等等
  • 缺点
    1. Promise 本身也是基于回调的
    2. Promise 无法终止,一创建立即执行,无法取消
    3. 当状态为 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的实现原理:

  1. 状态为成功 fulfilled

    由于 then 方法是异步执行,所以需要添加一个 setTimout 将成功的回调函数 onfulfilled 包裹起来,并且将成功的值 value传入。

  2. 状态为失败 rejected

    由于 then 方法是异步执行,所以需要添加一个 setTimout 将成功的回调函数 onrejected 包裹起来,并且将失败的原因 reason传入。

  3. 状态为等待 pending

    当状态为 Pending 时,此时需要将成功的回调函数 onfulfilled 和异步的回调函数onrejected 分别存放到对应的存放成功回调 onResolvedCallbacks 的数组中和存放失败回调 onRejectedCallbacks 的数组中。等待状态变为成功态者失败态时,依次执行数组里的回调函数。

二、执行 then 方法后返回新的 Promise

由于执行 then 方法,要返回一个新的 Promise 。不管当前 Promise 是哪一种状态执行后都会有一个返回值。为了可以实现链式调用,需要将该返回值传递下去。成功就 resolve让新的 Promise 状态为成功态,失败就 reject让新的 Promise 状态为失败态

在返回新的 Promise 时,需要考虑以下几点:

  1. 用户一上来就 resolve ,此时由于 JS执行机制 的影响,会出现新返回的 Promiseundefined。解决此问题,需要添加一个定时器setTimeout当前上下文添加到异步队列中等new Promise 执行完后再执行逻辑。
  2. 用户一上来就抛错,然而我们自己写的 Promiseexecutor 的异常处理只能捕获同步异常情况。此时,需要在处理异步的情况下也添加一个 try - catch 捕获异常。如果有异常的话,直接让新返回的 promise 直接 reject

注意:该返回值有几种情况,下小节我们会详解

三、判断返回值 x 去推导新返回的 Promise 的状态

x 是普通值就直接 resolve这个值;若 xPromise 类型的就执行.then看结果是成功还是失败;若 x 是失败的 Promise 就执行 reject;通过这三种情况,我们编写一个公共的方法 - resolvePromise 来判断 x 和新的 Promise 的关系。

在编写公共 resolvePromise 方法时, Promise A+要求的需要注意一下几点:

  1. x 的值不能和返回的 promise 一样,否则报错。new TypeError('Chaining cycle detected for promise #<Promise>'

  2. 需要编写一个功能方法 isPromise 来判断 x 的值是否是 Promise 类型。若是,则调用.then方法;若不是,则为普通值,调用 promise2resolve方法,并把值传递下去。

  3. xPromise 类型时,x.then 有可能报错。此时需要添加一个 try-catch 异常。用 promise2reject 方法将错误信息抛出。

  4. then 属性是一个函数的话,就默认规定 x 是一个 Promise 类型;若 then 属性不是一个函数的话,就是一个普通对象,直接调用 promise2resolve 方法并将值传入。

  5. 通过 then.call(x) 是为了不用再次取 then 的值,防止取不到 then。并且将 y 作为成功回调的参数,r 作为 失败回调的参数。下一层可以获取到 promise2返回成功的 y和失败的r。只需要状态成功时调用 promise2resolve并将 y传入;状态失败时调用 promise2reject 并将 r传入。

  6. 注意 y 有可能还是一个 Promise类型,此时需要递归调用 resolvePromise 方法直到 y 是一个普通值。

核心方法 - resolvePromise

  • 判断对象是否是 Promise

  • 完整版 Promise

测试Promise是否符合标准

  • 安装测试Promise依赖包

    npm install promises-aplus-tests -g
    
  • 编写 - 延迟对象

  • 执行验证

    promises-aplus-tests promise.js
    

扩展

Promise.resolve

Promise.resolve 的作用:返回一个 fulfilledPromise 实例,或原始 Promise 实例。

在实现该功能前,我们需要了解 Promise.resolve 方法中参数的情况:

  1. 若参数为空,则返回一个状态为 fulfilledPromise 实例
  2. 若参数为 Promise 实例,则返回这个实例,不做修改
  3. 若参数为普通值,则返回一个状态为 fulfilledPromise 实例并且 fulfilled 响应函数会得到这个参数
  4. 若参数为 thenable 对象, 立即执行它的 .then() 方法

注意:对象里含有 then 方法就叫 thenable 对象。

resolve 的实现

接下来,我们给不同的参数的例子来验证写的 Promise.resolve 是否符合预期。

Promise.reject

reject:返回一个状态为 rejectedPromise 实例

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常见的题

题目一

输出一下结果?

详细的分析一下这题的解题思路,首先我们要知道宏任务微任务任务队列等概念:

  1. 由于``JS执行机制,代码从上往下执行,先执行完同步任务再执行异步任务,将异步任务都存放对应的**任务队列**中。故,一轮输出:1 8`。下面分析下执行异步时的情况:
  2. 当代码执行到 setTimeout 时,由于setTimeout宏任务。故将 2 存放在宏任务队列中等待执行。Promise 相关等方法属于微任务,将值依次存放在微任务队列中等待执行。
  3. 当代码执行到 new Promise调用 .then 方法时,由于回调函数中执行了 resolve 方法,代码会走成功的回调,并且 resolve 函数没有参数,所以成功的回调参数 data 没有值。故,输出:3undefined
  4. 当代码执行到 Promise.resolve 时,直接走成功的回调,并且 resolve 中有参数,所以将这个参数作为成功的回调参数的 data。故,输出 65
  5. 当第二轮微任务队列走完后,再走宏任务队列,故,第三轮输出:2

注意:做此题的技巧:① 先走完同步、② 再走异步,其中异步方法中先走微任务再走宏任务

题目二

输出一下结果?

详细的分析一下这题的解题思路:

  1. 相信有很多小伙伴由第一道题的经验可知:第一轮输出 17这样输出就错了!相信如果实现上方小节 Promise 源码小节都知道,Promise 参数里的执行器是默认立即执行的。故,第一轮输出: 137。下面分析下执行异步时的情况:
  2. 同理,2 存放到宏任务队列中。由于执行了 resolve(4),那么第一个 .then方法的成功回调方法中的打印值会存放在微任务队列中等待执行。此时,要注意:第二个 .then方法的成功回调不会执行,一直在等待状态。故,第二轮输出:54
  3. 当第一个 .then 方法执行完后,再执行第二个 .then 方法,并且将成功回调方法中的打印值会存放在微任务队列中等待执行。由于第一个 then 方法执行没有 return ,所以第二个 then 参数 dataundefined。故,第三轮输出:6undefined
  4. 当第三轮微任务队列走完后,再走宏任务队列,故,第四轮输出:2

注意:做此题的技巧:同上题技巧。但要明白 Promise 的特点。

题目三

输出一下结果?

详细的分析一下这题的解题思路:

  1. 由第二题可知,故,第一轮输出:13911。下面分析下执行异步时的情况:
  2. 同理,2 存放到宏任务队列中。第一个 Promise 执行 resolve方法,所以先执行第一个 Promise中的第一个 then方法。45 被存放在微任务队列中。但又 new Promise 并且执行 resolve 方法。注意:此时,第二个 resolve 执行时,67的代码还没有存放在微任务队列中。第一个Promise 中的第二个 then 方法中的 8也没有存放在微任务队列中。代码执行到三个 new Promise 中调用 resolve方法,所以 10 被存在微任务队列中。故,第二轮输出:4510
  3. 执行上述分析的微任务队列中的 10,当走第二个 Promiseresolve 方法时,6 被存放在微任务队列中,7 没有存放在微任务队列中。此时,8被存放在微任务队列中。故,第三轮输出:1068
  4. 最后执行第二个 Promise 的 第二个 then 方法。故,第四轮输出:7
  5. 当第四轮微任务队列走完后,再走宏任务队列,故,第四轮输出:2

注意:做此题的技巧:同上题技巧。但要知道Promise的状态是否是成功或者失败状态回调函数是否执行。

通过以上三道有关 Promise 的题,当你完全掌握了 Promise同步异步宏任务微任务任务队列JS执行机制等相关知识点后,这种题都是可以用眼睛就能做出来。