大厂面试题分享 面试题库
前端面试题库 (面试必备)
地址:前端面试题库
前言
之前我写过一篇文章,讨论了为什么async await
中的错误可以被try catch
,而setTimeout
等api不能,有小伙伴提出之前面试被面试官问过为什么Promise
的错误不能try catch
,为什么要这么设计。好吧,虽然Promise
这个话题大家都聊烂了,今天我们再来展开聊聊🤭。
什么是Promise
Promise
是一个用来代表异步操作结果的对象,我们可以通过观察者模式观察异步操作的结果。在其它语言里面,我们多多少少接触过future
,deferred
这些概念,Promise
其实就是Javascript
的类似实现。 根据MDN定义:
A Promise
is in one of these states:
- pending: initial state, neither fulfilled nor rejected.
- fulfilled: meaning that the operation was completed successfully.
- rejected: meaning that the operation failed.
一个fulfilled Promise
有一个fulfillment
值,而rejected Promise
则有一个rejection reason
。
为什么要引入Promise?
异步处理在我们日常开发中是很常见的场景,在Promise
出现之前,我们都是通过回调来处理异步代码的结果,但是出现了一些问题:
-
回调地狱
,在有多个异步逻辑存在依赖关系时,我们只能在回调里嵌套,这些深度嵌套的代码让代码难以阅读和维护,业界称之为回调地狱 - 回调也没用标准的方式来处理错误,大家都凭自己的喜好来处理错误,可能我们使用的库跟api都定义了一套处理错误的方式,那我们把多个库一起搭配使用时,就需要花额外的精力去把他们处理皮实
- 有时候我们需要对一个已经完成的逻辑注册回调。这也没有统一的标准,对于大部分代码,我们根本就不能对这些已经执行完的代码注册回调,有些会同步执行回调,有些会异步执行回调,我们根本不可能记住所有api的机制,要么每次使用时我们都要研究这个api的实现机制,要么我们可能就在写bug
- 而且,如果我们想对一个异步逻辑注册多个回调,这也要看api提供方支不支持
- 最重要的,如果有统一的方式来处理错误跟正确结果的话,我们就有可能实现一套通用的逻辑来简化代码复杂度,这种自己发挥的情况就很难
是的,Promise
的出现就是为了解决这所有的问题。
怎么创建Promise
Promise构造函数
Promise
有一个构造函数,接收一个函数作为参数,这个传入构造函数里的函数被称作executor
。 Promise
的构造函数会同步地调用executor
,executor
又接收resolve
函数跟reject
函数作为参数,然后我们就可以通过这两个函数俩决定当前Promise
的状态(resolve
进入fulfilled
或者reject
进入rejected
)。
我们在resolve Promise
时,可以直接给它一个值,或者给它另外一个Promise
,这样最终是fulfilled
还是rejected
将取决于我们给它的这个Promise
最后的状态。
假如我们现在有一个promise a
:
- 如果我们在
promise a
里面调用resolve
,传入了另一个promise b
,promise a
的状态将取决于promise b
的执行结果 - 如果我们直接传给
resolve
一个普通的值,则promise a
带着这个值进入fulfilled
状态 - 如果我们调用
reject
,则promise a
带着我们传给reject
的值进入rejected
状态
Promise
在一开始都是pending
状态,之后执行完逻辑之后变成settled(fulfilled或者rejected)
,settled
不能变成pending
,fulfilled
不能变成rejected
,rejected
也不能变成fulfilled
。总之一旦变成settled
状态,之后就不会再变了。
我们也不能直接拿到Promise
的状态,只能通过注册handler
的方式,Promise
会在恰当的时机调用这些handler
,JavaScript Promise
可以注册三种handler
:
-
then
当Promise
进入fulfilled
状态时会调用此函数 -
catch
当Promise
进入rejected
状态时会调用此函数 -
finally
当Promnise
进入settled
状态时会调用此函数(无论fulfilled
还是rejected
)
这三个handler
函数都会返回一个新的Promise
,这个新的Promise
跟前面的Promise
关联在一起,他的状态取决于前面Promise
状态以及当前handler
的执行情况。
我们先来看一段代码直观感受下:
maybeNum
函数返回了一个Promise
,Promise
里面我们调用了setTimeout
做了一些异步操作,以及一些console
打印。
出现的结果类似这样:
或者这样:
我们可以发现,除了setTimeout
里的部分,其它都是同步按顺序执行的,所以Promise
本身并没有做什么骚操作,它只是提供了一种观察异步逻辑的途径,而不是让我们的逻辑变成异步,比如在这里我们自己实现异步逻辑时还是要通过调用setTimeout
。
此外,我们还可以通过Promise.resolve
跟Promise.reject
来创建Promise
。
Promise.resolve
Promise.resolve(x)
等价于
如果我们传给它的参数是一个Promise
,(而不是thenable
,关于什么是thenable
我们稍后会讲)它会立即返回这个Promise
,否则它会创建一个新的Promise
,resolve
的结果为我们传给它的参数,如果参数是一个thenable
,那会视这个thenable
的情况而定,否则直接带着这个值进入fulfilled
状态。
这样我们就可以很轻松地把一个thenable
转换为一个原生的Promise
,而且更加方便的是如果有时候我们不确定我们接收到的对象是不是Promise,用它包裹一下就好了,这样我们拿到的肯定是一个Promise
。
Promise.reject
Promise.reject
等价于
也就是说,不管我们给它什么,它直接用它reject
,哪怕我们给的是一个Promise
。
Thenable
JavaScript Promise
的标准来自Promise/A+
,,所以JavaScript
的Promise
符合Promise/A+
标准,但是也增加了一些自己的特性,比如catch
跟finally
。(Promise/A+
只定义了then
)
在Promise/A+
里面有个thenable
的概念,跟Promise
有一丢丢区别:
- A “promise” is an object or function with a
then
method whose behavior conforms to [the Promises/A+ specification]. - A “thenable” is an object or function that defines a
then
method.
所以Promise
是thenable
,但是thenable
不一定是Promise
。之所以提到这个,是因为互操作性。Promise/A+
是标准,有不少实现,我们刚刚说过,我们在resolve
一个Promise
时,有两种可能性,Promise
实现需要知道我们给它的值是一个可以直接用的值还是thenable
。如果是一个带有thenable
方法的对象,就会调用它的thenable
方法来resolve
给当前Promise
。这听起来很挫,万一我们恰好有个对象,它就带thenable
方法,但是又跟Promise
没啥关系呢? 这已经是目前最好的方案了,在Promise
被添加进JavaScript
之前,就已经存在很多Promise
实现了,通过这种方式可以让多个Promise
实现互相兼容,否则的话,所有的Promise
实现都需要搞个flag
来表示它的Promise
是Promise
。
再具体谈谈使用Promise
刚刚的例子里,我们已经粗略了解了一下Promise
的创建使用,我们通过then``catch``finally
来“hook”进Promise
的fulfillment
,rejection
,completion
阶段。大部分情况下,我们还是使用其它api返回的Promise
,比如fetch
的返回结果,只有我们自己提供api时或者封装一些老的api时(比如包装xhr
),我们才会自己创建一个Promise
。所以我们现在来进一步了解一下Promise
的使用。
then
then
的使用很简单,
我们注册了一个fulfillment handler
,并且返回了一个新的Promise(p2)
。p2
是fulfilled
还是rejected
将取决于p1
的状态以及doSomethingWith
的执行结果。如果p1
变成了rejected
,我们注册的handler
不会被调用,p2
直接变成rejected
,rejection reason
就是p1
的rejection reason
。如果p1
是fulfilled
,那我们注册的handler
就会被调用了。根据handler
的执行情况,有这几种可能:
-
doSomethingWith
返回一个thenable
,p2
将会被resolve
到这个thenable
(取决于这个thenable
的执行情况,决定p2
是fulfilled
还是rejected
) - 如果返回了其它值,
p2
直接带着那个值进入fulfilled
状态 - 如果
doSomethingWith
中途出现throw
,p2
进入rejected
状态
这词儿怎么看着这么眼熟?没错我们刚刚介绍resolve
跟reject
时就是这么说的,这些是一样的行为,在我们的handler
里throw
跟调用reject
一个效果,return
跟resolve
一个效果。
而且我们知道了我们可以在then/catch/finally
里面返回Promise
来resolve
它们创建的Promise
,那我们就可以串联一些依赖其它异步操作结果且返回Promise
的api了。像这样:
其中任何一步出了差错都会调用catch
。
如果这些代码都改成回调的方式,就会形成回调地狱
,每一步都要判断错误,一层一层嵌套,大大增加了代码的复杂度,而Promise
的机制能够让代码扁平化,相比之下更容易理解。
catch
catch
的作用我们刚刚也讨论过了,它会注册一个函数在Promise
进入rejected
状态时调用,除了这个,其他行为可以说跟then一模一样。
这里我们在p1
上注册了一个rejection handler
,并返回了一个新的Promise p2
,p2
的状态将取决于p1
跟我们在这个catch
里面做的操作。如果p1
是fulfilled
,这边的handler
不会被调用,p2
就直接带着p1
的fulfillment value
进入fulfilled
状态,如果p1
进入rejected
状态了,这个handler
就会被调用。取决于我们的handler
做了什么:
-
doSomethingWith
返回一个thenable
,p2
将会被resolve
到这个thenable
- 如果返回了其它值,
p2
直接带着那个值进入fulfilled
状态 - 如果
doSomethingWith
中途出现throw
,p2
进入rejected
状态
没错,这个行为跟我们之前讲的then
的行为一模一样,有了这种一致性的保障,我们就不需要针对不同的机制记不同的规则了。
这边尤其需要注意的是,如果我们从catch handler
里面返回了一个non-thenable
,这个Promise
就会带着这个值进入fulfilled
状态。这将p1
的rejection
转换成了p2
的fulfillment
,这有点类似于try/catch
机制里的catch
,可以阻止错误继续向外传播。
这是有一个小问题的,如果我们把catch handler
放在错误的地方:
这种情况如果someOperation
失败了,reportError
会报告错误,但是catch handler
里什么都没返回,默认就返回了undefined
,这会导致后面的then
里面因为返回了undefined
的someProperty
而报错。
由于这时候的错误没有catch
来处理,JavaScript
引擎会报一个Unhandled rejection
。 所以如果我们确实需要在链式调用的中间插入catch handler
的话,我们一定要确保整个链路都有恰当的处理。
finally
我们已经知道,finally
方法有点像try/catch/finally
里面的finally
块,finally handler
到最后一定会被调用,不管当前Promise
是fulfilled
还是rejected
。它也会返回一个新的Promise
,然后它的状态也是根据之前的Promise
以及handler
的执行结果决定的。不过finally handler
能做的事相比而言更有限。
我们可以在做某件耗时操作时展示一个加载中的组件,然后在最后结束时把它隐藏。我在这里没有去处理finally handler
可能出现的错误,这样我代码的调用方既可以处理结果也可以处理错误,而我可以保证我打开的一些副作用被正确销毁(比如这里的隐藏loading)。
细心的同学可以发现,Promise
的三种handler
有点类似于传统的try/catch/finally
:
正常情况下,finally handler
不会影响它之前的Promise
传过来的结果,就像try/catch/finally
里面的finally
一样。除了返回的rejected
的thenable
,其他的值都会被忽略。也就是说,如果finally
里面产生了异常,或者返回的thenable
进入rejected
状态了,它会改变返回的Promise
的结果。所以它即使返回了一个新的值,最后调用方拿到的也是它之前的Promise
返回的值,但是它可以把fulfillment
变成rejection
,也可以延迟fulfillment
(毕竟返回一个thenable
的话,要等它执行完才行)。
简单来说就是,它就像finally
块一样,不能包含return
,它可以抛出异常,但是不能返回新的值。
这边我们可以看到最后返回的值并不是finally
里面返回的值,主要有两方面:
-
finally
主要用来做一些清理操作,如果需要返回值应该使用then
- 没有
return
的函数、只有return
的函数、以及return undefined
的函数,从语法上来说都是返回undefined
的函数,Promise
机制无法区分这个undefined
要不要替换最终返回的值
then其实有两个参数
我们目前为止看到的then
都是接受一个handler
,其实它可以接收两个参数,一个用于fulfillment
,一个用于rejection
。而且Promise.catch
等价于Promise.then(undefined,rejectionHadler)
。
这个跟
可不等价,前者两个handler
都注册在同一个Promise
上,而后者catch
注册在then
返回的Promnise
上,这意味着如果前者里只有p1
出错了才会被处理,而后者p1
出错,以及then
返回的Promise
出错都能被处理。
解答开头的问题
现在我们知道要提供Promise
给外部使用,Promise
设计成在外面是没有办法获取resolve
函数的,也就改变不了一个已有Promise
的状态,我们只能基于已有Promise
去生成新的Promise
。如果允许异常向外抛出,那我们该怎么恢复后续Promise
的执行?比如Promise a
出现异常了,异常向外抛出,外面是没办法改变Promise a
的数据的。设计成在Promise
里面发生任何错误时,都让当前Promise
进入rejected
状态,然后调用之后的catch handler
,catch handler
有能力返回新的Promise
,提供fallback
方案,可以大大简化这其中的复杂度。
工具方法
Promise
还提供了一些工具方法,我们可以使用它们来同时处理多个Promise
,例如Promise.all
,Promise.race
,Promise.allsettled
,Promise.any
,今天我就不一一介绍了,大家感兴趣的可以自行了解一下。
写在结尾
Promise
的出现,让我们:
-
Promise
提供了标准的方式来处理结果 -
Promise
的then
返回新的Promise
,可以多个串联,达到注册多个回调的效果 - 对于已经完成的异步操作,我们后来注册的
then
也能被调用 - 我们只能通过
executor
函数提供的两个函数来改变Promise
的状态,没有其他办法可以resolve
或者reject
Promise
,而且这两个方法也不存在于Promise
本身,所以我们可以把我们的Promise
对象给其他人去使用,比如我们提供给外部一个api,以Promise
返回,可以放心地让外部通过Promise
来观察最终的结果,他们也没办法来改变Promise
的状态。 - 可以实现统一的同时处理多个
Promise
的逻辑
而且,我在本文开头提到过,回调地狱有两个问题是:
- 向已经完成的操作添加回调并没有统一的标准
- 很难向某个操作添加多个回调
这些都被Promise
的标准解决了,标准确保了两件事:
-
handler
一定会被调用 - 调用是异步的
也就是说,如果我们获取到了其它api提供的Promise
,有了类似如下的代码:
标准确保了,执行结果是before
,然后是after
,最后是(在p1
变成fulfilled
状态或者已经变成fulfilled
状态时)in
。如果Promise
在经过一段时间之后才变成fulfilled
,这个handler
也会被往后调度。如果Promise
已经变成fulfilled
了,那fulfillment handler
会被立即调度(不是立即执行),调度指的是被加入微任务队列,确保这些handler
被异步调用大概是Promise
唯一让同步代码被异步调用的情形了。
Promise
推出也好多年了,我们日常开发中已经离不开它了,即使是async
await
背地里还是在跟它打交道,希望本文带给大家对Promise
更全面的认识,当然了,关于Promise
还有一些最佳实践跟反模式,由于篇幅的原因下次再见啦,Happy coding~