Javascript非同步程式設計模型進化,從promise到generator
Javascript語言是單執行緒的,沒有複雜的同步互斥;但是,這並沒有限制它的使用範圍;相反,藉助於Node,Javascript已經在某些場景下具備通吃前後端的能力了。近幾年,多執行緒同步IO的模式已經在和單執行緒非同步IO的模式的對決中敗下陣來,Node也因此得名。接下來我們深入介紹一下Javascript的殺手鐗,非同步程式設計的發展歷程。
讓我們假設一個應用場景:一篇文章有10個章節,章節的資料是通過XHR非同步請求的,章節必須按順序顯示。我們從這個問題出發,逐步探求從粗糙到優雅的解決方案。
1.回憶往昔之callback
在那個年代,javascript僅限於前端的簡單事件處理,這是非同步程式設計的最基本模式了。 比如監聽dom事件,在dom事件發生時觸發相應的回撥。 javascript
element.addEventListener('click',function(){
//response to user click
});
比如通過定時器執行非同步任務。
setTimeout(function(){
//do something 1s later
}, 1000);
但是這種模式註定無法處理複雜的業務邏輯的。假設有N個非同步任務,每一個任務必須在上一個任務完成後觸發,於是就有了如下的程式碼,這就產生了回撥黑洞。
doAsyncJob1(function(){
doAsyncJob2(function(){
doAsyncJob3(function(){
doAsyncJob4(function(){
//Black hole
});
})
});
});
2.活在當下之promise
針對上文的回撥黑洞問題,有人提出了開源的promise/A+規範,具體規範見如下地址:https://promisesaplus.com/。promise代表了一個非同步操作的結果,其狀態必須符合下面幾個要求:
一個Promise必須處在其中之一的狀態:pending, fulfilled 或 rejected.
如果是pending狀態,則promise可以轉換到fulfilled或rejected狀態。
如果是fulfilled狀態,則promise不能轉換成任何其它狀態。
如果是rejected狀態,則promise不能轉換成任何其它狀態。
2.1 promise基本用法
promise有then方法,可以新增在非同步操作到達fulfilled狀態和rejected狀態的處理函式。
promise.then(successHandler,failedHandler);
而then方法同時也會返回一個promise物件,這樣我們就可以鏈式處理了。
promise.then(successHandler,failedHandler).then().then();
MDN上的一張圖,比較清晰的描述了Pomise各個狀態之間的轉換。
假設上文中的doAsyncJob都返回一個promise物件,那我們看看如何用promise處理回撥黑洞:
doAsyncJob1().then(function(){
return doAsyncJob2();;
}).then(function(){
return doAsyncJob3();
}).then(function(){
return doAsyncJob4();
}).then(//......);
這種程式設計方式是不是清爽多了。我們最經常使用的jQuery已經實現了promise規範,在呼叫$.ajax時可以寫成這樣了:
var options = {type:'GET',url:'the-url-to-get-data'};
$.ajax(options).then(function(data){
//success handler
},function(data){
//failed handler
});
我們可以使用ES6的Promise的建構函式生成自己的promise物件,Promise建構函式的引數為一個函式,該函式接收兩個函式(resolve,reject)作為引數,並在成功時呼叫resolve,失敗時呼叫reject。如下程式碼生成一個擁有隨機結果的promise。
var RandomPromiseJob = function(){
return new Promise(function(resolve,reject){
var res = Math.round(Math.random()*10)%2;
setTimeout(function(){
if(res){
resolve(res);
}else{
reject(res);
}
}, 1000)
});
}
RandomPromiseJob().then(function(data){
console.log('success');
},function(data){
console.log('failed');
});
jsfiddle演示地址:http://jsfiddle.net/panrq4t7/
promise錯誤處理也十分靈活,在promise建構函式中發生異常時,會自動設定promise的狀態為rejected,從而觸發相應的函式。
new Promise(function(resolve,reject){
resolve(JSON.parse('I am not json'));
}).then(undefined,function(data){
console.log(data.message);
});
其中then(undefined,function(data)可以簡寫為catch。
new Promise(function(resolve,reject){
resolve(JSON.parse('I am not json'));
}).catch(function(data){
console.log(data.message);
});
jsfiddle演示地址:http://jsfiddle.net/x696ysv2/
2.2 一個更復雜的例子
promise的功能絕不僅限於上文這種小打小鬧的應用。對於篇頭提到的一篇文章10個章節非同步請求,順序展示的問題,如果使用回撥處理章節之間的依賴邏輯,顯然會產生回撥黑洞; 而使用promise模式,則程式碼形式優雅而且邏輯清晰。假設我們有一個包含10個章節內容的陣列,並有一個返回promise物件的getChaper函式:
var chapterStrs = [
'chapter1','chapter2','chapter3','chapter4','chapter5',
'chapter6','chapter7','chapter8','chapter9','chapter10',
];
var getChapter = function(chapterStr) {
return get('<p>' + chapterStr + '</p>', Math.round(Math.random()*2));
};
下面我們探討一下如何優雅高效的使用promise處理這個問題。
(1). 順序promise
順序promise主要是通過對promise的then方法的鏈式呼叫產生的。
//按順序請求章節資料並展示
chapterStrs.reduce(function(sequence, chapterStr) {
return sequence.then(function() {
return getChapter(chapterStr);
}).then(function(chapter) {
addToPage(chapter);
});
}, Promise.resolve());
這種方法有一個問題,XHR請求是序列的,沒有充分利用瀏覽器的並行性。網路請求timeline和顯示效果圖如下:
檢視jsfiddle演示程式碼: http://jsfiddle.net/81k9nv6x/1/
(2). 併發promise,一次性
Promise類有一個all方法,其接受一個promise陣列:
Promise.all([promise1,promise2,...,promise10]).then(function(){});
只有promise陣列中的promise全部兌現,才會呼叫then方法。使用Promise.all,我們可以併發性的進行網路請求,並在所有請求返回後在集中進行資料展示。
//併發請求章節資料,一次性按順序展示章節
Promise.all(chapterStrs.map(getChapter)).then(function(chapters){
chapters.forEach(function(chapter){
addToPage(chapter);
});
});
這種方法也有一個問題,要等到所有資料載入完成後,才會一次性展示全部章節。效果圖如下:
檢視jsfiddle演示程式碼:http://jsfiddle.net/7ops845a/
(3). 併發promise,漸進式
其實,我們可以做到併發的請求資料,儘快展示滿足順序條件的章節:即前面的章節展示後就可以展示當前章節,而不用等待後續章節的網路請求。基本思路是:先建立一批並行的promise,然後通過鏈式呼叫then方法控制展示順序。
chapterStrs.map(getChapter).reduce(function(sequence, chapterStrPromise) {
return sequence.then(function(){
return chapterStrPromise;
}).then(function(chapter){
addToPage(chapter);
});
}, Promise.resolve());
效果如下:
檢視jsfiddle演示程式碼:http://jsfiddle.net/fuog1ejg/
這三種模式基本上概括了使用Pormise控制併發的方式,你可以根據業務需求,確定各個任務之間的依賴關係,從而做出選擇。
2.3 promise的實現
ES6中已經實現了promise規範,在新版的瀏覽器和node中我們可以放心使用了。對於ES5及其以下版本,我們可以藉助第三方庫實現,q(https://github.com/kriskowal/q)是一個非常優秀的實現,angular使用的就是它,你可以放心使用。下一篇文章準備實現一個自己的promise。
3.憧憬未來之generater
非同步程式設計的一種解決方案叫做"協程"(coroutine),意思是多個執行緒互相協作,完成非同步任務。隨著ES6中對協程的支援,這種方案也逐漸進入人們的視野。Generator函式是協程在 ES6 的實現.
3.1 Generator三大基本特性
讓我們先從三個方面瞭解generator。
(1) 控制權移交
在普通函式名前面加*號就可以生成generator函式,該函式返回一個指標,每一次呼叫next函式,就會移動該指標到下一個yield處,直到函式結尾。通過next函式就可以控制generator函式的執行。如下所示:
function *gen(){
yield 'I';
yield 'love';
yield 'Javascript';
}
var g = gen();
console.log(g.next().value); //I
console.log(g.next().value); //love
console.log(g.next().value); //Javascript
next函式返回一個物件{value:'love',done:false},其中value表示yield返回值,done表示generator函式是否執行完成。這樣寫有點low?試試這種語法。
for(var v of gen()){
console.log(v);
}
(2) 分步資料傳遞
next()函式中可以傳遞引數,作為yield的返回值,傳遞到函式體內部。這裡有點tricky,next引數作為上一次執行yeild的返回值。理解“上一次”很重要。
function* gen(x){
var y = yield x + 1;
yield y + 2;
return 1;
}
var g = gen(1);
console.log(g.next()) // { value: 2, done: false }
console.log(g.next(2)) // { value: 4, done: true }
console.log(g.next()); //{ value: 1, done: true }
比如這裡的g.next(2),引數2為上一步yield x + 1 的返回值賦給y,從而我們就可以在接下來的程式碼中使用。這就是generator資料傳遞的基本方法了。
(3) 異常傳遞
通過generator函式返回的指標,我們可以向函式內部傳遞異常,這也使得非同步任務的異常處理機制得到保證。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
console.log(g.next()); //{ value: 3, done: false }
g.throw('error'); //error
3.2 用generator實現非同步操作
仍然使用本文中的getChapter方法,該方法返回一個promise,我們看一下如何使用generator處理非同步回撥。gen方法在執行到yield指令時返回的result.value是promise物件,然後我們通過next方法將promise的結果返回到gen函式中,作為addToPage的引數。
function *gen(){
var result = yield getChapter('I love Javascript');
addToPage(result);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
g.next(data);
});
gen函式的程式碼,和普通同步函式幾乎沒有區別,只是多了一條yield指令。
jsfiddle地址如下:http://jsfiddle.net/fhnc07rq/3/
3.3 使用co進行規範化非同步操作
雖然gen函式本身非常乾淨,只需要一條yield指令即可實現非同步操作。但是我卻需要一堆程式碼,用於控制gen函式、向gen函式傳遞引數。有沒有更規範的方式呢?其實只需要將這些操作進行封裝,co庫為我們做了這些(https://github.com/tj/co)。那麼我們用generator和co實現上文的逐步載入10個章節資料的操作。
function *gen(){
for(var i=0;i<chapterStrs.length;i++){
addToPage(yield getChapter(chapterStrs[i]));
}
}
co(gen);
jsfiddle演示地址:http://jsfiddle.net/0hvtL6e9/
這種方法的效果類似於上文中提到“順序promise”,我們能不能實現上文的“併發promise,漸進式”呢?程式碼如下:
function *gen(){
var charperPromises = chapterStrs.map(getChapter);
for(var i=0;i<charperPromises.length;i++){
addToPage(yield charperPromises[i]);
}
}
co(gen);
jsfiddle演示地址: http://jsfiddle.net/gr6n3azz/1/
經歷過複雜性才能達到簡單性。我們從最開始的回撥黑洞到最終的generator,越來越複雜也越來越簡單。
本文同時發表在我的部落格積木村の研究所 :http://foio.github.io/javascript-asyn-pattern/
相關文章
- [前端漫談_2] 從 Dva 的 Effect 到 Generator + Promise 實現非同步程式設計前端Promise非同步程式設計
- [前端怪談_2] 從 Dva 的 Effect 到 Generator + Promise 實現非同步程式設計前端Promise非同步程式設計
- JavaScript非同步程式設計:Generator與AsyncJavaScript非同步程式設計
- JS非同步程式設計 (2) – Promise、Generator、async/awaitJS非同步程式設計PromiseAI
- 前端- JavaScript非同步程式設計Promise前端JavaScript非同步程式設計Promise
- JavaScript非同步程式設計助手:Promise模式JavaScript非同步程式設計Promise模式
- JavaScript非同步程式設計的Promise模式JavaScript非同步程式設計Promise模式
- 深入解析JavaScript非同步程式設計:Generator與AsyncJavaScript非同步程式設計
- JavaScript非同步程式設計史:回撥函式到Promise到Async/AwaitJavaScript非同步程式設計函式PromiseAI
- JavaScript非同步程式設計–Generator函式、async、awaitJavaScript非同步程式設計函式AI
- 非同步程式設計---Promise非同步程式設計Promise
- javascript promise程式設計JavaScriptPromise程式設計
- JS非同步程式設計之GeneratorJS非同步程式設計
- JavaScript深入淺出非同步程式設計二、promise原理JavaScript非同步程式設計Promise
- JavaScript非同步程式設計(1)- ECMAScript 6的Promise物件JavaScript非同步程式設計Promise物件
- 非同步程式設計解決方案全集—promise、generator+co、async+await非同步程式設計PromiseAI
- Javascript中常見的非同步程式設計模型JavaScript非同步程式設計模型
- 進化感悟:從程式設計小白到應用開發者程式設計
- JS非同步程式設計之PromiseJS非同步程式設計Promise
- 學習Promise非同步程式設計Promise非同步程式設計
- 非同步程式設計方案進化論非同步程式設計
- 我瞭解到的JavaScript非同步程式設計JavaScript非同步程式設計
- 從 JavaScript 到 TypeScript 5 - 路由進化JavaScriptTypeScript路由
- ES6 Promise非同步程式設計Promise非同步程式設計
- 一文徹底搞定(阻塞/非阻塞/同步/非同步)網路IO、併發程式設計模型、非同步程式設計模型的愛恨情仇非同步程式設計模型
- Javascript 非同步程式設計JavaScript非同步程式設計
- JavaScript非同步程式設計JavaScript非同步程式設計
- 【進階之路】併發程式設計(三)-非阻塞同步機制程式設計
- 非同步程式設計方案----Promise實現小解非同步程式設計Promise
- ES6 非同步程式設計之三:Generator續非同步程式設計
- FE.ES-非同步程式設計進化史非同步程式設計
- [面試專題]JS非同步之Promise,Generator,Async面試JS非同步Promise
- 【理解ES7async/await並實現】手把手進行ES6非同步程式設計:Generator + Promise = Async/AwaitAI非同步程式設計Promise
- 從 generator 的角度看 Rust 非同步程式碼Rust非同步
- 程式程式碼進化的一些思考:從物件導向到設計模式,到函數語言程式設計物件設計模式函數程式設計
- 探索Javascript非同步程式設計JavaScript非同步程式設計
- promise-java非同步程式設計解決方案PromiseJava非同步程式設計
- Promise是如何實現非同步程式設計的?Promise非同步程式設計