Promise
Senior JavaScript Promise
Published: 2020-03-20

The article contains the knowledge of Promise…

实例对象与函数对象

总结:括号左边是函数,点左边是对象

回调函数的两种类型

有的回调函数是同步的,有的是异步的

同步回调

显然上图forEach里面的回调函数是同步函数,因为只有当forEach执行完毕之后才打印下面的log

异步回调

JS的error处理

常见的内置错误

ReferenceError

当我们引用一个不存在的变量的时候会发生:

TypeError

数据类型不正确的错误

RangeError

SyntaxError

错误处理

上图message和stack是这个错误对象里面的属性,上面没有try catch的时候浏览器输出的其实是stack里面的信息

利用try catch throw输出偶数的案例

Promise的理解和使用

什么是Promise

  1. 抽象表达

    Promise是JS中进行异步编程的新的解决方案,旧的是纯回调函数

  2. 具体表达

    从语法上来说,Promise是一个构造函数;

    从功能上来说,Promise对象用来封装一个异步操作并可以获取其结果

Promise的状态改变

promise有三种状态,分别是pending、resolved和rejected,pending表示未知的、未确定的状态,另外两个就不解释了

要注意的是,只能由pending变成其他的两种状态,不能倒着变。另外每一个promise对象只能改变一次。最后,成功的结果返回value,失败的结果返回reason

Promise的基本流程

注意,我执行resolve(),但是我的状态是resolved状态;我执行reject(),但是我的状态是rejectd状态

一旦确定了是什么状态之后,就会去调用成功或者失败的回调函数,那么这个回调函数是怎么指定的呢?可以用then()也可以用catch(),其中then()可以指定成功的也可以指定失败的,而catch()只能指定失败的。最后返回一个新的Promise对象

Promise的基本使用

为什么要用Promise

原来的纯回调函数:

在真正执行异步任务之前需要指定回调函数,他是先指定回调函数,后面再执行异步任务,而且还必须是这样,我们不能在他执行完之后再去调用回调函数,他都完成了,再执行回调函数得不到数据了,因此必须在他真正执行异步任务之前就交给他回调函数,这是原来的纯回调实现异步的做法。

下面来看看Promise:

Promise执行器函数是同步回调的,当Promise对象产生的时候异步任务已经启动了,换句话说Promise在异步任务启动之后指定回调函数(在异步任务启动之前那肯定是可以的),甚至能在任务完成之后指定回调Promise都能得到结果,比如我有一个异步任务要两秒钟,但是回调函数要3秒钟,Promise依然可以在该回调函数中得到异步任务的结果,我们来做个实验:

上图表示我异步任务1秒之后执行,回调函数2秒之后执行,这意味着我先有了结果,过了一段时间之后再指定的回调函数

结果发现回调函数竟然可以得到数据

回调地狱

回调函数嵌套调用,第二个函数以第一个函数的结果为条件,第三个函数以第二个函数的结果为条件,以此类推。回调函数不便于阅读,不便于异常处理。

比方说上图的三个嵌套的回调函数,第一个的返回结果作为第二个的条件,第二个再去创建新的回调函数,第二个的结果再作为第三个的条件,第三个再去创建回调函数来接收最终的结果finalResult

可以看到她非常难以阅读而且异常处理也不方便,而且如果一旦里面有一个出现异常,也是很费劲的一件事

上面展示了纯回调的时候如何去编写串联的异步(串联的异步意思就是第一个搞完才搞第二个,第二个搞完才搞第三个)

使用Promise去编写回调地狱

注意,一旦得到一个Promise对象,说明我启动了一个异步任务

那么doSomething()就会生成一个Promise对象,就会启动一个异步任务

上图的完整流程:

  1. doSomething()开启一个异步任务;

  2. 执行内部成功的回调函数,并用result来接受成功的回调结果;

  3. doSomethingElse(result)开启另一个异步任务;

  4. 执行第一个then中内部的成功的回调函数,并用newResult来接受成功的回调结果;

  5. doThirdThing(newResult)开启另一个异步任务;

  6. 执行第二个then中内部的成功的回调函数,并用finalResult来接受成功的回调结果;

  7. 执行console.log();

  8. 如果出现错误就跳到最后的.catch调用里面的错误的回调函数,而不会每一个函数都写一个错误回调函数

