在JavaScript的世界中荣茫,所有代碼都是單線程執(zhí)行的。
由于這個“缺陷”睁本,導致JavaScript的所有網(wǎng)絡操作尿庐,瀏覽器事件,都必須是異步執(zhí)行呢堰。異步執(zhí)行可以用回調函數(shù)實現(xiàn):
functioncallback(){console.log('Done');}console.log('before setTimeout()');setTimeout(callback,1000);// 1秒鐘后調用callback函數(shù)console.log('after setTimeout()');
觀察上述代碼執(zhí)行抄瑟,在Chrome的控制臺輸出可以看到:
before setTimeout()
after setTimeout()
(等待1秒后)
Done
可見,異步操作會在將來的某個時間點觸發(fā)一個函數(shù)調用枉疼。
AJAX就是典型的異步操作皮假。以上一節(jié)的代碼為例:
request.onreadystatechange =function(){if(request.readyState ===4) {if(request.status ===200) {returnsuccess(request.responseText);? ? ? ? }else{returnfail(request.status);? ? ? ? }? ? }}
把回調函數(shù)success(request.responseText)和fail(request.status)寫到一個AJAX操作里很正常,但是不好看骂维,而且不利于代碼復用惹资。
有沒有更好的寫法?比如寫成這樣:
varajax = ajaxGet('http://...');ajax.ifSuccess(success)? ? .ifFail(fail);
這種鏈式寫法的好處在于航闺,先統(tǒng)一執(zhí)行AJAX邏輯褪测,不關心如何處理結果,然后潦刃,根據(jù)結果是成功還是失敗汰扭,在將來的某個時候調用success函數(shù)或fail函數(shù)。
古人云:“君子一諾千金”福铅,這種“承諾將來會執(zhí)行”的對象在JavaScript中稱為Promise對象萝毛。
Promise有各種開源實現(xiàn),在ES6中被統(tǒng)一規(guī)范滑黔,由瀏覽器直接支持笆包。先測試一下你的瀏覽器是否支持Promise:
'use strict';
new Promise(function () {});
?Run
支持Promise!
我們先看一個最簡單的Promise例子:生成一個0-2之間的隨機數(shù),如果小于1略荡,則等待一段時間后返回成功庵佣,否則返回失敗:
functiontest(resolve, reject){vartimeOut = Math.random() *2;? ? log('set timeout to: '+ timeOut +' seconds.');? ? setTimeout(function(){if(timeOut <1) {? ? ? ? ? ? log('call resolve()...');? ? ? ? ? ? resolve('200 OK');? ? ? ? }else{? ? ? ? ? ? log('call reject()...');? ? ? ? ? ? reject('timeout in '+ timeOut +' seconds.');? ? ? ? }? ? }, timeOut *1000);}
這個test()函數(shù)有兩個參數(shù)汛兜,這兩個參數(shù)都是函數(shù)巴粪,如果執(zhí)行成功,我們將調用resolve('200 OK')粥谬,如果執(zhí)行失敗肛根,我們將調用reject('timeout in ' + timeOut + ' seconds.')÷┎撸可以看出派哲,test()函數(shù)只關心自身的邏輯,并不關心具體的resolve和reject將如何處理結果掺喻。
有了執(zhí)行函數(shù)芭届,我們就可以用一個Promise對象來執(zhí)行它储矩,并在將來某個時刻獲得成功或失敗的結果:
varp1 =newPromise(test);varp2 = p1.then(function(result){console.log('成功:'+ result);});varp3 = p2.catch(function(reason){console.log('失敗:'+ reason);});
變量p1是一個Promise對象褂乍,它負責執(zhí)行test函數(shù)持隧。由于test函數(shù)在內部是異步執(zhí)行的,當test函數(shù)執(zhí)行成功時逃片,我們告訴Promise對象:
// 如果成功屡拨,執(zhí)行這個函數(shù):p1.then(function(result){console.log('成功:'+ result);});
當test函數(shù)執(zhí)行失敗時,我們告訴Promise對象:
p2.catch(function(reason){console.log('失斕馑小:'+ reason);});
Promise對象可以串聯(lián)起來洁仗,所以上述代碼可以簡化為:
newPromise(test).then(function(result){console.log('成功:'+ result);}).catch(function(reason){console.log('失敗:'+ reason);});
實際測試一下性锭,看看Promise是如何異步執(zhí)行的:
'use strict';
// 清除log:
var logging = document.getElementById('test-promise-log');
while (logging.children.length > 1) {
? ? logging.removeChild(logging.children[logging.children.length - 1]);
}
// 輸出log到頁面:
function log(s) {
? ? var p = document.createElement('p');
? ? p.innerHTML = s;
? ? logging.appendChild(p);
}
?Run
(no?output)
Log:
start new Promise...
set timeout to: 0.08028750076878355 seconds.
call resolve()...
Done: 200 OK
可見Promise最大的好處是在異步執(zhí)行的流程中赠潦,把執(zhí)行代碼和處理結果的代碼清晰地分離了:
Promise還可以做更多的事情,比如草冈,有若干個異步任務她奥,需要先做任務1,如果成功后再做任務2怎棱,任何任務失敗則不再繼續(xù)并執(zhí)行錯誤處理函數(shù)哩俭。
要串行執(zhí)行這樣的異步任務,不用Promise需要寫一層一層的嵌套代碼拳恋。有了Promise凡资,我們只需要簡單地寫:
job1.then(job2).then(job3).catch(handleError);
其中,job1谬运、job2和job3都是Promise對象隙赁。
下面的例子演示了如何串行執(zhí)行一系列需要異步計算獲得結果的任務:
'use strict';
var logging = document.getElementById('test-promise2-log');
while (logging.children.length > 1) {
? ? logging.removeChild(logging.children[logging.children.length - 1]);
}
function log(s) {
? ? var p = document.createElement('p');
? ? p.innerHTML = s;
? ? logging.appendChild(p);
}
?Run
(no?output)
Log:
start new Promise...
calculating 123 x 123...
calculating 15129 + 15129...
calculating 30258 x 30258...
calculating 915546564 + 915546564...
Got value: 1831093128
setTimeout可以看成一個模擬網(wǎng)絡等異步執(zhí)行的函數(shù)。現(xiàn)在梆暖,我們把上一節(jié)的AJAX異步執(zhí)行函數(shù)轉換為Promise對象伞访,看看用Promise如何簡化異步處理:
'use strict';
// ajax函數(shù)將返回Promise對象:
function ajax(method, url, data) {
? ? var request = new XMLHttpRequest();
? ? return new Promise(function (resolve, reject) {
? ? ? ? request.onreadystatechange = function () {
? ? ? ? ? ? if (request.readyState === 4) {
? ? ? ? ? ? ? ? if (request.status === 200) {
? ? ? ? ? ? ? ? ? ? resolve(request.responseText);
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? reject(request.status);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? };
? ? ? ? request.open(method, url);
? ? ? ? request.send(data);
? ? });
}
?Run
(no?output)
{"categories":[{"id":"0013738748415562fee26e070fa4664ad926c8e30146c67000","name":"編程","tag":"tech","display_order":0,"description":"","created_at":1373874841556,"updated_at":1429763779958,"version":5},{"id":"0013738748248885ddf38d8cd1b4803aa74bcda32f853fd000","name":"讀書","tag":"other","display_order":1,"description":"","created_at":1373874824888,"updated_at":1429763779974,"version":5}]}
除了串行執(zhí)行若干異步任務外,Promise還可以并行執(zhí)行異步任務轰驳。
試想一個頁面聊天系統(tǒng)厚掷,我們需要從兩個不同的URL分別獲得用戶的個人信息和好友列表,這兩個任務是可以并行執(zhí)行的级解,用Promise.all()實現(xiàn)如下:
varp1 =newPromise(function(resolve, reject){setTimeout(resolve,500,'P1');});varp2 =newPromise(function(resolve, reject){setTimeout(resolve,600,'P2');});// 同時執(zhí)行p1和p2冒黑,并在它們都完成后執(zhí)行then:Promise.all([p1, p2]).then(function(results){console.log(results);// 獲得一個Array: ['P1', 'P2']});
有些時候,多個異步任務是為了容錯蠕趁。比如薛闪,同時向兩個URL讀取用戶的個人信息,只需要獲得先返回的結果即可俺陋。這種情況下豁延,用Promise.race()實現(xiàn):
varp1 =newPromise(function(resolve, reject){setTimeout(resolve,500,'P1');});varp2 =newPromise(function(resolve, reject){setTimeout(resolve,600,'P2');});Promise.race([p1, p2]).then(function(result){console.log(result);// 'P1'});
由于p1執(zhí)行較快,Promise的then()將獲得結果'P1'腊状。p2仍在繼續(xù)執(zhí)行诱咏,但執(zhí)行結果將被丟棄。
如果我們組合使用Promise缴挖,就可以把很多異步任務以并行和串行的方式組合起來執(zhí)行袋狞。