js-Promise

9次阅读

共计 6850 个字符,预计需要花费 18 分钟才能阅读完成。

一、为什么需要 Promise
Javascript 采用回调函数 (callback) 来处理异步编程。从同步编程到异步回调编程有一个适应的过程,但是如果出现多层回调嵌套,也就是我们常说的回调金字塔(Pyramid of Doom),绝对是一种糟糕的编程体验。于是便有了 Promises/A , Promises/A + 等规范,用于解决回调金字塔问题。
// 回调金字塔
asyncOperation((data) => {
// 处理 data
anotherAsync((data1) => {
// 处理 data1
yetAnotherAsync(() => {
// 处理完成
});
});
});

// 引入 Promise 之后
promiseSomething()
.then((data) => {
// 处理 data
return anotherAsync();
})
.then((data1) => {
// 处理 data1
return yetAnotherAsync();
})
.then(() => {
// 完成
});
什么是 Promise? 一个 Promise 对象代表一个目前还不可用,但是在未来的某个时间点可以被解析的值。Promise 表示一个异步操作的最终结果。
二、Promise/A+ 基本的规范

一个 Promise 可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)。
一个 Promise 的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换。
Promise 必须实现 then 方法(可以说,then 就是 promise 的核心),而且 then 必须返回一个 Promise,同一个 Promise 的 then 可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致。
then 方法接受两个参数,第一个参数是成功时的回调,在 Promise 由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在 Promise 由“等待”态转换到“拒绝”态时调用。同时,then 可以接受另一个 Promise 传入,也接受一个“类 then”的对象或方法,即 thenable 对象。ajax 就是一个 thenable 对象。

Promise 状态变化优点:有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
// 不友好的层层嵌套
loadImg(‘a.jpg’, function() {
loadImg(‘b.jpg’, function() {
loadImg(‘c.jpg’, function() {
console.log(‘all done!’);
});
});
});
缺点:Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
三、ES6 Promise 基本的 API

Promise.resolve() // 生成一个成功的 promise 对象
Promise.reject() // 生成错误的一个 promise 对象

Promise.prototype.then() // 核心部分
返回一个新的 Promise。

Promise.prototype.catch() // 异常捕获

Promise.all()

接收 promise 对象组成的数组作为参数(promise.all 方法的参数可以不是数组,但必须具有 Iterator 接口)。
当这个数组里的所有 promise 对象 全部变为 resolve 或遇到第一个 reject 状态的时候,它才会去调用 .then 方法。
传递给 Promise.all 的 promise 并不是一个个的顺序执行的,而是 同时开始、并行执行的。

Promise.race() // 最先执行的 promise 结果

只要有一个 Promise 对象进入 resolve 或者 reject 状态的话,就会调用后面的.then 方法。
如果有一个 Promise 对象执行完成了,后面的还会不会再继续执行了呢?在 ES6 Promises 规范中,也没有取消(中断)Promise 对象执行的概 念,我们必须要确保 Promise 最终进入 resolve or reject 状态之一。所以,后面的 Promise 对象还是会继续执行的。

四、ES6 Promise 基本用法

创建 Promise 对象。

new Promise(fn) 返回一个 Promise 对象
在 fn 中指定异步等处理。
处理结果正常的话,调用 resolve(处理结果值)。
处理结果错误的话,调用 reject(Error 对象)。

