目前來(lái)看,異步操作的未來(lái)非async/await(ES7)莫屬。但是大多數(shù)項(xiàng)目中激挪,還不能立刻扔掉歷史包袱,而且Promise其實(shí)也是實(shí)現(xiàn)async/await的基礎(chǔ)锋叨,在ES6中Promise也被寫(xiě)入了規(guī)范中垄分,所以深入學(xué)習(xí)一下Promise還是很有必要的。
首先拋開(kāi)Promise娃磺,了解一下異步操作的流程薄湿。
假設(shè)有一個(gè)異步任務(wù)的模板,我們使用setTimeout模擬異步豌鸡,在每個(gè)異步任務(wù)中先打印下這個(gè)參數(shù)arg嘿般,然后以2*arg作為參數(shù)傳入回調(diào)函數(shù)中。下面我們分別以串行涯冠、并行方式執(zhí)行幾個(gè)異步任務(wù)炉奴。
asyncFn=(arg,cb)=>{
setTimeout(function(){
console.log(`參數(shù)為${arg}`)
cb(arg*2)
},1000);
}
//這是要執(zhí)行異步任務(wù)的參數(shù)隊(duì)列
let items=[1,2,3,4,5,6];
串行任務(wù):在每個(gè)異步任務(wù)的回調(diào)方法中通過(guò)shift()方法,每次從任務(wù)隊(duì)列中取出一個(gè)值蛇更,并且更新剩余任務(wù)的數(shù)組瞻赶,實(shí)現(xiàn)任務(wù)的接力進(jìn)行。
let results=[];
final=(value)=>{
console.log(`完成:${value}`);
}
series=(item)=>{
if (item) {
asyncFn(item,function(res){
results.push(res);;
return series(items.shift());
})
}else{
final(results);
console.timeEnd('sync');
}
}
console.time('sync');
series(items.shift());
//串行執(zhí)行6.10s
并行任務(wù):一開(kāi)始就將所有任務(wù)都執(zhí)行派任,然后監(jiān)測(cè)負(fù)責(zé)保存異步任務(wù)執(zhí)行結(jié)果的數(shù)組的長(zhǎng)度砸逊,若等于任務(wù)隊(duì)列長(zhǎng)度時(shí),則是所有異步任務(wù)都執(zhí)行完畢掌逛。
let len=items.length;
console.time('asyncFn');
items.forEach((item)=>{
asyncFn(item,function(res){
results.push(res);
if (results.length===len) {
final(results);
console.timeEnd('asyncFn');
}
})
})
//并行執(zhí)行1.01s
對(duì)并行任務(wù)的數(shù)量進(jìn)行控制:增加一個(gè)參數(shù)記錄正在執(zhí)行的任務(wù)的個(gè)數(shù)师逸,每開(kāi)始執(zhí)行一個(gè)任務(wù)加1,每到回調(diào)函數(shù)即將結(jié)束時(shí)減1豆混。
//并行與串行的配合篓像,即設(shè)定每次最多能并行n個(gè)異步任務(wù)
let running=0;
let limit=2;
console.time('control');
launcher=()=>{
while(running < limit && items.length>0){
let item = items.shift();
running++;
asyncFn(item,function(res){
results.push(res);
running--;
if (items.length>0) {
launcher();
}else if(running==0){
final();
console.timeEnd('control');
}
});
}
}
launcher();
//3.01s
Promise基礎(chǔ)回顧
then方法可以鏈?zhǔn)秸{(diào)用
(new Promise(fn1))
.then(step1)
.then(step2)
.then(step3)
.then(
console.log
console.error
)
錯(cuò)誤具有傳遞性。console.error可以顯示之前任一步發(fā)生的錯(cuò)誤皿伺,而且該步之后的任務(wù)不會(huì)繼續(xù)執(zhí)行员辩。但是console.log只能顯示step3的返回值。
新建一個(gè)promise對(duì)象
var promise=new Promise((resolve,reject){})
實(shí)例方法
promise.then(onFullfilled,onRejected)
靜態(tài)方法
Promise.resolve()
Promise.reject()
-
Promise.resolve
將傳遞給他的參數(shù)填充到Promise對(duì)象并返回這個(gè)Promise對(duì)象鸵鸥。
Promise.resolve(42)
可以被認(rèn)為是new Promise(function(resolve){ resolve(42) })
的語(yǔ)法糖奠滑。
Promise.resolve()
方法還能將thenable
對(duì)象轉(zhuǎn)換為ES6中定義的promise對(duì)象。thenable
對(duì)象就是具有then方法但不是promise對(duì)象的對(duì)象,比如jQuery.ajax()的返回對(duì)象即使一個(gè)對(duì)象具有 .then 方法宋税,也不一定就能作為ES6 Promises對(duì)象使用
var promise=Promise.resolve($.ajax('/json/comment.json')); promise.then(function(value){ console.log(value); })
-
Promise.reject
與Promise.resolve類(lèi)似的靜態(tài)方法
Promise.reject(new Error('err'))摊崭;
等同于
new Promise(function(resolve,reject){
reject(new Error('err'))
})
常見(jiàn)應(yīng)用:
//使用Promise封裝一個(gè)ajax請(qǐng)求:
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));
};
res.send();
})
}
//異步加載圖片
let preloadImage=(path)=>{
return new Promise(function(resolve,reject){
let img=new Image();
img.onload=resolve;
img.onerror=reject;
img.src=path;
})
}
錯(cuò)誤捕獲: catch與then
catch方法只是then(undefined,onReject)的封裝,實(shí)質(zhì)是一樣的弃甥。
promise.then(undefined,function(err){
console.error(err);
})
- 使用promise.then(onFulfilled, onRejected) 的話onFulFilled中發(fā)生錯(cuò)誤無(wú)法捕獲
- 使用.catch鏈?zhǔn)皆趖hen后調(diào)用可以捕獲then中的錯(cuò)誤
- 本質(zhì)上一樣爽室,區(qū)別使用場(chǎng)合
錯(cuò)誤捕獲在IE8的問(wèn)題
catch是ES3中的保留字,所以在IE8以下不能作為對(duì)象的屬性名使用淆攻,會(huì)出現(xiàn)identifier not found錯(cuò)誤。
- 點(diǎn)標(biāo)記法要求對(duì)象的屬性必須是有效的標(biāo)識(shí)符
- 中括號(hào)標(biāo)記法可以將非法標(biāo)識(shí)符作為對(duì)象的屬性名使用
var promise=Promise.reject(new Error('msg'));
promise["catch"](function(error){
console.error(error);
})
或者使用then方法中添加第二個(gè)參數(shù)來(lái)避免這個(gè)問(wèn)題
then(undefined,onReject)
Promise的同步異步
Promise只能使用異步調(diào)用方式嘿架。
//Promise在定義時(shí)就會(huì)調(diào)用
var promise=new Promise(function(resolve){
resolve(2);//異步調(diào)用回調(diào)函數(shù)
console.log('innner')
})
promise.then(function(value){
console.log(value);
})
console.log('outer');
會(huì)依次打印inner,outer,2
- 決不能對(duì)異步函數(shù)進(jìn)行同步調(diào)用瓶珊,處理順序可能會(huì)與語(yǔ)氣不符,可能帶來(lái)意料之外的后果
- 還可能導(dǎo)致棧溢出或者異常處理錯(cuò)誤等耸彪。
Promise保證了每次調(diào)用都是異步的伞芹,所以在實(shí)際編碼中不需要使用setTimeout自己實(shí)現(xiàn)異步。
有多個(gè)Promise實(shí)例時(shí):
-
promise.all() 所有異步任務(wù)并行執(zhí)行
接受promise對(duì)象組成的數(shù)組作為參數(shù)蝉娜。輸出的每個(gè)promise的結(jié)果和參數(shù)數(shù)組的順序一致唱较。
-
promise.race 有一個(gè)異步任務(wù)完成則返回結(jié)果
promise.race()同樣接受多個(gè)promise對(duì)象組成的數(shù)組作為參數(shù),但是只要有一個(gè)promise對(duì)象變?yōu)閒ulFilled或者rejected狀態(tài)召川,就會(huì)繼續(xù)后面的處理
基于promise.race()實(shí)現(xiàn)超時(shí)處理
function delayPromise(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
})
}
function timeoutPromise(promise, ms) {
//用以提供超時(shí)基準(zhǔn)的promise實(shí)例
var timeout = delayPromise(ms).then(function() {
throw new Error(`operation timed out after ${ms} ms`);
})
return Promise.race([promise, timeout]);
}
//新的task
var taskPromise = new Promise(function(resolve) {
var delay = Math.random() * 2000;
setTimeout(function() {
resolve(`${dealy} ms`);
}, dealy)
});
timeoutPromise(taskPromise, 1000)
.then(function(value) {
console.log(`task在規(guī)定時(shí)間內(nèi)結(jié)束${value}`)
})
.catch(function(err) {
console.log(`發(fā)生超時(shí):${err}`);
})
但是不能區(qū)分這個(gè)異常是普通錯(cuò)誤還是超時(shí)錯(cuò)誤南缓。需要定制一個(gè)error對(duì)象。
function copyOwnFrom(target,source){
Object.getOwnPropertyNames(source).forEach(function(propName){
Object.defineProperty(target,propName,Object.getOwnPropertyDescriptor(source,propName));
})
return target
}
//通ECMAScript提供的內(nèi)建對(duì)象Error實(shí)現(xiàn)繼承
function TimeoutError(){
var superInstance=Error.apply(null,arguments);
copyOwnFrom(this,superInstance);
}
TimeoutError.prototype=Object.create(Error.prototype);
TimeoutError.prototype.constructor=TimeoutError;
用于提供超時(shí)基準(zhǔn)的promise實(shí)例改為
var timeout = delayPromise(ms).then(function() {
throw new TimeoutError(`operation timed out after ${ms} ms`);
})
在錯(cuò)誤捕獲中可修改為:
timeoutPromise(taskPromise, 1000)
.then(function(value) {
console.log(`task在規(guī)定時(shí)間內(nèi)結(jié)束${value}`)
})
.catch(function(err) {
if(err instanceof TimeoutError){
console.log(`發(fā)生超時(shí):${err}`);
}else{
console.log(`錯(cuò)誤:${err}`);
}
})
超時(shí)取消XHR請(qǐng)求
//通過(guò)cancelableXHR 方法取得包裝了XHR的promise對(duì)象和取消該XHR請(qǐng)求的方法
//
function cancelableXHR(url){
var req=new XMLHttpRequest();
var promise=new Promise(function(resolve,reject){
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.responseText))
}
req.onabort=function(){
reject(new Error('abort this request'))
}
res.send();
})
var abort=function(){
if (req.readyState!==XMLHttpRequest.UNSENT) {
req.abort();
}
}
return {
promise:promise,
abort:abort
}
}
var object=cancelableXHR('http://www.sqqs.com/index')
timeoutPromise(object.promise,1000).then(function(content){
console.log(`content:${content}`);
}).catch(function(error){
if (error instanceof TimeoutError) {
object.abort();
return console.log(error)
}
console.log(`XHR Error:${error}`);
})
promise 順序處理sequence
promise.all()是多個(gè)promise對(duì)象同時(shí)執(zhí)行荧呐,沒(méi)有api直接支持多個(gè)任務(wù)線性執(zhí)行汉形。
我們需要在上一個(gè)任務(wù)執(zhí)行結(jié)果的promise對(duì)象的基礎(chǔ)上執(zhí)行下一個(gè)promise任務(wù)。
var promiseA = function() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(111);
}, 200)
})
}
var promiseB = function(args) {
return new Promise(function(resolve) {
setTimeout(function() {
resolve(2222);
console.timeEnd('sync');
}, 200);
})
}
console.time('sync');
var result = Promise.resolve();
[promiseA, promiseB].forEach(function(promise) {
result = result.then(promise)
})
//print
//sync:408ms
通過(guò)這個(gè)名為result的promise對(duì)象來(lái)不斷更新保存新返回的promise對(duì)象倍阐,從而實(shí)現(xiàn)一種鏈?zhǔn)秸{(diào)用概疆。
也可以使用reduce重寫(xiě)循環(huán),使得代碼更加美觀一些:
console.time('sync');
tasks=[promiseA, promiseB];
tasks.reduce(function(result,promise){
return result.then(promise)
},Promise.resolve())
其中Promise.resolve()作為reduce方法的初始值賦值給result峰搪。
promise穿透--永遠(yuǎn)往then中傳遞函數(shù)
如下例子岔冀,在then中傳遞了一個(gè)promise實(shí)例
Promise.resolve('foo').then(Promise.resolve('bar')).then(function(result){
console.log(result)
})
打印結(jié)果為foo,像then 中傳遞的并非一個(gè)函數(shù),實(shí)際上會(huì)將其解釋為then(null)概耻。若想要得到bar,需要將then中傳遞一個(gè)函數(shù)
Promise.resolve('foo').then(function() {
return Promise.resolve('bar')
}).then(function(result) {
console.log(result)
})
//print result:
//bar
如果在then中的函數(shù)沒(méi)有對(duì)promise對(duì)象使用return返回呢使套,又是什么結(jié)果?
Promise.resolve('foo').then(function() {
Promise.resolve('bar')
}).then(function(result) {
console.log(result)
})
會(huì)返回一個(gè)undefined咐蚯。
拋磚引玉童漩,我們?cè)倏偨Y(jié)一下向then中傳遞函數(shù)的情況:
var doSomething=function(){
return Promise.resolve('bar')
}
var printResult=function(result){
console.log(`result:${result}`)
}
//試想一下,以下幾個(gè)例子輸出的結(jié)果分別是什么
Promise.resolve('foo').then(function(value){
return doSomething();
}).then(printResult)
Promise.resolve('foo').then(function(){
doSomething();
}).then(printResult)
Promise.resolve('foo').then(doSomething()).then(printResult)
Promise.resolve('foo').then(doSomething).then(printResult)