第一次接觸到Promise這個東西,是2012年微軟發佈Windows8操做系統後抱着做死好奇的心態研究用html5寫Metro應用的時候。當時配合html5提供的WinJS庫裏面的異步接口全都是Promise形式,這對那時候剛剛畢業一點javascript基礎都沒有的我而言簡直就是天書。我當時想的是,微軟又在腦洞大開的瞎搗鼓了。javascript
結果沒想到,到了2015年,Promise竟然寫進ES6標準裏面了。並且一項調查顯示,js程序員們用這玩意用的還挺high。html
諷刺的是,做爲早在2012年就在Metro應用開發接口裏面普遍使用Promise的微軟,其自家瀏覽器IE直到2015年壽終正寢了都還不支持Promise,看來微軟不是沒有這個技術,而是真的對IE放棄治療了。。。html5
如今回想起來,當時看到Promise最頭疼的,就是初學者看起來匪夷所思,也是最被js程序員廣爲稱道的特性:then
函數調用鏈。java
then
函數調用鏈,從其本質上而言,就是對多個異步過程的依次調用,本文就從這一點着手,對Promise這一特性進行研究和學習。node
考慮以下場景,函數延時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);
}
複製代碼
因爲嵌套了多層回調結構,這裏造成了一個典型的金字塔結構。若是業務邏輯再複雜一些,就會變成使人聞風喪膽的回調地獄。編程
若是意識比較好,知道提煉出簡單的函數,那麼代碼差很少是這個樣子:promise
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這個東西。bash
這裏請容許我引用MDN對Promise的描述:
Promise 對象用於延遲(deferred) 計算和異步(asynchronous ) 計算.。一個Promise對象表明着一個還未完成,但預期未來會完成的操做。
Promise 對象是一個返回值的代理,這個返回值在promise對象建立時未必已知。它容許你爲異步操做的成功或失敗指定處理方法。 這使得異步方法能夠像同步方法那樣返回值:異步方法會返回一個包含了原返回值的 promise 對象來替代原返回值。
Promise對象有如下幾種狀態:
pending狀態的promise對象既可轉換爲帶着一個成功值的fulfilled 狀態,也可變爲帶着一個失敗信息的 rejected 狀態。當狀態發生轉換時,promise.then綁定的方法(函數句柄)就會被調用。(當綁定方法時,若是 promise對象已經處於 fulfilled 或 rejected 狀態,那麼相應的方法將會被馬上調用, 因此在異步操做的完成狀況和它的綁定方法之間不存在競爭條件。)
更多關於Promise的描述和示例能夠參考MDN的Promise條目,或者MSDN的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,採用這個value的最終狀態;不然的話返回值會用這個value知足(fullfil)返回的Promise對象。
複製代碼
簡而言之,這就是異步調用成功狀況下的回調。
咱們來看看普通的異步接口中,成功狀況的回調是什麼樣的,就拿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
複製代碼