Promise進階

目前來看,異步操作的未來非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)

【參考資料】

promise中需要注意的問題

promise 迷你書

js原生promise

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 官方中文版原文鏈接 感謝社區中各位的大力支持,譯者再次奉上一點點福利:阿里云產品券,享受所有官網優惠,并抽取幸運大...
    HetfieldJoe閱讀 11,038評論 26 95
  • 00、前言Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區...
    夜幕小草閱讀 2,139評論 0 12
  • Promiese 簡單說就是一個容器,里面保存著某個未來才會結束的事件(通常是一個異步操作)的結果,語法上說,Pr...
    雨飛飛雨閱讀 3,373評論 0 19
  • Promise的含義: ??Promise是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和...
    呼呼哥閱讀 2,189評論 0 16
  • 小學同學群,剛開始創建,是依稀的幾個人,還是幾個男同學,后來進入個女同學,也是沒見出來吭過聲,于是大家就把她忽略了...
    你的珍貴閱讀 1,378評論 21 16