目前來看,異步操作的未來非async/await(ES7)莫屬。但是大多數項目中,還不能立刻扔掉歷史包袱,而且Promise其實也是實現async/await的基礎,在ES6中Promise也被寫入了規范中,所以深入學習一下Promise還是很有必要的。
首先拋開Promise,了解一下異步操作的流程。
假設有一個異步任務的模板,我們使用setTimeout模擬異步,在每個異步任務中先打印下這個參數arg,然后以2*arg作為參數傳入回調函數中。下面我們分別以串行、并行方式執行幾個異步任務。
asyncFn=(arg,cb)=>{
setTimeout(function(){
console.log(`參數為${arg}`)
cb(arg*2)
},1000);
}
//這是要執行異步任務的參數隊列
let items=[1,2,3,4,5,6];
串行任務:在每個異步任務的回調方法中通過shift()方法,每次從任務隊列中取出一個值,并且更新剩余任務的數組,實現任務的接力進行。
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());
//串行執行6.10s
并行任務:一開始就將所有任務都執行,然后監測負責保存異步任務執行結果的數組的長度,若等于任務隊列長度時,則是所有異步任務都執行完畢。
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');
}
})
})
//并行執行1.01s
對并行任務的數量進行控制:增加一個參數記錄正在執行的任務的個數,每開始執行一個任務加1,每到回調函數即將結束時減1。
//并行與串行的配合,即設定每次最多能并行n個異步任務
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基礎回顧
then方法可以鏈式調用
(new Promise(fn1))
.then(step1)
.then(step2)
.then(step3)
.then(
console.log
console.error
)
錯誤具有傳遞性。console.error可以顯示之前任一步發生的錯誤,而且該步之后的任務不會繼續執行。但是console.log只能顯示step3的返回值。
新建一個promise對象
var promise=new Promise((resolve,reject){})
實例方法
promise.then(onFullfilled,onRejected)
靜態方法
Promise.resolve()
Promise.reject()
-
Promise.resolve
將傳遞給他的參數填充到Promise對象并返回這個Promise對象。
Promise.resolve(42)
可以被認為是new Promise(function(resolve){ resolve(42) })
的語法糖。
Promise.resolve()
方法還能將thenable
對象轉換為ES6中定義的promise對象。thenable
對象就是具有then方法但不是promise對象的對象,比如jQuery.ajax()的返回對象即使一個對象具有 .then 方法,也不一定就能作為ES6 Promises對象使用
var promise=Promise.resolve($.ajax('/json/comment.json')); promise.then(function(value){ console.log(value); })
-
Promise.reject
與Promise.resolve類似的靜態方法
Promise.reject(new Error('err'));
等同于
new Promise(function(resolve,reject){
reject(new Error('err'))
})
常見應用:
//使用Promise封裝一個ajax請求:
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;
})
}
錯誤捕獲: catch與then
catch方法只是then(undefined,onReject)的封裝,實質是一樣的。
promise.then(undefined,function(err){
console.error(err);
})
- 使用promise.then(onFulfilled, onRejected) 的話onFulFilled中發生錯誤無法捕獲
- 使用.catch鏈式在then后調用可以捕獲then中的錯誤
- 本質上一樣,區別使用場合
錯誤捕獲在IE8的問題
catch是ES3中的保留字,所以在IE8以下不能作為對象的屬性名使用,會出現identifier not found錯誤。
- 點標記法要求對象的屬性必須是有效的標識符
- 中括號標記法可以將非法標識符作為對象的屬性名使用
var promise=Promise.reject(new Error('msg'));
promise["catch"](function(error){
console.error(error);
})
或者使用then方法中添加第二個參數來避免這個問題
then(undefined,onReject)
Promise的同步異步
Promise只能使用異步調用方式。
//Promise在定義時就會調用
var promise=new Promise(function(resolve){
resolve(2);//異步調用回調函數
console.log('innner')
})
promise.then(function(value){
console.log(value);
})
console.log('outer');
會依次打印inner,outer,2
- 決不能對異步函數進行同步調用,處理順序可能會與語氣不符,可能帶來意料之外的后果
- 還可能導致棧溢出或者異常處理錯誤等。
Promise保證了每次調用都是異步的,所以在實際編碼中不需要使用setTimeout自己實現異步。
有多個Promise實例時:
-
promise.all() 所有異步任務并行執行
接受promise對象組成的數組作為參數。輸出的每個promise的結果和參數數組的順序一致。
-
promise.race 有一個異步任務完成則返回結果
promise.race()同樣接受多個promise對象組成的數組作為參數,但是只要有一個promise對象變為fulFilled或者rejected狀態,就會繼續后面的處理
基于promise.race()實現超時處理
function delayPromise(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
})
}
function timeoutPromise(promise, ms) {
//用以提供超時基準的promise實例
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在規定時間內結束${value}`)
})
.catch(function(err) {
console.log(`發生超時:${err}`);
})
但是不能區分這個異常是普通錯誤還是超時錯誤。需要定制一個error對象。
function copyOwnFrom(target,source){
Object.getOwnPropertyNames(source).forEach(function(propName){
Object.defineProperty(target,propName,Object.getOwnPropertyDescriptor(source,propName));
})
return target
}
//通ECMAScript提供的內建對象Error實現繼承
function TimeoutError(){
var superInstance=Error.apply(null,arguments);
copyOwnFrom(this,superInstance);
}
TimeoutError.prototype=Object.create(Error.prototype);
TimeoutError.prototype.constructor=TimeoutError;
用于提供超時基準的promise實例改為
var timeout = delayPromise(ms).then(function() {
throw new TimeoutError(`operation timed out after ${ms} ms`);
})
在錯誤捕獲中可修改為:
timeoutPromise(taskPromise, 1000)
.then(function(value) {
console.log(`task在規定時間內結束${value}`)
})
.catch(function(err) {
if(err instanceof TimeoutError){
console.log(`發生超時:${err}`);
}else{
console.log(`錯誤:${err}`);
}
})
超時取消XHR請求
//通過cancelableXHR 方法取得包裝了XHR的promise對象和取消該XHR請求的方法
//
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()是多個promise對象同時執行,沒有api直接支持多個任務線性執行。
我們需要在上一個任務執行結果的promise對象的基礎上執行下一個promise任務。
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
通過這個名為result的promise對象來不斷更新保存新返回的promise對象,從而實現一種鏈式調用。
也可以使用reduce重寫循環,使得代碼更加美觀一些:
console.time('sync');
tasks=[promiseA, promiseB];
tasks.reduce(function(result,promise){
return result.then(promise)
},Promise.resolve())
其中Promise.resolve()作為reduce方法的初始值賦值給result。
promise穿透--永遠往then中傳遞函數
如下例子,在then中傳遞了一個promise實例
Promise.resolve('foo').then(Promise.resolve('bar')).then(function(result){
console.log(result)
})
打印結果為foo,像then 中傳遞的并非一個函數,實際上會將其解釋為then(null)。若想要得到bar,需要將then中傳遞一個函數
Promise.resolve('foo').then(function() {
return Promise.resolve('bar')
}).then(function(result) {
console.log(result)
})
//print result:
//bar
如果在then中的函數沒有對promise對象使用return返回呢,又是什么結果?
Promise.resolve('foo').then(function() {
Promise.resolve('bar')
}).then(function(result) {
console.log(result)
})
會返回一個undefined。
拋磚引玉,我們再總結一下向then中傳遞函數的情況:
var doSomething=function(){
return Promise.resolve('bar')
}
var printResult=function(result){
console.log(`result:${result}`)
}
//試想一下,以下幾個例子輸出的結果分別是什么
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)