今天先來介紹一下Promise,然後再看看未來在Flow Control相關領域的發展狀況或可能性
Promise
其實Promise可能還有幾種不同的名稱,包含Deferred、Future等,不過Javascript標準組織CommonJS已經有Promise的提案,所以就把它統稱為Promise吧。
從Wiki上面關於Future and promises的條目可以知道,其實很早就提出這個概念...
在Javascript中,可以把Promise視為一個物件,他有三個狀態:unfulfill、fulfilled(resolved)、fail(rejected),當一個Promise物件初生成時,是在unfullfil狀態,之後他的狀態可以轉變為fulfilled(resolved)或是fail(rejected),但是狀態轉變為fulfilled(resolved)或是failed(rejected)後,他的狀態就不會再有變化。
之前看到的一些Flow Control,基本上都是用函數執行計數的方式在做執行狀態的控管。在Promise內部,則會傳遞Promise物件,只有在Promise的狀態到達fulfilled(resolved)或是failed(rejected)時,才進入下一步,並且把傳遞的訊息傳給下一步。
之前Vexed大大已經有做過jQuery以及win.js裡面實作的Promise的比較,我接下來的例子,則會使用Q這個Library。在這之前,先介紹一下基本的API。
目前主要提供Promise機制的Library,大概都跟CommonJS Promise/A+相容,所以等一下就盡量只使用這個規格提案中的API。
除了本身的狀態,Promise還會提供:then(onResolved, onRejected):當Promise的狀態改變,就會呼叫相對應的處理函數。
由於只有Promise提供then的功能,在使用Promise時,至少需要返回一個Promise物件,才有辦法使用then,通常有兩種方法可以開始:
呼叫Q.fcall(function(){}):只要把要使用的函數傳給他,fcall就會返回一個promise物件,這樣才能呼叫then
Q.fcall(function(){
return 1;
})
.then(onresolved, onrejected);//console顯示1
Q.fcall(function() {
throw "new error.";
})
.then(onresolved, onrejected);//console顯示警告:new error
function onresolved(m) {
console.log(m)
}
function onrejected(m) {
console.warn(m);
}
使用Q.defer()產生一個defferred物件,然後返回他的promise成員
(function() {
var deferred = Q.defer();
setTimeout(function() {defer.resolved(1);}, 1000);
return deferred.promise;
})()
.then(onresolved, onrejected);//一秒鐘後在console顯示1
(function() {
var deferred = Q.defer();
setTimeout(function() {defer.reject('new error');}, 1000);
return deferred.promise;
})()
.then(onresolved, onrejected);//一秒鐘後在console顯示警告:new error
function onresolved(m) {
console.log(m)
}
function onrejected(m) {
console.warn(m);
}
使用deferred.resolve(),可以把它的promise成員狀態改為fulfilled(resolved),使用deferred.reject(),則會把他的成員promise狀態改為fail(rejected)。簡單的使用方法就像這樣。再來把昨天的例子用Promise改寫:
<script src='http://code.jquery.com/jquery-1.9.1.min.js'></script>
<script src='https://raw.github.com/kriskowal/q/master/q.min.js'></script>
<button id='btn1'>start promise</button>
<div id='panel'></div>
<script>
$(function() {
$('#btn1').click(function() {
Q.all(function() {//這裡需要回傳promise陣列,所以使用Q.all
var d1 = Q.defer();
var d2 = Q.defer();
$.get('test877a.txt', function(data, status) {
if(status==='success') {
d1.resolve(data);
} else {
d1.reject('ajax error');
}
});
$.get('test877b.txt', function(data, status) {
if(status==='success') {
d2.resolve(data);
} else {
d2.reject('ajax error');
}
});
return [d1.promise, d2.promise];
}())//Q.all接收的是promise陣列不是函數,所以這裡直接執行
.then(
function(texts){
return texts.map(function(text) {
return text.toUpperCase();
});
},
function(m){console.warn(m)}
)
.then(
function(texts){
return texts.map(function(text) {
return '[['+text+']]';
});
},
function(m){console.warn(m)}
)
.done(
function(texts) {
$('#panel').html(texts.join('<br>'));
},
function(m){console.warn(m)}
);
});
});
</script>
處理非同步的狀況,需要同時等待多個Promise時需要注意,每一個Promise的狀態都需要是fulfilled(resolved)或是fail(rejected)時,才會進入下一步。這裡常常會出問題,例如$.get只接收data而沒有處理textStatus,萬一url給錯或伺服器有問題,就可能不會進入下一步,也不會提示錯誤。所以自己產生promise時,一定要讓promise物件走到這兩個狀態。
網路上討論及介紹Promise的文章很多,google一下應該就可以找到很多參考資料。
Future?
其實這裡不是要介紹Future...而是看看Flow Control的未來的其他可能性。
ECMA-262 Edition 6 (aka Harmony,不過他的歷史很火爆)預計在今年年底完成,裡面有一個新的功能,叫做Generator/Iterator。用程式來解釋會比較清楚:
var a = function() {
for(var i=0; i<10; i++) {
yield i;
}
}();
console.log(a.next());//顯示0
console.log(a.next());//顯示1
console.log(a.next());//顯示2
console.log(a.next());//顯示3
console.log(a.next());//顯示4
console.log(a.next());//顯示5
console.log(a.next());//顯示6
console.log(a.next());//顯示7
console.log(a.next());//顯示8
console.log(a.next());//顯示9
console.log(a.next());//出錯
簡單地說,函數執行到yield時,會中斷執行並返回一個Iterator,每次呼叫next()時,會返回yield回傳的結果,然後函數繼續執行到碰到下一個yield。透過send(),可以把資訊傳給函數,例如:
var a = function() {
var i;
for(i=0; i<10; i++) {
if(typeof r !== 'undefined') {
i = r;
}
var r = yield i;
}
}();
console.log(a.next());//顯示0
console.log(a.next());//顯示1
console.log(a.next());//顯示2
console.log(a.next());//顯示3
console.log(a.next());//顯示4
console.log(a.next());//顯示5
console.log(a.next());//顯示6
console.log(a.next());//顯示7
console.log(a.next());//顯示8
console.log(a.send(0));//顯示0
console.log(a.next());//顯示1
這裡的demo目前只能在Firefox執行,需要在script tag加上type="application/javascript;version=1.7"。V8目前還在努力中,就等看他什麼時候出來。
另外ECMA6的Generator,跟Mozilla Javascript 1.7的不太一樣,必須在Generator function加上*,例如:
var a = function*() {
........
}():
........
Generator到底有什麼用處?簡單地說,這是產生Iterator的好方法。常看到的例子是費氏數列,利用Generator可以產生一個無限的費氏數列Iterator,只要一直next(),就可以取得數列的下一個值。這樣就不用一次把所有結果算出來,只要計算每次需要的結果,所以也可以節省一些計算資源。
Node.js的Fibers模組,也提供了類似功能的支援,也可以用它來試試yield能做什麼...其實看起來比Javascript的Generator還強XD。例如sleep()的範例,使用Javascript的Generator有點難寫的出來...而且跑起來也沒相同效果...
<script type="application/javascript;version=1.7">
console.log(sleep(1000));//不用console.log不會動XD
function sleep(ms) {
var t1 = new Date().getTime();
var a = function () {
setTimeout(function() {
console.log(new Date().getTime()-t1);
a.close();
}, ms);
yield;
}();
return a;
}
console.log('here');
</script>
嚴格說來,Generator並不是Flow Control,他只是會影響函數的執行流程就是了。
Q與未來的承諾
如果有看過ECMA-262 Strawman:Concurrency就會發現,Promise有可能在未來成為Javascript的內建標準物件,而且目前提案的作者(Mark S. Miller, Caja的設計者),似乎就把Q拿來改一改...在Q的github wiki上,Q的作者Kris Kowal有做了一個比較:Comparing ES Harmony Concurrency Strawman。參考這兩份文章,也許可以一窺Promise在Javascript的未來。(但是未來很難說)
就目前態勢看起來,Promise有機會成為在Javascript中做Flow Control的標準方法,這樣就可以從數十種Library中解脫,專注在Promise上囉。