- 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();
#### 最后更多分享:**前端字节跳动真题解析**
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**
- ![](https://img-blog.csdnimg.cn/img_convert/223b8a7b4279184bf64cb5f59f132666.webp?x-oss-process=image/format,png)
mise`,然后它的状态也是根据之前的`Promise`以及`handler`的执行结果决定的。不过`finally handler`能做的事相比而言更有限。
function doStuff() {
loading.show();
最后更多分享:前端字节跳动真题解析
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
- [外链图片转存中…(img-jpkKZdmn-1714513511401)]