这种编码方式是从上往下编写的,看着就是同步的编码方式。

函数之间不是嵌套的关系,也就没有嵌套地狱的问题

但是Promise是解决回调地狱最优化的解决方案嘛?

不是。因为他还有一个回调函数function(){}嵌在里面

那怎么解决或者说干掉会点函数呢?

用async/await

async/await

我们发现用了async/await之后就没有回调函数了

上图从编码方式上来说,是纯粹的同步的编码方式

而Promise还不能称为纯粹的同步的编码方式

纯粹的同步的编码方式

没有回调函数,就像上面的async/await就是纯粹的同步的编码方式

结论

1、

Promise更加灵活,纯回调函数需要在异步任务之前指定回调函数,而Promise可以在异步任务之前、之后甚至完成之后指定回调函数

2、

Promise支持链式调用,可以解决回调地狱问题

如何使用Promise

这里的Promise是实例对象,记住函数对象就一个,而实例对象可以有多个,函数对象不会启动异步操作,实例对象才会启动异步操作

Promise构造函数

里面的执行器executor是同步执行的,他会在Promise内部立即同步回调,而异步操作会在执行器中执行

原型方法then

返回一个新的promise对象,这点很关键

原型方法catch

catch只接受失败的回调,相当于:

then(undefined, onRejected)

Promise.resolve方法

这是一个创建成功的Promise对象的简洁语法

Promise.reject方法

这是一个创建失败的Promise对象的简洁语法

Promise.all方法

列表中所有的都成功了才会执行成功的回调函数,只要有一个失败了就会执行失败的回调函数,并且返回的reason值是那个失败的回调函数传过来的值

比方说这里的p3是失败的,那么只会输出reason,而且值是p3所指定的reject()括号中的值

之后我们把p3去掉,p1、p2是成功的,那么返回值是value,而且是一个数组,里面的元素是p1、p2指定的resolve()括号中的值。

而且这个value数组里面的元素顺序就是Promise.all()括号里面的参数的顺序,像上面这个他的顺序就是p1、p2,要是没顺序就乱套了,不知道哪个是哪个(注意就算p1设置了延迟,顺序还是p1、p2)

Promise.race方法

返回第一个完成的Promise,成功还是失败不管,反正谁第一个完成我就返回谁

在没有设置延迟的时候,第一个参数永远都是最先完成的,也就是上图的p1一定是先完成的(谁放第一个谁先完成)

当我们给p1设置延迟而p2、p3没有设置延迟的时候,p1后面那个会最先完成,也就是上图的p2

例子

基本语法

此时执行的是成功回调,只会输出value

此时执行的是失败的回调,会输出reason

产生一个成功值为1的promise对象

我们对比一下p1和p2的写法,p1是中规中矩的,p2其实是简洁语法,是语法糖

注意p3可以写then也可以写catch

Promise的几个关键问题

改变状态函数、.then函数、回调函数是同步执行还是异步执行

((resolve,reject) => {})这个代码块里的是同步执行的,也就是上图的resolve(1)是同步执行的;

.then也是同步执行的(他都 .then 调用了肯定是同步);

而回调函数是异步执行的

证明回调函数是异步的

在最下方写一句代码,如果是异步的则会在同步方法之后执行,也就是会先执行log代码,后执行回调函数

证明了它是异步的

也就是说回调函数不管是成功还是失败状态都不是马上执行的,都要等同步执行完了之后再执行

如何改变promise的状态

resolve和reject就不说了,关键是如果我们抛出一个异常,promise的状态会转变为什么?

上面报错的原因是因为没有catch异常

我们发现他也是rejected状态

而且reason的内容就是抛出的error

那如果我们抛一个3呢?

输出果然就是3

一个promise指定多个成功/失败回调函数,都会调用嘛?

同时调用两次失败的监听

他会输出两次

其实这两个失败结果监听的调用里面还可以分别做不同的事情,同理成功的结果监听

改变promise状态和指定回调函数谁先谁后?

Promise的成功和失败函数是异步执行的

一般情况下会先指定回调函数,后改状态(而且改状态的同时会指定数据),因为一般情况下都是异步执行任务的,这个resolve()会在一段时间后再执行。那么连状态都还没有改变,他是怎么知道要指定哪个回调函数的呢?其实它是先保存当前指定的回调函数的,只有先保存了后面才有机会去调用这个回调函数

