Blog
Blog copied to clipboard
抓重点理解Promise
前言
基本上面试问Promise首先会问你很简单的一句,“你了解过Promise吗”
这是时候只要你把我下面“是什么”的说明理解好了就足够了。但是假设面试官追问下去,你要对他的一些方法要有理解。
是什么
Promise是解决异步编程的一种方案。以前我们处理异步操作,一般都是通过回调函数来处理,典型的例子就好像使用setTimeout
一样,如果执行操作函数里面还有setTimeout
,一层一层往下,都有的话。那么代码看起来十分臃肿,不利于维护,也很容易写出bug。
而Promise的出现,能够让异步编程变得更加可观,把异步操作按照同步操作的流程表达出来,避免层层嵌套的回调函数。
Promise对象有三种状态,进行中pending
、完成成功fulfilled
、失败rejected
,顾名思义,表示这个异步操作是进行中还是成功还是失败了。
Promise的状态一旦确定了,就不会再更改了,这就是promise(承诺)的由来吧,承诺状态确定了就是确定了。
然而Promise还是有不足的地方:
- 如果没有执行捕获错误的函数(如下述说的catch,then的第二个参数),则Promise内部发生的错误(虽然会报错但)是无法传递到Promise外部代码上的,因此外部脚本并不会因为错误而导致不继续执行下去。
- 一旦新建了,就无法中断它的操作。不像
setTimeout
那样,我还可以使用clearTimeout
取消掉。
创建
promise是一种对象类型,即可通过new Promise()
来创建一个promise实例对象。
let promise1 = new Promise((resolve, reject) => {
if (xxx) {
resolve('异步操作完成');
} else {
reject('异步操作失败');
}
});
参数是一个函数,这个函数带有两个参数,两个参数都是函数类型,调用resolve
方法,表示异步操作完成成功了;调用reject
方法,表示异步操作失败了。
调用这两个方法,会返回一个新的Promise对象,而这个Promise对象的最终状态才是这个异步操作的最终状态。具体规则会在下面的resolve
和reject
章节说明
在创建Promise
对象时,参数的函数内的同步脚本会立即执行,如
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
实际上上述的console.log('Promise');
是同步脚本,resolve
了之后的then
方法是异步脚本。
要注意,执行了resolve
或reject
方法后,并不会终止异步函数往下执行
let promise1 = new Promise((resolve, reject) => {
if (xxx) {
resolve('异步操作完成');
console.log('keep on');
} else {
reject('异步操作失败');
}
});
假设上述例子条件判断为true
,则执行了resolve
之后,还会执行console.log
。
一般执行resolve
或reject
都是宣告异步操作这个函数本身完结了,其余还需要进一步的操作应该放在外部再处理(如下述说的在then
里处理),所以我们一般是使用return
宣告此部分动作完结
let promise1 = new Promise((resolve, reject) => {
if (xxx) {
return resolve('异步操作完成');
console.log('keep on');
} else {
return reject('异步操作失败');
}
});
这样就执行不到console.log
了。
常用方法
then
then是promise的实例方法,接受两个参数,类型是函数。
promise.propotype.then(fn1, fn2)
fn1
是表示该promise
状态为成功时,需要进行的下一步操作,即被resolve
了,fn1
函数带有一个参数,这个参数就是promise
里执行resolve(result)
传的参数result。
fn2
是可选的,表示该promise
状态为失败时,需要进行的下一步操作,即被reject
了,fn2
函数带有一个参数,这个参数就是promise
里执行reject(result)
传的参数result,或者脚本抛出的错误信息。
例子
let promiseExample = new Promise((resolve, reject) => {
if (xxx) {
return resolve('异步操作完成');
} else {
return reject('异步操作失败');
}
});
promiseExample.then((res) => {
console.log(res);
}, (e) => {
console.log(e);
});
上述例子如果xxx结果为true,则会输出“异步操作完成”,false的话输出“异步操作失败”。
then
方法放回的是一个新的Promise
实例,因此后面还可以接着then
,因此可以采用链式写法
promise.then().then().then()
如果返回的还是一个Promise
对象(即有异步操作),那么后面的then
是要等前面的异步操作完成了才会执行。
如果你跟上面那几句话有点混乱的话,这么理解下:
then里的方法就算你不显式写return,then方法本身也是会返回一个Promise实例;但是如果你在里面的方法里显式return了一个promise对象,那么之后的then方法执行要等上一个返回的异步操作完成才会触发
catch
Promise的实例方法。catch
方法实际上是then的一种特殊形式。用来捕获它前面的异步操作抛出的错误。即异步操作的失败时就可以调用该方法做进一步操作。
promise.then(null, fn2)
或
promise.then(undefined, fn2)
即无视了异步操作成功时的情况,执行失败时的情况。上述then中的例子可以改为以下形式:
let promiseExample = new Promise((resolve, reject) => {
if (xxx) {
return resolve('异步操作完成');
} else {
return reject('异步操作失败');
}
});
promiseExample.then(res => {
console.log(res);
}).catch(e => {
console.log(e);
});
相当于
promiseExample.then(res => {
console.log(res);
}).then(null, e => {
console.log(e);
});
但是我们要注意,虽然catch
方法和then
里的第二个参数方法都是用来对异步操作失败时做进一步处理,但是他们还是有区别的。前者是能把链式结构中的“上游”部分中只要有抛出错误,就能捕获到,执行catch
。而后者仅仅能捕获到它本次异步操作的失败。
promise.then().then(fn1, fn2).catch()
上述例子中,
catch
能把前面的promise操作以及前两个then操作中的错误给捕获到。而例子中的fn2函数,只能捕获到第一个then里执行错误的情况
因此我们建议,使用catch替换用then的第二个参数来捕获错误,而且书写格式也比较直观。
吃掉错误
如果不使用catch
捕获Promise
操作抛出的错误,那么就算报错了也不会影响到外部代码,不会退出进程、终止脚本。
const asyncDoing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为i没有声明
resolve(i === 5);
});
};
asyncDoing().then(function() {
console.log('have Done');
});
setTimeout(() => { console.log(111) }, 5000);
// 控制台最终输出
// Uncaught (in promise) ReferenceError: i is not defined
// 111
先是浏览器抛出i未定义的错误,后面就打印出111
我们之前的认识是,在同步脚本里,如果某个脚本出错了,会终止下面的脚本继续执行,但是这里并不会
catch
方法也是返回一个Promise
对象,如果后面接着一个then
方法,那么catch
前的Promise
对象如果没有抛出错误,那么会跳过catch
,然后执行then
,如果有错,那么先执行catch
再执行then
finally
Promise的实例方法。上面我们说到用then来处理异步成功,用catch处理失败,那么如果我们并不在乎他们是否成功失败,都要在之后执行某个操作,就可以是用final
方法了。用法跟上面的很相似。
all
类方法。当你有好几个异步操作,你想等他们都执行完了之后再执行某个操作,那么就要用到all
方法了。
Promise.all([promise1, promise2, promise3]).then(res).catch(e);
参数数组里面的每个元素都是Promise对象,如果原本不是的,会使用下面说到的Promise.resolve
进行转化。参数是的形式不局限于数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
返回的状态的情况:
- 如果全部promise都是成功的,那么就返回成功状态,执行
then
里的函数,res
是一个数组,里面的元素按照all
参数里的promise的顺序一一对应它们各自的resolve
值 - 如果有一个promise处理失败了,那么整个状态就是失败的。执行
catch
,e
为对应的错误信息。 - 如果参数里的
Promise
实例本身有使用catch
捕获错误,那么对于all
来讲,是返回了fullfilled
状态的
race
类方法。如有一批异步操作,你只想当它们之中有一个事先结束了就执行某个操作,就可以使用它。
Promise.trace([promise1, promise2, promise3])
参数跟all
方法一样。
如果promise2
是第一个异步执行完的,那么就以它的状态为最终状态。
resolve
类方法。执行该方法返回一个Promise
对象
Promise.resolve(param);
等价于
new Promise(resolve => resolve(param))
关于返回的Promise
的状态按照如下规则:
-
param
如果是一个promise
对象,则原封不动地原样返回这个promise
对象 - 如果还是一个
promise
对象,但是对它已经有了如then
或catch
的操作,上面我们也说过这两个方法还是可能会返回Promise
对象,所以会以这个最终状态的Promise
对象返回。其实也就是上一个规则的特性情况而已 - 如果除上述两个情况外的其他值(包括不传),那么会返回成功状态的
Promise
对象
以上规则同样是使用在Promise
对象中的resolve
方法
reject
类方法。执行该方法返回一个Promise
对象
Promise.reject(param);
关于返回的Promise
的状态规则很简单,与上述的resolve
是不同的
-
param
如果是Promise
对象,那么原样返回,尽管它可能本身还有then
或catch
操作,也不管。这就是不同之处 - 如果除上述两个情况外的其他值(包括不传),那么会返回失败状态的
Promise
对象
以上规则同样是使用在Promise
对象中的reject
方法
然而Promise还是有不足的地方:
如果没有执行捕获错误的函数(如下述说的catch,then的第二个参数),则异步操作里发生的错误是无法反馈到外部的,俗话说“吃掉了错误”,因此外部脚本并不会因为错误而导致不继续执行下去
不写catch同样会被外界捕获啊, 你说的这种情况能举个例子吗?
@rxdxxxx
然而Promise还是有不足的地方:
如果没有执行捕获错误的函数(如下述说的catch,then的第二个参数),则异步操作里发生的错误是无法反馈到外部的,俗话说“吃掉了错误”,因此外部脚本并不会因为错误而导致不继续执行下去
不写catch同样会被外界捕获啊, 你说的这种情况能举个例子吗?
我这边措辞严谨点表达,如果没有执行捕获错误的函数(如下述说的catch,then的第二个参数),则异步操作里发生的错误虽然会报错出来,但是错误不会传递到Promise外部代码,外部脚本并不会因为错误而导致退出进程、终止脚本执行(正常情况下你脚本报错了,下面的脚本并不会继续执行下去)
执行下面的例子可以看到
const doSomeThing= function() {
return new Promise(function(resolve, reject) {
// 因为i没有声明,下面一行会报错
resolve(i++);
});
};
doSomeThing().then(function() {
console.log('promise success');
});
setTimeout(() => { console.log('运行了timeout') }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 运行了timeout
尽管报错了x is not defined
,但还是会执行了定时器里的脚本