Promise--優雅解決回調嵌套

最近一直在用空餘時間研究node,當我寫了一個簡單的複製一個文件夾中的文件到另外一個位置的時候,我看到了所謂的回調地獄,雖然只是四五個回調嵌套,可是這已經讓我感到懼怕,我寫這麼簡單的一個小demo就寫成這樣,那稍微複雜點兒還了得?記得在看ES6的時候,裏面提到過一種新的解決回調的方式---Promise,並且在node中也經常使用這個解決大量嵌套,因此這幾天花了點兒時間看了看Promise,讓我對Promise的認識更加清晰,因此寫一些東西總結一下。node

Promise狀態的理解

new Promise實例化的Promise對象有三個狀態:es6

  • 「has-resolution」 - Fulfilled數組

    • reslove(成功時),調用onFulfilledpromise

  • "has-rejection" - Rejected異步

    • reject(失敗時)。調用Rejectedasync

  • "unresolve" - Pending函數

    • 既不是resolve也不是reject狀態,也就是Promise剛剛被建立後的初始化狀態。this

圖片描述

note:

  1. 在Chrome中輸出resolve能夠獲得Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined},能夠看出[[PromiseStatus]]中存儲的就是Promise的狀態,可是並無公開訪問[[PromiseStatus]]的用戶API,因此暫時還沒法查詢其內部狀態。spa

  2. Promise中的then的回調只會被調用一次,由於Promise的狀態只會從Pending變爲Fulfilled或者Rejected,不可逆。設計

Promise的使用

在使用Promise實現有序執行異步的基本格式以下:

//defined Promise async function
function asyncFun(){
    return new Promise((reslove,reject)=>{
        if(reslove){
            reslove(/*reslove parameter*/);
        }else{
            reject(new Error(/*Error*/));
        }
    })
}

//use Promise&then
asyncFun().then(/*function*/).then(/*function*/)...

reslove方法的參數就是要傳給回調函數的參數,即resolve將運行獲得的結果傳出來,而then接受該參數給回調繼續執行後面的,若是這個then的中的函數還會返回Promise,則會重複執行該步驟直到結束。

reject方法的參數通常是包含了reject緣由的Error對象。rejectresolve同樣,也會將本身的參數傳出去,接收該參數的是then的第二個fun或者是catch。其實.catch只是Promise.then(onFulfilled,onRejected)的別名而已。

快捷建立Promise

通常狀況下咱們會使用new Promise來建立prmise對象,除此以外咱們也可使用Promise.reslovePromise.reject來直接建立,例如Promise.resolve(42)能夠認爲是如下代碼的語法糖

new Promise((reslove)=>{
    reslove(42);
});

這段代碼可讓這個Promise對象當即進入resolve狀態,並將42傳遞給後面then裏所指定的onFulfilled函數。此外Promise.resolve還有一個做用,就是將非Promise對象轉換爲Promise對象。

Promise.reject(value)與之相似。

Promise.then()的異步調用帶來的思考

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

/*輸出:
"inner promise"
"outer promise"
42
*/

從以上的這段代碼咱們能夠看出Promise.then()是異步調用的,這也是Promise設計上規定的,其緣由在於同步調用和異步調用同時存在會致使混亂

以上那段代碼若是在調用onReady以前DOM已經載入的話,對回調函數進行同步調用,若是在調用onReady以前DOM尚未載入的話,經過註冊DOMContentLoader事件監聽器來對回調進行異步調用。這會致使該代碼在源文件中不一樣位置輸出不一樣的結果,關於這個現象,有以下幾點:

  • 絕對不能對異步函數(即便在數據已經就緒)進行同步調用

  • 若是對異步回調函數進行同步調用,處理順序可能會與預期不符,帶來意外的結果

  • 對異步回調函數進行同步調用,還可能致使棧溢出或者異常處理錯亂等問題

  • 若是想在未來的某個時刻調用異步回調,可使用setTimeout等異步API

因此以上代碼應該使用 setTimeout(fn, 0)進行調用。

