本文学习资料等来源于阮一峰的《ECMAScript 6入门》网络版,链接地址:http://es6.ruanyifeng.com/#docs/promise
Promise 的含义
因为学习步骤本身就是跟随阮一峰的介绍过程,所以第一个标题索性也使用的原文的标题。
简单来说Promise对象其实就是处理异步操作的的一种方案。它本身可以理解成一个容器,将一个或多个promise对象放入一个容器中,再进行进一步的异步操作,等待有返回值时,再将结果或者异常抛出。原文中这个概念的的解释其实相当通俗易懂:
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
promise的基础用法
直观点理解promise,其实它本身的意义更多的就是个异步操作的合集,然后将异步操作的结果信息反馈出来。
promise实际上是有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),只有当异步操作有结果之后,才能使用这个结果去改变它的状态,其他任何方式都无法去强制改变promise的状态。也是“承诺”的意义所在。
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
最简单的示例,直接引用阮一峰的示例代码:
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve的作用:在异步操作成功后调用,直接将promise对象的状态从pending(进行中)改变为fulfilled(已成功),并将括号内的结果作为参数抛出。
reject的作用:在异步操作失败后调用,直接将promise对象的状态从pending(进行中)改变为rejected(已失败),并将括号内的结果作为参数抛出。
在promise实例生成后,即状态改变有返回值后,可以用then方法获取resolved的返回值,catch方法获取reject的返回值。
阮一峰的原文中有更加详细的描述,其实只需要通过then方法就可以指定获取resolved和reject的值,因为then可以接受两个回调函数,第一个必要的回调函数就是获取resolved的返回值的,第二个可选的回调函数就是指定获取reject返回值的。而原文中也明确建议使用catch的方式捕获reject的返回值,所以我直接将其整合写在一起,省略了其中的实验和对比过程,直接总结结果。
下面是示例代码
promise = new Promise(function(resolve, reject) {
let boo = true
if (boo ){
resolve(1);
} else {
reject(2);
}
}).then((res) => {
console.log(res)
}).catch((err) => {
console.log(err)
})
// 打印结果
1
因为boo为true,调用resolve方法,将promise状态改变为成功,随后结果1被抛出给then调用,被打印出来。
如果boo为false,则会调用reject方法,promise状态则会变为失败,结果2会被抛出,被catch捕捉到。
另外,catch不只是调用reject方法后才会执行,它本身的意义是promise状态为失败即调用,即使没有主动调用reject,当promise内部代码运行报错时,抛出的错误依旧会被catch捕捉到。示例如下:
new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
}).then((r) => {
console.log(r,'r')
}).catch((e) => {
console.log(e,'e')
})
// 打印结果
ReferenceError: x is not defined
at Promise.then (<anonymous>:3:13)
at new Promise (<anonymous>)
at <anonymous>:1:1 "e"
另外有两点值得注意,在原文中也有描述
- promise新建后就会立即执行。
- 调用resolve或reject并不会终结 Promise 的参数函数的执行。
promise在被创建出来后,可以把它整体看做一个版块,这个版块和外层代码是属于同一队列的,都是被同步调用的。这里直接引用阮一峰的示例。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
即是说,promise内部执行的代码,和最后打印“Hi”的代码是同一队列,同步操作的。而then里面的代码是被resolve调用后抛出的,属于异步代码,会被放在同步队列后执行。
调用resolve或reject并不会终结 Promise 的参数函数的执行。这两个改变状态的代码自身并不会像return一样终止代码的运行(所以大多数使用的时候会直接return掉),当然,抛出的代码依旧是属于异步,会在同步代码后执行。示例代码也可以直接看阮一峰的示例,很清晰明了。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
promise的特点
- 对象的状态不受外界影响。
- 一旦状态改变,就不会再变。
关于第一点,对象的状态不受外界影响的理解,我的理解是promise内部的所有操作,最终会根据状态抛出,哪怕过程中报错了,依旧不会影响promise对象以外的代码。普通的报错代码,在错误发生时,浏览器就会抛出异常,退出进程、终止脚本执行,而终止后续代码的读写,而promise不会,即使报错,它依旧会执行下去,最终将错误信息抛出,我们只需要捕捉错误信息就可以了。
注意: 实验证明,promise内部报错后,其后续代码同样会被终止,只是不会影响外部而已。
new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
console.log(666)
}).then((r) => {
console.log(r,'r')
}).catch((e) => {
console.log(e,'e')
})
// 打印结果
ReferenceError: x is not defined
at Promise.then (<anonymous>:3:13)
at new Promise (<anonymous>)
at <anonymous>:1:1 "e"
第二点,一旦状态改变,就不会再变的理解。我的理解是promise一旦抛出resolve(成功)或reject(失败),其状态就不会再次改变。即是说,promise本身这个容器可以理解为一次性的,当然它可能是链式的多层使用,但每次调用时,当前promise对象只有一个状态且只能改变一次。所以,我可以将其粗略理解为promise的一次性,以下是示例:
new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(1);
reject(2)
}).then((r) => {
console.log(r,'r')
}).catch((e) => {
console.log(e,'e')
})
// 打印结果
1 "r"
示例代码中,先调用了resolve,讲promise的状态转变成了成功,接着调用reject,试图promise状态改变为失败。各自抛出1、2方便区分,最终打印出来的是1,因为promise的状态先改变成了成功后,就不会再被改变了。
then方法
上文已经提到过then方法的功能,它主要是处理promise成功的回调函数。值得一提的是,then获取返回值后,还可以继续调用then。因为then方法返回的是一个新的promise对象,这个对象还可以继续调用新的then方法,这种做法就可以实现链式操作。
new Promise(function(resolve, reject) {
resolve(2);
}).then((r) => {
return (r * r);
}).then((n) => {
return (n * n);
}).then((x) => {
console.log(x)
})
// 打印结果
16
值得一提的是,这种链式写法,resolve只需要调用一次,它就已经将状态改变为成功了,后面的的then操作都是在resolve的基础上进行的(特别鸣谢:这里是来自小媳妇的讲解),后续的调用,希望将参数抛出的时候,直接使用return就可以了,再使用resolve反而会报错。
catch
同样提到过的东西,catch的主要作用是处理promise失败的回调函数,它需要注意的有两点,第一点:只有当状态为失败时,才会被catch捕获到,如果状态先已经变成成功,再调用失败,是无效的(前文提到过,promise的特点第一条)。这里直接使用阮一峰的示例就很一目了然。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
第二点:promise的catch是具有冒泡属性的,会一直向后传递,直到被捕获为止。即是说,一个promise构造函数,其实只需要写一个catch即可。
示例1:多个catch,被捕获时直接打出。
new Promise(function(resolve, reject) {
reject('失败');
}).catch((r) => {
console.log(r, 1);
}).catch((n) => {
console.log(n, 2);
}).catch((x) => {
console.log(x, 3);
})
// 打印结果
失败 1
示例2:一个catch,捕获第一个失败信息
new Promise(function(resolve, reject) {
reject('失败');
}).then((r) => {
console.log(r, 1);
}).then((n) => {
console.log(n, 2);
}).then((x) => {
console.log(x, 3);
}).catch((e) => {
console.log(e,'e')
})
// 打印结果
失败 e
示例3:一个catch,捕获中间的错误信息
new Promise(function(resolve, reject) {
resolve('成功');
}).then((r) => {
console.log(r, 1);
}).then((n) => {
return x;
}).then((x) => {
console.log(x, 3);
}).catch((e) => {
console.log(e,'e')
})
// 打印结果
成功 1
ReferenceError: x is not defined
at Promise.then.then (<anonymous>:6:2) "e"
finally
finally方法其实就是无视promise的状态,无论成功还是失败,都会调用它。
new Promise(function(resolve, reject) {
resolve('成功');
}).then((r) => {
console.log(r, 1);
}).catch((e) => {
console.log(e, 2)
}).finally((f) => {
console.log('我是finally')
})
// 打印结果
成功 1
我是finally
new Promise(function(resolve, reject) {
reject('失败');
}).then((r) => {
console.log(r, 1);
}).catch((e) => {
console.log(e, 2)
}).finally((f) => {
console.log('我是finally')
})
// 打印结果
失败 2
我是finally
all
promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
all方法的介绍直接使用阮一峰的原文介绍。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
Iterator接口本文暂不研究。
p的状态是根据p1,p2,p3来决定的。当三个参数状态都是成功的时候,p才会变成成功状态。当任意一个参数状态是失败的时候,则p的状态便是失败。
p状态变成成功时,p1、p2、p3的返回值会组成一个数组,传递给p的回调函数。
示例(代码测试环境为浏览器的Console区域)
var testList = [1+1,2+2,3+3];
Promise.all(testList).then((res) => {
console.log(res);
}).catch((err) => {
console.log(err);
})
// 打印结果
// 2
// 4
// 6
race
race方法,类似all方法,同样可以将多个promise对象包装成一个promise对象。它的作用在阮一峰的文章里的解释大致是:多个对象只要有其中一个率先改变promise的状态,即将该对象的返回值传递回来。
相当类似es6循环中的some,不过some循环是判断布尔值,只要某一个值为true则返回。而race方法是只要某一个对象改变了promise的状态,即返回。这里就不着重介绍了。
resolve/reject
前文已经使用过很多次,resolve方法主要是将promise的状态修改为成功,reject方法主要是将promise的状态修改为失败。它们的参数都会被抛给构造函数,分别由then和catch打印(二次重申,其实then都可以打印,但便于记忆和区分以及规范,then最好只处理成功回调,catch处理失败回调)。
值得注意的是,resolve和reject方法还具有个功能,就是可以将传入的对象转换为promise对象,同时promise的状态变为成功/失败。如果传入的对象是thenable对象(thenable对象指的是具有then方法的对象),转换后会立即执行该对象的then方法。
原文示例
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
另外一点需要注意的是resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
原文示例
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
原文解释
上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log(‘one’)则是立即执行,因此最先输出。
暂时粗略理解为:resolve的异步操作形成的队列优先级是高于setTimeOut异步操作的,会在同步队列执行完成后,优先执行resolve的异步操作。至于这个优先级是针对整个promise构造函数还是只针对resolve/reject,还需要进一步测试。
关于promise对象的学习暂时就告一段落。其实本身promise函数勉强会照猫画虎的使用,但经过这次学习,理解得更深入些。相信随着更频繁的使用,会更加得心应手。加油~
一个佛系的前端的自学笔记
2019.07.09