// 示例
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open(‘GET’, URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
// 运行示例
var URL = “http://baidu.com”;
getURL(URL)
.then(function onFulfilled(value){
console.log(value);
})
.catch(function onRejected(error){
console.error(error);
});
// 其实 .catch 只是 Promise.then(undefined, onRejected) 的别名而已,
// 如下代码也可以完 成同样的功能。
getURL(URL).then(onFulfilled, onRejected);

总结:用 new Promise 方法创建 promise 对象用 .then 或 .catch 添加 promise 对象的处理函数

Promise.prototype.then()

它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then 方法的第一个参数是 Resolved 状态的回调函数,第二个参数(可选)是 Rejected 状态的回调函数。
then 方法返回的是一个新的 Promise 实例。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法。

Promise.prototype.catch()
Promise.prototype.catch 方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
getAjax(‘url/info’).then(function(data) {
// …
}).catch(function(error) {
// 处理 ajax 和 前一个回调函数运行时发生的错误
console.log(‘ 发生错误!’, error);
});

总结:1. 上面代码中,getAjax 方法返回一个 Promise 对象,如果该对象状态变为 Resolved,则会调用 then 方法指定的回调函数;如果异步操作抛出错误,状态就会变为 Rejected,就会调用 catch 方法指定的回调函数,处理这个错误。另外,then 方法指定的回调函数,如果运行中抛出错误,也会被 catch 方法捕获。2.Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch 语句捕获。

有了 then 里面的第二个 onRejected 函数捕获错误,为什么还需要 catch?
function throwError(value) {// 抛出异常
throw new Error(value);
}
// <1> onRejected 不会被调用
function main1(onRejected) {
return Promise.resolve(1).then(throwError, onRejected);
}
// <2> 有异常发生时 onRejected 会被调用
function main2(onRejected) {
return Promise.resolve(1).then(throwError).catch(onRejected);
}
// 执行 main 函数
main1(function(){
console.log(“ 错误异常 ”);
}
// 执行 main2 函数
main2(function(){
console.log(“ 错误异常 ”);
}
/*Promise.prototype.catch 方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
一般来说,不要在 then 方法里面定义 Reject 状态的回调函数(即 then 的第二个参数),总是使用 catch 方法。
*/
Promise.resolve(1).then(throwError).then(null, onRejected);
在函数 main1 因为虽然我们在的第二个参数中指定了用来错误处理的函数,但实际上它却不能捕获第一个参数指定的函数 (本例为 throwError) 里面出现的错误。与此相对的是 main2 中的代码则遵循了 throwError → onRejected 的调用流程。这时候出现异常的话,在会被方法链中的下一个方法,即 .catch 所捕获,进行相应的错误处理。

总结:.then 方法中的 onRejected 参数所指定的回调函数,实际上针对的是其 Promise 对象或者之前的 Promise 对象,而不是针对方法里面指定的第一个参数,即 onFulfilled 所指向的对象,这也是 then 和 catch 表现不同的原因。

Promise.resolve()有时需要将现有对象转为 Promise 对象,Promise.resolve 方法就起到这个作用。该函数的参数四种情况:(1)参数是一个 Promise 实例, 那么 Promise.resolve 将不做任何操作,原封不动的将实例返回。(2)参数是一个 thenable 对象,会将其转为 Promise 对象,然后立即执行该对象的 then 方法。(3)参数不是具有 then 方法的对象,或根本就不是对象。比如说字符之类,则 Promise.resolve 方法返回一个新的 Promise 对象,并且状态 Resolved。(4)不带有任何参数,直接返回一个状态为 Resolved 的 Promise 对象。

使用 Promise.resolve()创建 Promise 对象
// 静态方法 Promise.resolve(value) 可以认为是 new Promise() 方法的快捷方式。
// 比如
Promise.resolve(1)
.then(function(value){
console.log(value);
});
// 可以认为是以下代码的语法糖。
new Promise(function(resolve){
resolve(1);
})
.then(function(value){
console.log(value);
});
// 控制台输出 1
注意:无论 Promise.resolve 的参数是什么,只要变成了 rejected, 或者 resolved。都会执行 then 里面的 resolve 函数。

将 thenable 对象转换为 promise 对象。什么是 thenable 对象?简单来说它就是一个非常类似 promise 的东西。thenable 指的是一个具有 .then 方法的对象。jQuery.ajax(), 这个对象具有 .then 方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});

Promise.reject()Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为 rejected。
Promise.reject(‘ 这是错误的信息 ’).then(function(){

},function(res){
console.log(res); // 这里是错误信息
});
// 注意:无论 Promise.reject 的参数是什么,只要变成了 rejected,或者 resolved。都会执行 then 里面的 reject 函数。

Promise.all()Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。如果传入的不是不是 Promise 对象就会调用 Promise.reslove()方法将其转换成 Promise 实例。
const p1 = Promise.resolve(3);
const p2 = Promise.reject(5);
Promise.all([true, p1, p2]).then((value) => {
console.log(‘ 成功了 ’ + value);
}, (value) => {
console.log(‘ 失败了 ’ + value);
});

// 错误了,5
// 如果是全部 resolved,返回的是一个数组[true,3,5]

Promise.race()Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p1 = new Promise((resolve, reject) => {
setTimeout(resolve, 50, ‘one’);
setTimeout(() => {
console.log(‘one’);
resolve(‘one’);
}, 4)
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(‘two’);
resolve(‘two’);
}, 10)
});
Promise.race([p1, p2]).then((value) => {
console.log(value);
});

// one one two 所以后面的 promise 对象肯定会继续执行
// 第一个 one 是在 p1 里面打印出来的

五、Promise 只能进行异步操作?
Promise 在规范上规定 Promise 只能使用异步调用方式。
// 可以看出 promise 是 一个异步函数
var promise = new Promise(function(resolve) {
console.log(“inner promise”); // 1
resolve(42);
});
promise.then(function(value) {
console.log(value); // 3
});
console.log(“outer promise”); // 2
why? 因为同步调用和异步调用同时存在容易导致一些混乱。举个类似的例子。
function onReady(fn) {
var readyState = document.readyState;
if (readyState === ‘interactive’ || readyState === ‘complete’) {
fn();
} else {
window.addEventListener(‘DOMContentLoaded’, fn);
}
}
onReady(function () {
console.log(‘DOM fully loaded and parsed’);
});
console.log(‘==Starting==’);
如上 js 函数会根据执行时 DOM 是否已经装载完毕来决定是对回调函数进行同步调用还是异步调用。因此,如果这段代码在源文件中出现的位置不同,在控制台上打印的 log 消息顺序也会不同。为了解决这个问题,我们可以选择统一使用异步调用的方式。
function onReadyPromise() {
return new Promise(function (resolve, reject) {
var readyState = document.readyState;
if (readyState === ‘interactive’ || readyState === ‘complete’) {
resolve();
} else {
window.addEventListener(‘DOMContentLoaded’, resolve);
}
});
}
onReadyPromise().then(function () {
console.log(‘DOM fully loaded and parsed’);
});
console.log(‘==Starting==’);

正文完
 0