前言
第一次接觸到Promise這個東西,是2012年微軟發布Windows8操作系統後抱著作死好奇的心態研究用html5寫Metro應用的時候。當時配合html5提供的WinJS庫裡面的異步接口全都是Promise形式,這對那時候剛剛畢業一點javascript基礎都沒有的我而言簡直就是天書。我當時想的是,微軟又在腦洞大開的瞎搗鼓了。
結果沒想到,到了2015年,Promise居然寫進ES6標准裡面了。而且一項調查顯示,js程序員們用這玩意用的還挺high。
諷刺的是,作為早在2012年就在Metro應用開發接口裡面廣泛使用Promise的微軟,其自家浏覽器IE直到2015年壽終正寢了都還不支持Promise,看來微軟不是沒有這個技術,而是真的對IE放棄治療了。。。
現在回想起來,當時看到Promise最頭疼的,就是初學者看起來匪夷所思,也是最被js程序員廣為稱道的特性:then函數調用鏈。
then函數調用鏈,從其本質上而言,就是對多個異步過程的依次調用,本文就從這一點著手,對Promise這一特性進行研究和學習。
Promise解決的問題
考慮如下場景,函數延時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); }
由於嵌套了多層回調結構,這裡形成了一個典型的金字塔結構。如果業務邏輯再復雜一些,就會變成令人聞風喪膽的回調地獄。
如果意識比較好,知道提煉出簡單的函數,那麼代碼差不多是這個樣子:
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水平有限,說不上來為什麼這樣寫不好。如果你知道為什麼這樣寫不太好所以發明了Promise,請告訴我。
現在讓我們言歸正傳,說說Promise這個東西。
Promise的描述
這裡請允許我引用MDN對Promise的描述:
Promise 對象用於延遲(deferred) 計算和異步(asynchronous ) 計算.。一個Promise對象代表著一個還未完成,但預期將來會完成的操作。
Promise 對象是一個返回值的代理,這個返回值在promise對象創建時未必已知。它允許你為異步操作的成功或失敗指定處理方法。 這使得異步方法可以像同步方法那樣返回值:異步方法會返回一個包含了原返回值的 promise 對象來替代原返回值。
Promise對象有以下幾種狀態:
•pending: 初始狀態, 非 fulfilled 或 rejected。
•fulfilled: 成功的操作。
•rejected: 失敗的操作。
pending狀態的promise對象既可轉換為帶著一個成功值的fulfilled 狀態,也可變為帶著一個失敗信息的 rejected 狀態。當狀態發生轉換時,promise.then綁定的方法(函數句柄)就會被調用。(當綁定方法時,如果 promise對象已經處於 fulfilled 或 rejected 狀態,那麼相應的方法將會被立刻調用, 所以在異步操作的完成情況和它的綁定方法之間不存在競爭條件。)
更多關於Promise的描述和示例可以參考MDN的Promise條目,或者MSDN的Promise條目。
嘗試使用Promise解決我們的問題
基於以上對Promise的了解,我們知道可以使用它來解決多層回調嵌套後的代碼蠢笨難以維護的問題。關於Promise的語法和參數上面給出的兩個鏈接已經說的很清楚了,這裡不重復,直接上代碼。
我們先來嘗試一個比較簡單的情況,只執行一次延時和回調:
new Promise(function(res, rej) { console.log(Date.now() + " start setTimeout"); setTimeout(res, 2000); }).then(function() { console.log(Date.now() + " timeout call back"); });
看起來和MSDN裡的示例也沒什麼區別,執行結果如下:
$ 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函數以及then函數的onFulfilled(或者叫onCompleted)回調函數的返回值裡面。
首先明確的一點是,then函數會返回一個新的Promise變量,你可以再次調用這個新的Promise變量的then函數,像這樣:
new Promise(...).then(...) .then(...).then(...).then(...)...
而then函數返回的是什麼樣的Promies,取決於onFulfilled回調的返回值。
事實上,onFulfilled可以返回一個普通的變量,也可以是另一個Promise變量。
如果onFulfilled返回的是一個普通的值,那麼then函數會返回一個默認的Promise變量。執行這個Promise的then函數會使Promise立即被滿足,執行onFulfilled函數,而這個onFulfilled的入參,即是上一個onFulfilled的返回值。
而如果onFulfilled返回的是一個Promise變量,那個這個Promise變量就會作為then函數的返回值。
關於then函數和onFulfilled函數的返回值的這一系列設定,MDN和MSDN上的文檔都沒有明確的正面描述,至於ES6官方文檔ECMAScript 2015 (6th Edition, ECMA-262)。。。我的水平有限實在看不懂,如果哪位高手能解釋清楚官方文檔裡面對著兩個返回值的描述,請一定留言指教!!!
所以以上為我的自由發揮,語言組織的有點拗口,上代碼看一下大家就明白了。
首先是返回普通變量的情況:
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); });
以上代碼執行結果為:
$ node promisTest.js 1450277122125 start setTimeout 1 1450277124129 timeout 1 call back 1450277124129 last onFulfilled return 1024
有點意思對不對,但這不是關鍵。關鍵是onFulfilled函數返回一個Promise變量可以使我們很方便的連續調用多個異步過程。比如我們可以這樣來嘗試連續做兩個延時操作:
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"); });
執行結果如下:
$ 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
可以看到,多個延時的回調函數被有序的排列下來,並沒有出現喜聞樂見的金字塔狀結構。雖然代碼裡面調用的都是異步過程,但是看起來就像是全部由同步過程構成的一樣。這就是Promise帶給我們的好處。
如果你有把啰嗦的代碼提煉成單獨函數的好習慣,那就更加畫美不看了:
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
接下來我們可以再繼續研究一下onFulfilled函數傳入入參的問題。
我們已經知道,如果上一個onFulfilled函數返回了一個普通的值,那麼這個值為作為這個onFulfilled函數的入參;那麼如果上一個onFulfilled返回了一個Promise變量,這個onFulfilled的入參又來自哪裡?
答案是,這個onFulfilled函數的入參,是上一個Promise中調用resolve函數時傳入的值。
跳躍的有點大一時間無法接受對不對,讓我們來好好縷一縷。
首先,Promise.resolve這個函數是什麼,用MDN上面文鄒鄒的說法
用成功值value解決一個Promise對象。如果該value為可繼續的(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這個函數而言,無論成功還是失敗,它都會調用callback這個回調函數,所以這個回調接受兩個入參,即失敗時的異常描述err和成功時的返回結果data。
那麼假如我們用Promise來重構這個讀取文件的例子,我們應該怎麼寫呢?
首先是封裝fs.readFile函數:
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; } );
想象一下,在其他語言的讀取文件的同步調用接口的裡面,文件的內容通常是放在哪裡?函數返回值對不對!答案出來了,這個resolve的入參是什麼?就是異步調用成功情況下的返回值。
有了這個概念之後,我們就不難理解“onFulfilled函數的入參,是上一個Promise中調用resolve函數時傳入的值”這件事了。因為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解決多層異步調用的簡單學習心得就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支持。