为什么要使用Promise
Promise
是一个代表了异步操作最终完成或者失败的对象
- 主要解决回调地狱
先看一个对比例子:
//回调地狱代码:
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
//promise方案:
doSomething()
.then(result => doSomethingElse(value))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);
//async/await方案:
async function foo() {
try {
let result = await doSomething();
let newResult = await doSomethingElse(result);
let finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}
- 有助于以一种有效的方式对异步进行分组
- Promises/A+规范早已经有其他语言实现
- 链式调用
回调和promise方式区别
- 回调需要立即调用方法,而Promise可以作为Promise对象返回并在以后调用。
- 在Callback中,我们必须显式地处理错误
- 在Callback中,我们必须调用传递的回调函数来传递错误
第三方实现的Promise库
什么是Promise
Promise
对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象
Promise状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
状态变化示意图
Promise对象有以下两个特点
-
对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
-
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
如果 Promise 状态已经变成resolved,再抛出错误是无效的。
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 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
语法
new Promise( function(resolve, reject) {...} /* executor */ );
参数
executor
executor是带有 resolve 和 reject 两个参数的函数 。Promise构造函数执行时立即调用executor 函数, resolve 和 reject 两个函数作为参数传递给executor(executor 函数在Promise构造函数返回新建对象前被调用)。resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦完成,可以调用resolve函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。
如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。
方法
Promise.all(iterable)
Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。
Promise.race(iterable)
当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。
Promise.reject(reason)
返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法
Promise.resolve(value)
返回一个状态由给定value决定的Promise对象。如果该值是一个Promise对象,则直接返回该对象;如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。
Promise 原型上的方法
Promise.prototype.catch(onRejected)
添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise。当这个回调函数被调用,新 promise 将以它的返回值来resolve,否则如果当前promise 进入fulfilled状态,则以当前promise的完成结果作为新promise的完成结果.
Promise.prototype.then(onFulfilled, onRejected)
添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来resolve.
Promise.prototype.finally(onFinally)
chrome 63+
添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。回调会在当前promise运行完毕后被调用,无论当前promise的状态是完成(fulfilled)还是失败(rejected)
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
手动实现finally方法
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
从上面的实现还可以看到,finally方法总是会返回原来的值。
如何处理错误
处理链式调用的错误有两种方式
'use strict';
// 第一种方式
yourPromise.catch(function (error) {
// Your Error Callback
});
// 第二种方式
yourPromise.then(undefined, function (error) {
// Your Error Callback
});
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
//等价于
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
如果方法体中出现语法错误
或者调用Promise.reject(Err)
,程序会延调用链向下找到 .catch
或者then
方法中onRejection callback
。执行错误处理。
错误处理顺序
错误顺序取决于.catch
或者then
方法中onRejection callback
的定义顺序。
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
以上代码的执行流程如图:
Promise 会吃掉错误
先看一个例子:
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
上面代码中,someAsyncThing函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
这个脚本放在服务器执行,退出码就是0(即表示执行成功)。不过,Node 有一个unhandledRejection事件,专门监听未捕获的reject错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。
process.on('unhandledRejection', function (err, p) {
throw err;
});
上面代码中,unhandledRejection事件的监听函数有两个参数,第一个是错误对象,第二个是报错的 Promise 实例,它可以用来了解发生错误的环境信息。
再看下面的例子。
const promise = new Promise(function (resolve, reject) {
resolve('ok');
setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test
上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。
如何将多个promise组合成一个
通过Promise.all
方法将多个promise组合成一个
'use strict';
// Usage
Promise.all([Promise1, Promise2, …]).then(onFulfilled, onRejected);
// Example
var promiseCall = function (waitSecond, returnData) {
return function (resolve, reject) {
setTimeout(resolve, waitSecond, returnData);
};
};
var p1 = new Promise(promiseCall(1000, "one"));
var p2 = new Promise(promiseCall(2000, "two"));
var p3 = new Promise(promiseCall(3000, "three"));
var p4 = new Promise(promiseCall(4000, "four"));
var p5 = new Promise(function (resolve, reject) {
reject('5th Promise Rejected');
});
// Calling Promise 1 - 4 in Promise.all()
Promise.all([p1, p2, p3, p4]).then(function (value) {
console.log(value);
}, function (reason) {
// Not Called
console.log(reason);
});
// Expected Output: ["one", "two", "three", "four"]
// Calling Promise 1 - 5 in Promise.all()
Promise.all([p1, p2, p3, p4, p5]).then(function (value) {
// Not Called
console.log(value);
}, function (reason) {
console.log(reason);
});
// Trigger Rejection (Second) Callback if any one is rejected.
// Expected Output: 5th Promise Rejected
Promise.race(iterable)方法使用
根据上边方法介绍,race
是比赛的意思,当iterable参数里的任意一个子promise改变状态后就进入then
方法。
'use strict';
var p1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function(value) {
console.log(value); // "two"
// Both resolve, but p2 is faster
});
当Promise.all方法中的iterable为非迭代器时返回什么?
当iterable的值为空、undefined、object、number、boolean时程序会报异常并将错误信息在catch中捕获,或者在onRejection callback
中捕获
TypeError: XXXX is not iterable (cannot read property Symbol(Symbol.iterator))
当iterable的值为字符串时为当作字符串数组
var invalidPromise7 = Promise.all('test');
// Trigger FulFillment Callback of then Method.
// Then Method Return: ["t", "e", "s", "t"]
当iterable的值为空字符串或者空数组时触发FulFillment Callback
返回空数组
var invalidPromise8 = Promise.all('');
var invalidPromise9 = Promise.all([]);
// Trigger FulFillment Callback of then Method.
// Then Method Return: []
当Promise.race方法中的iterable为非迭代器时返回什么?
当iterable的值为空、undefined、object、number、boolean时程序会报异常并将错误信息在catch中捕获,或者在onRejection callback
中捕获
TypeError: XXXX is not iterable (cannot read property Symbol(Symbol.iterator))
当iterable的值为字符串时为当作字符串数组
var invalidPromise7 = Promise.race('test');
// Trigger FulFillment Callback of then Method.
// Then Method Return: t
当iterable的值为空字符串或者空数组时由于值为undefined
因此不触发回调
var invalidPromise8 = Promise.race('');
varinvalidPromise9 = Promise.race([]);
// Promise value is undefined and trigger no callback.
Promise到达fulfilled
状态的方式
var responseValue = ‘Success’;
// First Approach
var directPromiseValue = Promise.resolve(responseValue);
// Second Approach
var instantiatePromise = new Promise(function (resolve) {
resolve(value);
});
如何将硬编码值变为Promise
var directPromiseValue = Promise.resolve('Success');
var directPromiseValue = Promise.reject('Custom Error Object');
并行
Promise.all()
和 Promise.race()
是并行运行异步操作的两个组合式工具
时序组合可以使用一些优雅的javascript形式
[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());
//等价于
Promise.resolve().then(func1).then(func2);
我们也可以写成可复用的函数形式,这在函数式 编程中极为普遍:
//通过Promise实现时序组合
let applyAsync = (acc,val) => acc.then(val);
let composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);
//通过async/await实现时序组合
for (let f of [func1, func2]) {
await f();
}
时序
Promise对象的回调函数会进入异步任务里边的“微任务”队列
Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2
完整的事件循环规定见另一篇文章学习Node定时器详解
并行式和顺序式:两者兼得
要实现的功能,获取一个story.json
,根据数据中的story.chapterUrls
获取每个章节然后添加到页面中
以一个同步获取数据的例子展示逻辑
try {
var story = getJSONSync('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}
catch (err) {
addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none'
改成异步方式加载各个章节
方法1:一章一章顺序加载
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter's promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
效果图:
方法2:所有章节全部加载完在进行渲染
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened so far
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
效果图:
方法3:第一章下载完后,我们可将其添加到页面。这可让用户在其他章节下载完毕前先开始阅读。第三章下载完后,我们不将其添加到页面,因为还缺少第二章。第二章下载完后,我们可添加第二章和第三章,后面章节也是如此添加。
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
效果图:
常见问题
// 错误示例,包含 3 个问题
doSomething().then(function(result) {
doSomethingElse(result) // 没有返回 Promise 以及没有必要的嵌套 Promise
.then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// 最后是没有使用 catch 终止 Promise 调用链,可能导致没有捕获的异常
修改后
doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing());
.catch(error => console.log(error));