先改状态后执行回调

一般情况不会这么搞,但是要知道我们是有能力改它们的顺序的

最简单的方法就是把setTimeout拿掉,这个时候代码从上到下走到.then那里就已经有resolve()执行后的数据了,所以会先改变状态(同时指定了数据),之后异步调用函数的时候就不必再保存指定的回调函数了,因为当时已经拿到了状态,这时候应该直接指定回调函数并异步执行回调函数

第二种方法比较笨,但是有效:

给.then也设置定时器,而且时间要更久

promise.then()返回的新promise的结果状态由什么决定?

他的第二个输出竟然是undefined

首先说结论:

新的promise的结果状态由上一个.then的回调函数的执行结果决定。

再来理一遍上图的执行顺序:

  1. resolve(1)执行;

  2. 第一个.then中执行成功的回调函数,但是只做了log而没有return(就相当于return了一个undefined);

  3. 第二个.then中的回调函数根据第一个.then的回调函数的执行结果的返回(也就是第一个.then return的值,这里显然没有return所以是undefined)来执行自己的成功或者失败的回调函数,由于上一个.then返回为undefined,而且也没有抛异常,是正常执行,所以此时的.then的value接收到的数据是undefined,所以他会输出undefined

当我们给他写了return返回值之后,第二个.then就能正常返回了

当然还有可能返回其他的东西

这里返回了Promise成功的值3,那他返回的就是成功的3

同理返回失败的,那.then就是执行失败的回调

同理抛出异常

promise如何串联多个操作任务?

很简单,就是.then的链式调用

当然,这些任务可能是同步的也可能是异步的

我们先看上图,先执行了一个异步任务,然后拿一个.then去接,之后再执行一个任务,再拿.then去接,然后再执行一个延时任务,再拿一个.then去接

这里其实有问题

  1. 我们用的resolve()都是第一行的Promise对象的resolve,按道理他只能在第一个异步任务(执行器函数)里面去用,从第一个.then开始我们就不能再去用它了;

  2. 为了解决1的问题,我们把第一个.then之后的resolve()都改成return,但是问题还是没有解决,而且这是错误的解决方法,那是因为我们当时说了.then后面的回调函数如果是异步的应该返回一个新的Promise对象,用Promise对象把异步任务包在里面返回(当时说的是每一个新的Promise对象都能启动一个异步任务),这里第一个.then里面的回调函数执行的由于是同步的任务,所以没关系,但是第二个.then的回调里面执行的是异步,而且她显然没有用新的Promise对象包裹返回

那应该怎么做呢?

return一个新的Promise对象并在里面设置这个异步任务就好

现在我return了一个成功的Promise(该Promise返回的值就是他所在的那一层.then所返回的值),而且她会在一秒之后才会成功,光有回调函数没有结果是没有用的,所以下面的.then会一直等,到1秒之后输出3

结果:

注意任务二虽然是同步的,但也要等到任务一结束之后才会执行,因为第二次执行是以第一次执行的结果为条件的

总结

