前言
第一次接觸到Promise這個東西应役,是2012年微軟發(fā)布Windows8操作系統(tǒng)后抱著作死好奇的心態(tài)研究用html5寫Metro應(yīng)用的時候库倘。當時配合html5提供的WinJS庫里面的異步接口全都是Promise形式,這對那時候剛剛畢業(yè)一點javascript基礎(chǔ)都沒有的我而言簡直就是天書。我當時想的是,微軟又在腦洞大開的瞎搗鼓了各谚。
結(jié)果沒想到,到了2015年到千,Promise居然寫進ES6標準里面了昌渤。而且一項調(diào)查顯示,js程序員們用這玩意用的還挺high父阻。
諷刺的是愈涩,作為早在2012年就在Metro應(yīng)用開發(fā)接口里面廣泛使用Promise的微軟,其自家瀏覽器IE直到2015年壽終正寢了都還不支持Promise加矛,看來微軟不是沒有這個技術(shù)履婉,而是真的對IE放棄治療了。斟览。毁腿。
現(xiàn)在回想起來,當時看到Promise最頭疼的,就是初學者看起來匪夷所思已烤,也是最被js程序員廣為稱道的特性:then
函數(shù)調(diào)用鏈鸠窗。
then
函數(shù)調(diào)用鏈,從其本質(zhì)上而言胯究,就是對多個異步過程的依次調(diào)用稍计,本文就從這一點著手,對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);
}
由于嵌套了多層回調(diào)結(jié)構(gòu)怎虫,這里形成了一個典型的金字塔結(jié)構(gòu)。如果業(yè)務(wù)邏輯再復雜一些困介,就會變成令人聞風喪膽的回調(diào)地獄大审。
如果意識比較好,知道提煉出簡單的函數(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對象代表著一個還未完成,但預(yù)期將來會完成的操作漾狼。
Promise 對象是一個返回值的代理重慢,這個返回值在promise對象創(chuàng)建時未必已知。它允許你為異步操作的成功或失敗指定處理方法逊躁。 這使得異步方法可以像同步方法那樣返回值:異步方法會返回一個包含了原返回值的 promise 對象來替代原返回值似踱。
Promise對象有以下幾種狀態(tài):
- pending: 初始狀態(tài), 非 fulfilled 或 rejected。
- fulfilled: 成功的操作。
- rejected: 失敗的操作核芽。
pending狀態(tài)的promise對象既可轉(zhuǎn)換為帶著一個成功值的fulfilled 狀態(tài)囚戚,也可變?yōu)閹е粋€失敗信息的 rejected 狀態(tài)。當狀態(tài)發(fā)生轉(zhuǎn)換時轧简,promise.then綁定的方法(函數(shù)句柄)就會被調(diào)用驰坊。(當綁定方法時,如果 promise對象已經(jīng)處于 fulfilled 或 rejected 狀態(tài)哮独,那么相應(yīng)的方法將會被立刻調(diào)用拳芙, 所以在異步操作的完成情況和它的綁定方法之間不存在競爭條件。)
更多關(guān)于Promise的描述和示例可以參考MDN的Promise條目借嗽,或者MSDN的Promise條目态鳖。
嘗試使用Promise解決我們的問題
基于以上對Promise的了解,我們知道可以使用它來解決多層回調(diào)嵌套后的代碼蠢笨難以維護的問題恶导。關(guān)于Promise的語法和參數(shù)上面給出的兩個鏈接已經(jīng)說的很清楚了浆竭,這里不重復,直接上代碼惨寿。
我們先來嘗試一個比較簡單的情況邦泄,只執(zhí)行一次延時和回調(diào):
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í)行結(jié)果如下:
$ 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
)回調(diào)函數(shù)的返回值里面晕换。
首先明確的一點是午乓,then
函數(shù)會返回一個新的Promise變量,你可以再次調(diào)用這個新的Promise變量的then
函數(shù)闸准,像這樣:
new Promise(...).then(...)
.then(...).then(...).then(...)...
而then
函數(shù)返回的是什么樣的Promies益愈,取決于onFulfilled
回調(diào)的返回值。
事實上夷家,onFulfilled
可以返回一個普通的變量蒸其,也可以是另一個Promise變量。
如果onFulfilled
返回的是一個普通的值库快,那么then
函數(shù)會返回一個默認的Promise變量摸袁。執(zhí)行這個Promise的then
函數(shù)會使Promise立即被滿足,執(zhí)行onFulfilled
函數(shù)缺谴,而這個onFulfilled
的入?yún)⒌蹋词巧弦粋€onFulfilled
的返回值耳鸯。
而如果onFulfilled
返回的是一個Promise變量,那個這個Promise變量就會作為then
函數(shù)的返回值膀曾。
關(guān)于then
函數(shù)和onFulfilled
函數(shù)的返回值的這一系列設(shè)定县爬,MDN和MSDN上的文檔都沒有明確的正面描述,至于ES6官方文檔ECMAScript 2015 (6th Edition, ECMA-262)添谊。财喳。。我的水平有限實在看不懂斩狱,如果哪位高手能解釋清楚官方文檔里面對著兩個返回值的描述耳高,請一定留言指教!K弧泌枪!
所以以上為我的自由發(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í)行結(jié)果為:
$ node promisTest.js
1450277122125 start setTimeout 1
1450277124129 timeout 1 call back
1450277124129 last onFulfilled return 1024
有點意思對不對,但這不是關(guān)鍵继薛。關(guān)鍵是onFulfilled
函數(shù)返回一個Promise變量可以使我們很方便的連續(xù)調(diào)用多個異步過程修壕。比如我們可以這樣來嘗試連續(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í)行結(jié)果如下:
$ 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
可以看到遏考,多個延時的回調(diào)函數(shù)被有序的排列下來慈鸠,并沒有出現(xiàn)喜聞樂見的金字塔狀結(jié)構(gòu)。雖然代碼里面調(diào)用的都是異步過程灌具,但是看起來就像是全部由同步過程構(gòu)成的一樣青团。這就是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)⒂謥碜阅睦铮?/p>
答案是瘪松,這個onFulfilled
函數(shù)的入?yún)ⅲ巧弦粋€Promise中調(diào)用resolve
函數(shù)時傳入的值锨阿。
跳躍的有點大一時間無法接受對不對宵睦,讓我們來好好縷一縷。
首先墅诡,Promise.resolve這個函數(shù)是什么壳嚎,用MDN上面文鄒鄒的說法
用成功值value解決一個Promise對象。如果該value為可繼續(xù)的(thenable,即帶有then方法)烟馅,返回的Promise對象會“跟隨”這個value说庭,采用這個value的最終狀態(tài);否則的話返回值會用這個value滿足(fullfil)返回的Promise對象郑趁。
簡而言之刊驴,這就是異步調(diào)用成功情況下的回調(diào)。
我們來看看普通的異步接口中寡润,成功情況的回調(diào)是什么樣的捆憎,就拿nodejs的上的fs.readFile(file[, options], callback)
來說,它的典型調(diào)用例子如下
fs.readFile('/etc/passwd', function (err, data) {
if (err) throw err;
console.log(data);
});
因為對于fs.readFile
這個函數(shù)而言梭纹,無論成功還是失敗躲惰,它都會調(diào)用callback這個回調(diào)函數(shù),所以這個回調(diào)接受兩個入?yún)⒈涑椋词r的異常描述err
和成功時的返回結(jié)果data
础拨。
那么假如我們用Promise來重構(gòu)這個讀取文件的例子,我們應(yīng)該怎么寫呢瞬沦?
首先是封裝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);
}
});
});
}
其次是調(diào)用:
readFile('theFile.txt').then(
function(data) {
console.log(data);
},
function(err) {
throw err;
}
);
想象一下太伊,在其他語言的讀取文件的同步調(diào)用接口的里面,文件的內(nèi)容通常是放在哪里逛钻?函數(shù)返回值對不對僚焦!答案出來了,這個resolve
的入?yún)⑹鞘裁词锒唬烤褪钱惒秸{(diào)用成功情況下的返回值芳悲。
有了這個概念之后,我們就不難理解“onFulfilled
函數(shù)的入?yún)⒈呃ぃ巧弦粋€Promise中調(diào)用resolve
函數(shù)時傳入的值”這件事了名扛。因為onFulfilled
的任務(wù),就是對上一個異步調(diào)用成功后的結(jié)果做處理的茧痒。
哎終于理順了肮韧。。旺订。
總結(jié)
下面請允許我用一段代碼對本文講解到的要點進行總結(jié):
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