function onReady(fn) {
    var readyState = document.readyState;
    if (readyState === 'interactive' || readyState === 'complete') {
        setTimeout(fn, 0);
    } else {
        window.addEventListener('DOMContentLoaded', fn);
    }
}
onReady(function () {
    console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');

因此在Promise中then是異步的。

Promise鏈式調用

各個Task相互獨立

若是想實現Promise的鏈式調用,要求每次鏈式調用都返回Promise。因此每一個異步執行都須要使用Promise包裝,這裏有一個誤區:每一個thencatch會返回也會反回一個新的Promise,可是這僅僅實現了鏈式調用,若是不將異步操做用Promise進行包裝,依然不行。下面的例子就是錯誤的

function pro1(){
    return new Promise((reslove,reject)=>{
        if(reslove){
            setTimeout(()=>{console.log(1000)},1000);
            reslove();
        }
    })
}

function pro2(){
    setTimeout(()=>{console.log(2000)},2000);
}

function pro3(){
    setTimeout(()=>{console.log(3000)},3000);
}

pro1().then(pro2).then(pro3);
//or
function pro1(){
    setTimeout(()=>{console.log(1000)},1000);
}

Promise.resolve().then(pro1).then(pro2).then(pro3);

上面的寫法有兩處錯誤:

  1. 雖然在第一個函數返回了一個Promise,可是因爲後面的異步操做並無被Promise包裝,因此並不會起任何做用,正確的作法是每個異步操做都要被Promise包裝

  2. resolve()調用的時機不對,resolve須要在異步操做執行完成後調用,因此須要寫在異步操做內部,若是像上面那樣寫在異步操做外面,則不會起做用。

因此正確寫法以下:

//直接返回Promise
function pro1(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(1000);resolve();},1000);
        
    })
}
function pro2(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(5000);resolve();},5000);
        
    });
}
function pro3(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(500);resolve();},500);
    })
}
pro1().then(pro2).then(pro3);

//or使用Promise.reslove()

function pro1(cb){setTimeout(()=>{console.log(1000);cb()},1000)};
function pro2(cb){setTimeout(()=>{console.log(3000);cb()},3000)};
function pro3(cb){setTimeout(()=>{console.log(500);cb()},500)};


Promise.resolve()
       .then(()=>new Promise(resolve=>pro1(resolve)))
       .then(()=>new Promise(resolve=>pro2(resolve)))
       .then(()=>new Promise(resolve=>pro3(resolve)));

各個Task須要參數的傳遞

在Promise的鏈式調用中,有可能各個task之間存在相互依賴,例如TaskA想給TaskB傳遞一個參數,像下面這樣:

/*例1.使用Promise.resolve()啓動*/
let task1 = (value1)=>value1+1;
let task2 = (value2)=>value2+2;
let task3 = (value3)=>{console.log(value3+3)};

Promise.resolve(1).then(task1).then(task2).then(task3);//console => 7


