使用Promise解決多層異步調用的簡單學習心得

字號:


    下面小編就為大家?guī)硪黄褂肞romise解決多層異步調用的簡單學習心得。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。
    前言
    第一次接觸到Promise這個東西,是2012年微軟發(fā)布Windows8操作系統(tǒng)后抱著作死好奇的心態(tài)研究用html5寫Metro應用的時候。當時配合html5提供的WinJS庫里面的異步接口全都是Promise形式,這對那時候剛剛畢業(yè)一點javascript基礎都沒有的我而言簡直就是天書。我當時想的是,微軟又在腦洞大開的瞎搗鼓了。
    結果沒想到,到了2015年,Promise居然寫進ES6標準里面了。而且一項調查顯示,js程序員們用這玩意用的還挺high。
    諷刺的是,作為早在2012年就在Metro應用開發(fā)接口里面廣泛使用Promise的微軟,其自家瀏覽器IE直到2015年壽終正寢了都還不支持Promise,看來微軟不是沒有這個技術,而是真的對IE放棄治療了。。。
    現(xiàn)在回想起來,當時看到Promise最頭疼的,就是初學者看起來匪夷所思,也是最被js程序員廣為稱道的特性:then函數(shù)調用鏈。
    then函數(shù)調用鏈,從其本質上而言,就是對多個異步過程的依次調用,本文就從這一點著手,對Promise這一特性進行研究和學習。
    Promise解決的問題
    考慮如下場景,函數(shù)延時2秒之后打印一行日志,再延時3秒打印一行日志,再延時4秒打印一行日志,這在其他的編程語言當中是非常簡單的事情,但是到了js里面就比較費勁,代碼大約會寫成下面的樣子:
    var myfunc = function() {  
      setTimeout(function() {
        console.log("log1");
        setTimeout(function() {
          console.log("log2");
          setTimeout(function() {
            console.log("log3");
          }, 4000);
        }, 3000); 
      }, 2000);
    }
    由于嵌套了多層回調結構,這里形成了一個典型的金字塔結構。如果業(yè)務邏輯再復雜一些,就會變成令人聞風喪膽的回調地獄。
    如果意識比較好,知道提煉出簡單的函數(shù),那么代碼差不多是這個樣子:
    var func1 = function() {
      setTimeout(func2, 2000);
    };
    var func2 = function() {
      console.log("log1");
      setTimeout(func3, 3000);
    };
    var func3 = function() {
      console.log("log2");
      setTimeout(func4, 4000);
    };
    var func4 = function() {
      console.log("log3");
    };
    這樣看起來稍微好一點了,但是總覺得有點怪怪的。。。好吧,其實我js水平有限,說不上來為什么這樣寫不好。如果你知道為什么這樣寫不太好所以發(fā)明了Promise,請告訴我。
    現(xiàn)在讓我們言歸正傳,說說Promise這個東西。
    Promise的描述
    這里請允許我引用MDN對Promise的描述:
    Promise 對象用于延遲(deferred) 計算和異步(asynchronous ) 計算.。一個Promise對象代表著一個還未完成,但預期將來會完成的操作。
    Promise 對象是一個返回值的代理,這個返回值在promise對象創(chuàng)建時未必已知。它允許你為異步操作的成功或失敗指定處理方法。 這使得異步方法可以像同步方法那樣返回值:異步方法會返回一個包含了原返回值的 promise 對象來替代原返回值。
    Promise對象有以下幾種狀態(tài):
    •pending: 初始狀態(tài), 非 fulfilled 或 rejected。
    •fulfilled: 成功的操作。
    •rejected: 失敗的操作。
    pending狀態(tài)的promise對象既可轉換為帶著一個成功值的fulfilled 狀態(tài),也可變?yōu)閹е粋€失敗信息的 rejected 狀態(tài)。當狀態(tài)發(fā)生轉換時,promise.then綁定的方法(函數(shù)句柄)就會被調用。(當綁定方法時,如果 promise對象已經(jīng)處于 fulfilled 或 rejected 狀態(tài),那么相應的方法將會被立刻調用, 所以在異步操作的完成情況和它的綁定方法之間不存在競爭條件。)
    更多關于Promise的描述和示例可以參考MDN的Promise條目,或者MSDN的Promise條目。
    嘗試使用Promise解決我們的問題
    基于以上對Promise的了解,我們知道可以使用它來解決多層回調嵌套后的代碼蠢笨難以維護的問題。關于Promise的語法和參數(shù)上面給出的兩個鏈接已經(jīng)說的很清楚了,這里不重復,直接上代碼。
    我們先來嘗試一個比較簡單的情況,只執(zhí)行一次延時和回調:
    new Promise(function(res, rej) {
      console.log(Date.now() + " start setTimeout");
      setTimeout(res, 2000);
    }).then(function() {
      console.log(Date.now() + " timeout call back");
    });
    看起來和MSDN里的示例也沒什么區(qū)別,執(zhí)行結果如下:
    $ node promisTest.js
    1450194136374 start setTimeout
    1450194138391 timeout call back
    那么如果我們要再做一個延時呢,那么我可以這樣寫:
    new Promise(function(res, rej) {
      console.log(Date.now() + " start setTimeout 1");
      setTimeout(res, 2000);
    }).then(function() {
      console.log(Date.now() + " timeout 1 call back");
      new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 2");
        setTimeout(res, 3000);
      }).then(function() {
        console.log(Date.now() + " timeout 2 call back");
      })
    });
    似乎也能正確運行:
    $ node promisTest.js
    1450194338710 start setTimeout 1
    1450194340720 timeout 1 call back
    1450194340720 start setTimeout 2
    1450194343722 timeout 2 call back
    不過代碼看起來蠢萌蠢萌的是不是,而且隱約又在搭金字塔了。這和引入Promise的目的背道而馳。
    那么問題出在哪呢?正確的姿勢又是怎樣的?
    答案藏在then函數(shù)以及then函數(shù)的onFulfilled(或者叫onCompleted)回調函數(shù)的返回值里面。
    首先明確的一點是,then函數(shù)會返回一個新的Promise變量,你可以再次調用這個新的Promise變量的then函數(shù),像這樣:
    new Promise(...).then(...)
      .then(...).then(...).then(...)...
    而then函數(shù)返回的是什么樣的Promies,取決于onFulfilled回調的返回值。
    事實上,onFulfilled可以返回一個普通的變量,也可以是另一個Promise變量。
    如果onFulfilled返回的是一個普通的值,那么then函數(shù)會返回一個默認的Promise變量。執(zhí)行這個Promise的then函數(shù)會使Promise立即被滿足,執(zhí)行onFulfilled函數(shù),而這個onFulfilled的入?yún)?,即是上一個onFulfilled的返回值。
    而如果onFulfilled返回的是一個Promise變量,那個這個Promise變量就會作為then函數(shù)的返回值。
    關于then函數(shù)和onFulfilled函數(shù)的返回值的這一系列設定,MDN和MSDN上的文檔都沒有明確的正面描述,至于ES6官方文檔ECMAScript 2015 (6th Edition, ECMA-262)。。。我的水平有限實在看不懂,如果哪位高手能解釋清楚官方文檔里面對著兩個返回值的描述,請一定留言指教?。?!
    所以以上為我的自由發(fā)揮,語言組織的有點拗口,上代碼看一下大家就明白了。
    首先是返回普通變量的情況:
    new Promise(function(res, rej) {
      console.log(Date.now() + " start setTimeout 1");
      setTimeout(res, 2000);
    }).then(function() {
      console.log(Date.now() + " timeout 1 call back");
      return 1024;
    }).then(function(arg) {
      console.log(Date.now() + " last onFulfilled return " + arg);  
    });
    以上代碼執(zhí)行結果為:
    $ node promisTest.js
    1450277122125 start setTimeout 1
    1450277124129 timeout 1 call back
    1450277124129 last onFulfilled return 1024
    有點意思對不對,但這不是關鍵。關鍵是onFulfilled函數(shù)返回一個Promise變量可以使我們很方便的連續(xù)調用多個異步過程。比如我們可以這樣來嘗試連續(xù)做兩個延時操作:
    new Promise(function(res, rej) {
      console.log(Date.now() + " start setTimeout 1");
      setTimeout(res, 2000);
    }).then(function() {
      console.log(Date.now() + " timeout 1 call back");
      return new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 2");
        setTimeout(res, 3000);
      });
    }).then(function() {
      console.log(Date.now() + " timeout 2 call back");
    });
    執(zhí)行結果如下:
    $ node promisTest.js
    1450277510275 start setTimeout 1
    1450277512276 timeout 1 call back
    1450277512276 start setTimeout 2
    1450277515327 timeout 2 call back
    如果覺得這也沒什么了不起,那再多來幾次也不在話下:
    new Promise(function(res, rej) {
      console.log(Date.now() + " start setTimeout 1");
      setTimeout(res, 2000);
    }).then(function() {
      console.log(Date.now() + " timeout 1 call back");
      return new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 2");
        setTimeout(res, 3000);
      });
    }).then(function() {
      console.log(Date.now() + " timeout 2 call back");
      return new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 3");
        setTimeout(res, 4000);
      });
    }).then(function() {
      console.log(Date.now() + " timeout 3 call back");
      return new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 4");
        setTimeout(res, 5000);
      });
    }).then(function() {
      console.log(Date.now() + " timeout 4 call back");
    });
    $ node promisTest.js
    1450277902714 start setTimeout 1
    1450277904722 timeout 1 call back
    1450277904724 start setTimeout 2
    1450277907725 timeout 2 call back
    1450277907725 start setTimeout 3
    1450277911730 timeout 3 call back
    1450277911730 start setTimeout 4
    1450277916744 timeout 4 call back
    可以看到,多個延時的回調函數(shù)被有序的排列下來,并沒有出現(xiàn)喜聞樂見的金字塔狀結構。雖然代碼里面調用的都是異步過程,但是看起來就像是全部由同步過程構成的一樣。這就是Promise帶給我們的好處。
    如果你有把啰嗦的代碼提煉成單獨函數(shù)的好習慣,那就更加畫美不看了:
    function timeout1() {
      return new Promise(function(res, rej) {
        console.log(Date.now() + " start timeout1");
        setTimeout(res, 2000);
      });
    }
    function timeout2() {
      return new Promise(function(res, rej) {
        console.log(Date.now() + " start timeout2");
        setTimeout(res, 3000);
      });
    }
    function timeout3() {
      return new Promise(function(res, rej) {
        console.log(Date.now() + " start timeout3");
        setTimeout(res, 4000);
      });
    }
    function timeout4() {
      return new Promise(function(res, rej) {
        console.log(Date.now() + " start timeout4");
        setTimeout(res, 5000);
      });
    }
    timeout1()
      .then(timeout2)
      .then(timeout3)
      .then(timeout4)
      .then(function() {
        console.log(Date.now() + " timout4 callback");
      });
    $ node promisTest.js
    1450278983342 start timeout1
    1450278985343 start timeout2
    1450278988351 start timeout3
    1450278992356 start timeout4
    1450278997370 timout4 callback
    接下來我們可以再繼續(xù)研究一下onFulfilled函數(shù)傳入入?yún)⒌膯栴}。
    我們已經(jīng)知道,如果上一個onFulfilled函數(shù)返回了一個普通的值,那么這個值為作為這個onFulfilled函數(shù)的入?yún)ⅲ荒敲慈绻弦粋€onFulfilled返回了一個Promise變量,這個onFulfilled的入?yún)⒂謥碜阅睦铮?BR>    答案是,這個onFulfilled函數(shù)的入?yún)ⅲ巧弦粋€Promise中調用resolve函數(shù)時傳入的值。
    跳躍的有點大一時間無法接受對不對,讓我們來好好縷一縷。
    首先,Promise.resolve這個函數(shù)是什么,用MDN上面文鄒鄒的說法
    用成功值value解決一個Promise對象。如果該value為可繼續(xù)的(thenable,即帶有then方法),返回的Promise對象會“跟隨”這個value
    簡而言之,這就是異步調用成功情況下的回調。
    我們來看看普通的異步接口中,成功情況的回調是什么樣的,就拿nodejs的上的fs.readFile(file[, options], callback)來說,它的典型調用例子如下
    fs.readFile('/etc/passwd', function (err, data) {
     if (err) throw err;
     console.log(data);
    });
    因為對于fs.readFile這個函數(shù)而言,無論成功還是失敗,它都會調用callback這個回調函數(shù),所以這個回調接受兩個入?yún)ⅲ词r的異常描述err和成功時的返回結果data。
    那么假如我們用Promise來重構這個讀取文件的例子,我們應該怎么寫呢?
    首先是封裝fs.readFile函數(shù):
    function readFile(fileName) {
      return new Promise(function(resolve, reject) {
        fs.readFile(fileName, function (err, data) {
          if (err) {
            reject(err);
          } else {
            resolve(data);
          }
        });
      });
    }
    其次是調用:
    readFile('theFile.txt').then(
      function(data) {
        console.log(data);
      }, 
      function(err) {
        throw err;
      }  
    );
    想象一下,在其他語言的讀取文件的同步調用接口的里面,文件的內容通常是放在哪里?函數(shù)返回值對不對!答案出來了,這個resolve的入?yún)⑹鞘裁??就是異步調用成功情況下的返回值。
    有了這個概念之后,我們就不難理解“onFulfilled函數(shù)的入?yún)?,是上一個Promise中調用resolve函數(shù)時傳入的值”這件事了。因為onFulfilled的任務,就是對上一個異步調用成功后的結果做處理的。
    哎終于理順了。。。
    總結
    下面請允許我用一段代碼對本文講解到的要點進行總結:
    function callp1() {
      console.log(Date.now() + " start callp1");
      return new Promise(function(res, rej) {
        setTimeout(res, 2000);
      });
    }
    function callp2() {
      console.log(Date.now() + " start callp2");
      return new Promise(function(res, rej) {
        setTimeout(function() {
          res({arg1: 4, arg2: "arg2 value"});
        }, 3000);
      });
    }
    function callp3(arg) {
      console.log(Date.now() + " start callp3 with arg = " + arg);
      return new Promise(function(res, rej) {
        setTimeout(function() {
          res("callp3");
        }, arg * 1000);
      });
    }
    callp1().then(function() {
      console.log(Date.now() + " callp1 return");
      return callp2();
    }).then(function(ret) {
      console.log(Date.now() + " callp2 return with ret value = " + JSON.stringify(ret));
      return callp3(ret.arg1);
    }).then(function(ret) {
      console.log(Date.now() + " callp3 return with ret value = " + ret);
    })
    $ node promisTest.js
    1450191479575 start callp1
    1450191481597 callp1 return
    1450191481599 start callp2
    1450191484605 callp2 return with ret value = {"arg1":4,"arg2":"arg2 value"}
    1450191484605 start callp3 with arg = 4
    1450191488610 callp3 return with ret value = callp3
    以上這篇使用Promise解決多層異步調用的簡單學習心得就是小編分享給大家的全部內容了,希望能給大家一個參考