大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
前言
之前我写过一篇文章,讨论了为什么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
的执行情况。
我们先来看一段代码直观感受下:
function maybeNum() {
// create a promise
return new Promise((resolve, reject)=>{
console.info('Promise Start')
setTimeout(()=>{
try{
const num=Math.random();
const isLessThanHalf=num<=0.5;
if(isLessThanHalf){
resolve(num)
}else{
throw new Error('num is grater than 0.5')
}
}catch (e) {
reject(e)
}
},100)
console.info('Promise End')
})
}
maybeNum().then(value => {
console.info('fulfilled',value)
}).catch(error=>{
console.error('rejected',error)
}).finally(()=>{
console.info('finally')
})
console.info('End')
复制代码
maybeNum
函数返回了一个Promise
,Promise
里面我们调用了setTimeout
做了一些异步操作,以及一些console
打印。
出现的结果类似这样:
Promise Start
Promise End
End
fulfilled 0.438256424793777
finally
复制代码
或者这样:
Promise Start
Promise End
End
rejected Error: num is grater than 0.5 ...
finally
复制代码
我们可以发现,除了setTimeout
里的部分,其它都是同步按顺序执行的,所以Promise
本身并没有做什么骚操作,它只是提供了一种观察异步逻辑的途径,而不是让我们的逻辑变成异步,比如在这里我们自己实现异步逻辑时还是要通过调用setTimeout
。
此外,我们还可以通过Promise.resolve
跟Promise.reject
来创建Promise
。
Promise.resolve
Promise.resolve(x)
等价于
x instanceof Promise?x:new Promise(resolve=>resolve(x))
复制代码
如果我们传给它的参数是一个Promise
,(而不是thenable
,关于什么是thenable
我们稍后会讲)它会立即返回这个Promise
,否则它会创建一个新的Promise
,resolve
的结果为我们传给它的参数,如果参数是一个thenable
,那会视这个thenable
的情况而定,否则直接带着这个值进入fulfilled
状态。
这样我们就可以很轻松地把一个thenable
转换为一个原生的Promise
,而且更加方便的是如果有时候我们不确定我们接收到的对象是不是Promise,用它包裹一下就好了,这样我们拿到的肯定是一个Promise
。
Promise.reject
Promise.reject
等价于
new Promise((resolve,reject)=>reject(x))
复制代码
也就是说,不管我们给它什么,它直接用它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
的使用很简单,
const p2=p1.then(result=>doSomethingWith(result))
复制代码
我们注册了一个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了。像这样:
p1.then(result=>secondOperation(result))
.then(result=>thirdOperation(result))
.then(result=>fourthOperation(result))
.then(result=>fifthOperation(result))
.catch(error=>console.error(error))
复制代码
其中任何一步出了差错都会调用catch
。
如果这些代码都改成回调的方式,就会形成回调地狱
,每一步都要判断错误,一层一层嵌套,大大增加了代码的复杂度,而Promise
的机制能够让代码扁平化,相比之下更容易理解。
catch
catch
的作用我们刚刚也讨论过了,它会注册一个函数在Promise
进入rejected
状态时调用,除了这个,其他行为可以说跟then一模一样。
const p2=p1.catch(error=>doSomethingWith(error))
复制代码
这里我们在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()
.catch(error => {
reportError(error);
})
.then(result => {
console.log(result.someProperty);
});
复制代码
这种情况如果someOperation
失败了,reportError
会报告错误,但是catch handler
里什么都没返回,默认就返回了undefined
,这会导致后面的then
里面因为返回了undefined
的someProperty
而报错。
Uncaught (in promise) TypeError: Cannot read property 'someProperty' of undefined
复制代码
由于这时候的错误没有catch
来处理,JavaScript
引擎会报一个Unhandled rejection
。 所以如果我们确实需要在链式调用的中间插入catch handler
的话,我们一定要确保整个链路都有恰当的处理。
finally
我们已经知道,finally
方法有点像try/catch/finally
里面的finally
块,finally handler
到最后一定会被调用,不管当前Promise
是fulfilled
还是rejected
。它也会返回一个新的Promise
,然后它的状态也是根据之前的Promise
以及handler
的执行结果决定的。不过finally handler
能做的事相比而言更有限。
function doStuff() {
loading.show();
return getSomething()
.then(result => render(result.stuff))
.finally(() => loading.hide());
}
复制代码
我们可以在做某件耗时操作时展示一个加载中的组件,然后在最后结束时把它隐藏。我在这里没有去处理finally handler
可能出现的错误,这样我代码的调用方既可以处理结果也可以处理错误,而我可以保证我打开的一些副作用被正确销毁(比如这里的隐藏loading)。
细心的同学可以发现,Promise
的三种handler
有点类似于传统的try/catch/finally
:
try{
// xxx
}catch (e) {
// xxx
}finally {
}
复制代码
正常情况下,finally handler
不会影响它之前的Promise
传过来的结果,就像try/catch/finally
里面的finally
一样。除了返回的rejected
的thenable
,其他的值都会被忽略。也就是说,如果finally
里面产生了异常,或者返回的thenable
进入rejected
状态了,它会改变返回的Promise
的结果。所以它即使返回了一个新的值,最后调用方拿到的也是它之前的Promise
返回的值,但是它可以把fulfillment
变成rejection
,也可以延迟fulfillment
(毕竟返回一个thenable
的话,要等它执行完才行)。
简单来说就是,它就像finally
块一样,不能包含return
,它可以抛出异常,但是不能返回新的值。
function returnWithDelay(value, delay = 10) {
return new Promise(resolve => setTimeout(resolve, delay, value));
}
// The function doing the work
function work() {
return returnWithDelay("original value")
.finally(() => {
return "value from finally";
});
}
work()
.then(value => {
console.log("value = " + value); // "value = original value"
});
复制代码
这边我们可以看到最后返回的值并不是finally
里面返回的值,主要有两方面:
finally
主要用来做一些清理操作,如果需要返回值应该使用then
- 没有
return
的函数、只有return
的函数、以及return undefined
的函数,从语法上来说都是返回undefined
的函数,Promise
机制无法区分这个undefined
要不要替换最终返回的值
then其实有两个参数
我们目前为止看到的then
都是接受一个handler
,其实它可以接收两个参数,一个用于fulfillment
,一个用于rejection
。而且Promise.catch
等价于Promise.then(undefined,rejectionHadler)
。
p1.then(result=>{
},error=>{
})
复制代码
这个跟
p1.then(result=>{
}).catch(error=>{
})
复制代码
可不等价,前者两个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
,有了类似如下的代码:
console.log('before')
p1.then(()=>{
console.log('in')
})
console.log('after')
复制代码
标准确保了,执行结果是before
,然后是after
,最后是(在p1
变成fulfilled
状态或者已经变成fulfilled
状态时)in
。如果Promise
在经过一段时间之后才变成fulfilled
,这个handler
也会被往后调度。如果Promise
已经变成fulfilled
了,那fulfillment handler
会被立即调度(不是立即执行),调度指的是被加入微任务队列,确保这些handler
被异步调用大概是Promise
唯一让同步代码被异步调用的情形了。
Promise
推出也好多年了,我们日常开发中已经离不开它了,即使是async
await
背地里还是在跟它打交道,希望本文带给大家对Promise
更全面的认识,当然了,关于Promise
还有一些最佳实践跟反模式,由于篇幅的原因下次再见啦,Happy coding~
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库