串联的时候如果是同步任务直接return,如果是异步则需要在外层包裹一个新的Promise,在里面做新的异步操作(Promise是用来封装异步操作的

promise异常穿透

上图这个段代码中间的.then都不会执行,会执行最后的.catch,那他是直接执行了.catch嘛?不是的,而且我们看.catch所对应的Promise应该是最后一个.then里面的Promise,而不是第一个Promise,而且前面几个.then也没写错误处理代码。那他是怎么从第一个Promise传到最后的.catch的呢?

其实前面几个.then没写错误处理就相当于写了抛出错误的代码:

每一级都这样抛到最后才能到.catch

当然,除了throw error,我们还可以返回一个Promise的reject,可以起到同样的效果

除了程序本身出错以外的两种触发错误回调的做法
  1. 抛出一个异常

  2. 返回一个Promise的reject

注意,直接写Promise.reject()还没用,因为他没有返回

一定要写return

中断promise链

我们知道then和catch都返回一个Promise对象,所以catch和then其实没差的,catch后面的then也会执行而不会在catch这里中断

那么我们如果想要中断要怎么做呢?

return new Promise(()=>{})

返回一个pending状态的Promise即可

自定义Promise

定义整体结构

/Promise/068cea3e1a986bc9e134cd02adf8b730.png

编写ES5结构、暴露Promise、编写Promise的构造函数

  1. 这个ES5结构叫IIFE

  2. 整段js代码是一个立即执行函数,并需要传入window

  3. 如何向外暴露Promise呢?直接给window添加属性Promise并指向我们自己定义的Promise即可

  4. 编写Promise的构造函数,注意里面传入的参数是一个executor,注意该执行器是同步执行的

原型对象的then()

原型对象catch

resolve

reject

all

race

正式开始编写

resolve()代表一个value的概念

reject()代表一个reason的概念

注意,由于是执行器,在构造器最后需要调用一下让他执行,而且他执行的函数就是在构造器里面定义的resolve()和reject()这两个函数,如上图。

Promise中的属性值与回调函数

他可能需要一个status状态值,需要一个data用于存放数据,一个callbacks数组用域存放回调函数

下面开始模拟回调函数

callbacks里面放的都是回调的对象,之前说的一般情况都是先指定回调函数,再改变Promise状态(reject和resolve),这种情况下callbacks应该是空数组;如果是先改变Promise状态,再指定回调函数的话,callbacks里面应该是会有回调对象的,这个时候就需要去异步执行这些回调对象了

这里我们先去判断callbacks是否有回调对象,有的话就去执行

这里用setTimeout来模拟异步回调,是不太正确的,后面会有正确方式

同理reject()

还有一个问题,我们讲每一个Promise状态只能改变一次,要么是从pending变为resolved,要么变为rejected,所以要在最前面加一个判断

还有一个问题,如果执行器出现异常Promise将变为rejected状态,直接给最后执行器的调用捕获一下异常就行了:

编写原型方法then

这个this就是Promise对象

至此,我们上面写的东西都通了,可以开始测试

只需要在最前面引入自己的Promise,后面的使用跟以前都是一样的

结果发现他并没有输出

调式:

我们发现this的类型是Promise,这是对的

问题出在resolve里面的this指向

改变resolve,reject中this的指向

由于我们是直接调用resolve的,所以谁调用this就指向谁,显然直接调用的话他就指向window了,而我们希望他指向Promise

解决方法:

之前讲过如何改变this指向,这里用一种最简单的:

然后下面的this全部改为self

注意,then里面的this是对的,不应该改为self,这里可以这么修改:

直接再声明一个const self指向this

至此,我们已经可以成功输出想要的内容了

调用reject发现也可以成功输出

现在我们可以更好地理解Promise的机制了

我们现在知道.then之后回调函数没有立即调用,而是存到一个数组里面了,真正调用是在reject或者resolve里面遍历回调对象数组的时候去调用的

验证回调函数是否是异步执行

上面讲到回调函数会在reject内部去执行,那么如果他是异步的,就会在上图log之后再执行

原因就是我们利用setTimeout模拟了异步

这里虽然没有设置延迟时间,但他也变成了异步

将经常要写的字符封装成常量

细分then(里面可能是pending可能是resolved可能是rejected)

如果是pending说明先执行了then,如果是resolved说明先执行了resolve(),这个时候就不是保存函数了,而是直接异步执行函数

注意是异步执行,所以还是需要加上setTimeout来模拟,而且如果是resolved说明resolve(value)已经执行,value已经存在Promise的data中了,我们要调用的话直接self.data就可以取到value

rejected跟resolved差不多

返回新的Promise对象(★)

首先我们知道返回的新的Promise对象应该根据这一层的then或者catch的onResolved或onRejected函数的执行状态来写

所以如果这么写的话显然是不能拿到onResolved或onRejected函数执行的结果的,应该把这两个函数的执行写到新的Promise对象里面去

处理resolved

resolved的情况下返回新的Promise对象无非也就三种情况(如上图):

  1. 当前执行的函数抛出异常,那么return的promise失败,那么我们就应该调用promise的reject;

  2. 当前回调函数返回不是promise,那么return的promise就是成功的,value就是这个返回值;

  3. 当前回调函数返回是Promise的实例对象,那么return的promise结果就是这个返回的Promise实例对象的结果

第一种情况最简单,直接try catch就能分辨是否是第一种

第二种情况可以用 instanceof 来判断

第三种情况最麻烦,首先需要得到返回的promise对象的执行结果(我们需要知道返回的promise对象到底调用的是resolve还是reject,此时可以用then去进行判断如果then里面执行的是onResolved函数,那自然就是成功的,反之就是失败的),如下图:

要注意返回的是Promise时候的写法

注意上图的try的外层包了一层new Promise(下图)!!!,所以这里的resolve和reject是新的promise对象的resolve和reject:

####### 这一层的then返回不是异常也不是非promise而是Promise实例对象时的简洁写法

事实上直接给这一层的then返回的promise(注意不是新的promise,这里简称旧的promise)的then传入新的promise的resolve和reject函数即可,如果旧的promise是失败的,新的promise由于将resolve和reject函数传入了旧的promise,也会执行失败的回调,同理成功的情况。

上面这种处理方法其实有点类似下面这种:

解释:我们给div设置click事件,在这个事件里面我们希望直接调用fn函数,那么可以像上面这样写,但是其实可以更简单:

我们直接把click事件指向fn函数就好了,而且这里fn函数不能加括号,加括号就立即执行了。

处理rejected

直接将处理resolved的代码复制粘贴过来,改一个地方就好

处理pending

我们发现pending这里只是push了两个和新的Promise豪不相关的函数,这样会导致一旦执行了这两个函数新的promise的状态将不知道如何改变(我们知道pending的情况是因为先执行了then中的保存回调函数这一步(就仅仅只是保存了一下回调函数没做别的操作),后执行了改变promise状态的函数,所以一旦是这种情况就会导致返回的新的promise不知道旧的promise到底是什么状态,也就会导致新的promise不清楚应该如何改变他自己的状态

那怎么解决呢?

我们应该把原先处理resolve、reject的函数除了开头的setTimeout之外全部照搬过来,如上图,为什么不需要setTimeout这一块呢?因为触发该函数调用的函数的外层已经有一个setTimeout了:

之后我们发现代码有重复,因此我们将他封装出来,由于执行的时候可能调用resolve也可能调用reject,所以应该把这个回调函数作为形参传入:

之后代码将变得非常简洁

而且不仅可以简化pending还能简化resolved和rejected的代码块

处理then不传onRejected的情况

then有可能不传入onRejected函数,这个时候我们需要给他抛出异常,我们可以像上图那样直接调用reject

,也可以抛出一个异常

onResolved也最好处理一下

注意,这里onResolved如果不是function的话会直接返回value或者返回Promise.resolve(value),而onRejected不是function的话会抛出reason或者返回Promise.reject(reason),两者是不一样的

编写catch

写完了then之后,catch就很简单了

由于上面处理了onResolved和onRejected没有传递的错误:

这里的undefined会由上面这个操作进行处理而不会出现错误(catch处理了失败但是没有处理成功,但是成功也不会在catch这里断掉,还是会向下传递,也就是说如果catch下面还有catch或者then的话他们是能够正常接收到成功的回调函数的返回值的)

阶段一测试

测试中断链

编写resolve和reject

编写race和all

all:

全部成功才返回,而且返回的是一个数组,只要有一个失败了就返回失败,而且返回的是第一个失败(因为Promise的状态只能改变一次)

race:

返回最快执行完毕的那个,不管成功还是失败

注意,讲道理他这个数组里面可以传入的不仅仅是Promise,还可以是非Promise

但是上面我们没有考虑这个事情,我们直接写了p.then

其实要改很简单

直接套一个Promise就行了(race和all都是这么改的)

编写resolveDelay和rejectDelay(自定义的工具方法,原先Promise没有这两个方法)

总结

1、

2、

当是pending状态的时候,由于要满足返回的Promise的状态由onResolved/onRejected执行结果决定的要求,所以外面要包一层函数,如上图,到执行then的时候还是会传过来onResolved或onRejected函数,而且也不会替换掉上图的这两个函数,而是会在上图的这两个函数内部去执行传过来的onResolved或onRejected函数。

3、

如果传过来的resolve或reject不是函数,那我就想办法让他是,而且是使用正确的方式让他们分别变成一个函数

自定义Promise的类版本

直接套进去就行了

原型方法就是实例方法;类方法就是类方法

async/await

async 右边放函数定义, await右边放一个Promise对象或者任何需要等待的值

我们发现async的函数就算是return 1,返回的也是一个Promise对象(说明如果返回的是非Promise,那他会在返回值外面包一层Promise),而且他的接收就跟上面Promise的接收逻辑是一模一样的

原先我们通过then去拿到Promise的返回结果,现在我们不想要通过then了,我们想要直接拿到他的结果值,那么可以用await,如上图。而且我们如果用了await,那么就必须用上async,但是如果是async则并不一定行需要使用await

当然,await右边不一定就是Promise对象,也可以直接写一个数值

而且也可以是一个函数(最终有一个返回值的函数),得到的结果就是表达式本身的结果

注意,await只能得到右侧表达式正确的时候的返回值,如果是失败的时候呢?比方说我们reject一个值,直接拿他是拿不到的,需要用try catch去拿,catch中的error值就是reject的值,如上图。

JS异步之宏队列与微队列

JS引擎堆内放对象,栈内执行函数,栈内有可能new了一个Promise启动了一个异步任务,如果是异步任务的话会被放到另外的队列里面去

这里有两个队列,一个宏队列,一个微队列

dom事件回调、ajax回调、定时器回调这些都会被放到宏队列中,而Promise回调、mutation回调会被放到微队列中,其中宏队列中的任务被称为宏任务,微队列中的任务被称为微任务

这个mutation事实上是一个监视器,它用于监视标签属性是否改变,若属性改变则会有一个回调函数,而这个回调函数会被放入微队列

这里有两个函数,我执行上面的setTimeout代码,JS引擎就会将函数和事件交给定时器管理模块去管理

(如何管理呢?到了事件就把回调函数塞到队列里面去)

所以本质上这个函数会被立马塞到宏队列里面

而这个函数也会被立马塞到微队列里面

理论上按照时间先后的话会是上面的先执行

但是是微队列中的先执行

再来执行上图函数

我们发现是微队列的优先级更高,而且每个队列里面的顺序是从上到下的

JS执行机制

JS是单线程执行的,也就是说他会先执行完栈里面的同步函数,再执行微队列中的异步函数,最后执行宏队列中的异步函数。并且这些异步队列中的函数也会被拿到栈里面去执行

宏队列与微队列的微妙执行逻辑

现在我们把Promise放到setTimeout里面

我们发现执行的顺序是这样的:

注意最后一条,每次取出第一个宏任务执行前,都要将所有的微任务一个一个先取出来执行

所以上面的执行顺序就可以解释了,是因为在执行完微任务之后开始执行宏任务,而第一个宏任务里面有一个微任务,这个微任务会被放到微队列中,等到当前宏任务执行完毕之后他还不会执行,但是到下一个宏任务开始的时候,所有微队列里面的微任务都会被先执行,执行完毕之后才会执行当前宏任务。

案例

要分析执行顺序,套路就是先把同步的都找出来,再一个一个看异步的

1、

2、★

以下说明中输出值就代表该输出值所在的那个函数

  1. 1进入宏队列

  2. 由于Promise中.then后面的才是异步的,其他的都是同步执行,所以2直接输出,2和5一样都是同步代码块里的东西,所以5也在2之后被输出,resolve()里面没写东西,所以没有输出;

  3. 到达第一个.then,3进入微队列;

  4. 由于第一个.then没有执行完,第二个.then无法确定状态,即当前是“pending”状态,所以不会执行这个.then里面的函数而是会将他们保存到callbacks数组中;

  5. 同步代码执行完毕,现在到微队列里面去找3并执行3,执行3之后4的状态立马由“pending”变为“resolved”,4进入微队列;

  6. 4执行;

  7. 宏队列中的1执行

所以最终输出结果为:2 5 3 4 1

3、

答案 : 374125

4、

答案: 172384650

注意点

  1. then后面的函数执行需要满足两个条件:then上面的函数状态的确定;then后面的回调函数需要指定;

  2. 什么情况下then后面的函数会被保存到callbacks而不会进入微队列?

    then上面的函数进入了微队列但还没有执行,这么时候then上面的函数的状态还未确定,是pending状态,因此then后面的函数不能准备执行,所以只能保存到callbacks数组而不会进入微队列