/*例2.普通的返回一個Promise*/
function task1(value1){
  return new Promise((resolve,reject)=>{
    if(resolve){
      resolve(value1+1);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}

function task2(value2){
  return new Promise((resolve,reject)=>{
    if(resolve){
      resolve(value2+2);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}
function task3(value3){
  return new Promise((resolve,reject)=>{
    if(resolve){
      console.log(value3+3);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}

task1(1).then(task2).then(task3);//console => 7

關於reslovereject有如下兩點說明:

  • reslove函數的做用是將Promise對象的狀態從「未完成」變爲「成功」(即從Pending變爲Resolved),在異步操做成功時調用,並將異步操做的結果做爲參數傳遞出去

  • reject函數的做用是將Promise對象狀態從「未完成」變爲「失敗」(即從Pending變爲Rejected),在異步操做失敗時候調用,並將異步操做報出的錯誤做爲參數傳遞出去

因此從上面的例子和它們的用法能夠看出,若是想要傳遞給後面task有兩種方法:

  • 若是使用Promise.resolve()啓動Promise,則像例1中那樣在須要傳遞的參數前面加return便可。

  • 若是是利用Promise包裝了任務,則把想要傳遞給下一個task的參數傳入resolve()便可。

特別說明:若是須要resolve()日後傳遞多個參數,不能直接寫resolve(a1,a2,a3),這樣只能拿到第一個要傳的參數,須要以數組或對象去傳遞

let obj = {a1:a1,a2:a2,a3:a3};
resolve(obj)
//or
let arr =[a1,a2,a3];
resolve(arr);

thencatch返回新的Promise

在Promise中不管是then仍是catch方法,都會返回返回一個新的Promise對象。

圖片描述

var aPromise = new Promise(function (resolve) {
    resolve(100);
});
var thenPromise = aPromise.then(function (value) {
    console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
    console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true

因此像下面這樣將鏈式調用分開寫是不成功的

// 1: 對同一個promise對象同時調用 `then` 方法
var aPromise = new Promise(function (resolve) {
    resolve(100);
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    console.log("1: " + value); // => 100
});

因爲每次調用then方法都會返回一個新的Promise,因此致使最終輸出100而不是100 2 2。

Promise.all()的使用

有時候須要多個彼此沒有關聯的多個異步任務所有執行完成後再執行後面的操做,這時候就須要用到Promise.all(),它接收一個Promise的對象的數組做爲參數,當這個數組裏的全部Promise對象所有變成resolve或者reject的時候,它纔會去調用後面的.then()

這裏須要說明一點,兩個彼此無關的異步操做會同時執行,每一個Promise的結果(即每一個返回的Promise的resolve或reject時傳遞的參數)和傳遞給Promise.all的Promise數組的順序一致。也就是說,假設有兩個異步操做TaskA和TaskB,若是傳入順序爲Promise.all([TaskA,TaskB]),則執行完成後傳給.then的順序爲[TaskA,TaskB]。

function setTime(time){
  return new Promise((resolve)=>{
    setTimeout(()=>resolve(time),time);
  })
}

let startTime = Date.now();

Promise.all([setTime(1),setTime(100),setTime(200)])
       .then((value)=>{
         console.log(value);    //[1,100,200]
         console.log(Date.now() - startTime); //203
       });

從上面函數的輸出值能夠看出Promise.all()裏的異步操做是同時執行的並且傳給.then()的順序和Promise.all()裏的順序同樣。最終執行時間約爲200ms,爲何不是200ms,這裏涉及到關於setTimeout的精準問題,不在這裏討論。

Promise.race()的使用

Promise.rance()的用法與Promise.all()相似,不一樣的地方在於Promise.all()是在接收到的全部Promise都變爲FulFilled或者Rejected狀態以後纔會繼續進行後面的處理,而Promise.rance()只要有一個Promise對象進入FullFilled或者Rejected狀態,就會繼續進行後續處理。這至關於Promise.all()進行運算而Promise.rance()進行運算。可是這裏有一點須要注意一下:

var taskA = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is taskA');
            resolve('this is taskA');
        }, 4);
    });
var taskB = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is taskB');
            resolve('this is taskB');
        }, 1000);
    });

Promise.race([winnerPromise, loserPromise]).then(function (value) {
    console.log(value);
});

/*
輸出結果:
this is taskA
this is taskA
this is taskB
*/

從這裏能夠看出,在第一個Promise變爲FulFiled狀態運行then裏的回調後,後面的Promise並無中止運行,而是接續執行。也就是說, Promise.race 在第一個promise對象變爲Fulfilled以後,並不會取消其餘promise對象的執行。

Promise的reject和異步操做error的理解

function ReadEveryFiles(file){
    return new Promise((resolve,reject)=>{
        if(resolve){
            fs.readFile(`${__dirname}/jQuery/${file}`,(err,data)=>{
                if(err){
                    console.log(err);
                }else{
                    let obj = {data:data,file:file};
                    resolve(obj);
                }
            });
        }else{
            //promise reject error
        }
    });
}

這裏的readFile的error和Promise的reject不同,一個是readFile過程當中致使的錯誤,而另外一個是Promise作處理的時候致使的錯誤,能夠這樣理解,假設讀取文件成功了,可是Promise還須要講這個異步操做獲得的數據拿處處理,在Promise作這些操做的時候可能出錯。

寫在最後

這幾天開始用Promise寫了一些東西,發現其實若是用Promise,會使得代碼量加大,由於每個異步都要被Promise封裝,可是這樣換來的倒是更加容易的維護,因此仍是值得的,當代碼寫完後,咱們很容易就能看出代碼的執行過程,相對於原來用嵌套去寫要直觀許多,而若是想要解決Promise的代碼量過大的問題,咱們可使用Generator函數,另外,在ES7標準中推出了更加牛的異步解決方案Async/Await,關於它們,我將會在隨後繼續深刻。

參考

JavaScript Promise迷你書(中文版)
ECMAScript 6 入門---Promise對象

相關文章
相關標籤/搜索