談談 JavaScript 異步編程
目前需求中涉及到大量的異步操作,實際的頁面越來越傾向于單頁面應用。以后可以會使用backbone、angular、knockout等框架,但是關于異步編程的問題是首先需要面對的問題。隨著node的興起,異步編程成為一個非常熱的話題。經過一段時間的學習和實踐,對異步編程的一些細節進行總結。
1.異步編程的分類
解決異步問題方法大致包括:直接回調、pub/sub模式(事件模式)、異步庫控制庫(例如async、when)、promise、Generator等。
1.1 回調函數
回調函數是常用的解決異步的方法,經常接觸和使用到,易于理解,并且在庫或函數中非常容易實現。這種也是大家接使用異步編程經常使用到的方法。
但是回調函數的方式存在如下的問題:
1. 可能形成萬惡的嵌套金字塔,代碼不易閱讀;
2. 只能對應一個回調函數,在很多場景中成為一個限制。
1.2 pub/sub模式(事件)
該模式也稱為事件模式,是回調函數的事件化,在jQuery等類庫中非常常見。
事件發布訂閱者模式本身并無同步與異步調用的問題,但是在node中,emit調用多半是伴隨事件循環而異步觸發的。該模式常用來解耦業務邏輯,事件發布者無須關注注冊的回調函數,也不用關注回調函數的個數,數據通過消息的方式可以很靈活的傳遞。
該模式的好處是:1. 便于理解;2. 不再局限于一個回調函數。
不好的地方時:1. 需要借助類庫; 2.事件與回調函數的順序很重要
var img = document.querySelect(#id); img.addEventListener('load', function() { // 圖片加載完成 ...... }); img.addEventListener('error', function() { // 出問題了 ...... });
上述代碼存在兩個問題:
a. img實際已經加載完成,此時才綁定load回調函數,結果回調不會執行,但依然希望執行該對應回調函數。
var img = document.querySelect(#id); function load() { ... }if(img.complete) { load(); } else { img.addEventListener('load', load); }
img.addEventListener('error', function() { // 出問題了 ...... });</pre>
b. 無法很好處理存在異常
結論:事件機制最適合處理同一個對象上反復發生的事情,不需要考慮當綁定回調函數之前事件發生的情況。
1.3 異步控制庫
目前的異步庫主要有Q、when.js、win.js、RSVP.js等。
這些庫的特點是代碼是線性的,可以從上到下完成書寫,符合自然習慣。
不好的地方也是風格各異,不便于閱讀,增加學習成本。
1.4 Promise
Promise翻譯成中文為承諾,個人理解是異步完成之后,就會給外部一個結果(成功或失敗),并承諾結果不再發生改變。換句話就是Promise反應了一個操作的最終返回結果值(A promise represents the eventual value returned from the single completion of an operation)。目前Promise已經引入到ES6規范里面,Chrome、firefox等高級瀏覽器已經在內部實現了該原生方法,使用起來相當方便。
下面從如下幾個方面來解析Promise的特點:
1.4.1 狀態
包含三種狀態:pending、fulfilled、rejected,三種狀態只能發生兩種轉換(從pending—>fulfilled、pending—>rejected),并且狀態的轉換僅能發生一次。
1.4.2 then方法
then方法用于指定異步事件完成之后的回調函數。
這個方法可以說是Promise的靈魂方法,該方法讓Promise充滿了魔力。有如下幾個具體表現:
a) then方法返回Promise。這樣就實現了多個異步操作的串行操作。
關于上圖中黃圈1的對value的處理是Promise里面較為復雜的一個地方,value的處理分為兩種情況:Promise對象、非Promise對象。
當value 不是Promise類型時,直接將value作為第二個Promise的resolve的參數值即可;當為Promise類型時,promise2的狀態、參數完全由value決定,可以認為promsie2完全是value的傀儡,promise2僅僅是連接不同異步的橋梁。
Promise.prototype.then = function(onFulfilled, onRejected) { return new Promise(function(resolve, reject) { //此處的Promise標注為promise2 handle({ onFulfilled: onFulfilled, onRejected: onRejected, resolve: resolve, reject: reject }) }); }function handle(deferred) { var handleFn; if(state === 'fulfilled') { handleFn = deferred.onFulfilled; } else if(state === 'rejected') { handleFn = deferred.onRejected; } var ret = handleFn(value); deferred.resolve(ret); //注意,此時的resolve是promise2的resolve } function resolve(val) { if(val && typeof val.then === 'function') { val.then(resolve); // if val為promise對象或類promise對象時,promise2的狀態完全由val決定 return; } if(callback) { // callback為指定的回調函數 callback(val); } }</pre>
b)實現了多個不同異步庫之間的轉換。
在異步中存在一個叫thenable的對象,就是指具有then方法的對象,只要一個對象對象具有then方法,就可以對其進行轉換,例如:
var deferred = $('aa.ajax'); // !!deferred.then === true var P = Promise.resolve(deferred); p.then(......)1.4.3 commonJS Promise/A規范
目前關于Promise的規范存在Promise/A和Promise/A+規范,這說明關于Promise的實現是挺復雜的。
then(fulfilledHandler, rejectedHandler, progressHandler)1.4.4 注意事項
一個Promise里面的回調函數是共享value的,在結果處理中value作為參數傳遞給相應的回調函數,如果value是對象,那就要小心不要輕易修改value的值。
var p = Promise.resolve({x: 1}); p.then(function(val) { console.log('first callback: ' + val.x++); }); p.then(function(val) { console.log('second callback: ' + val.x) })// first callback: 1 // second callback: 2</pre>
1.5 Generator
上面所有的方法均是基于回調函數來完成異步操作的,無非是對回調函數進行封裝而已。ES6里面提出了Generator,增加了解決異步操作的途徑,不再依據回調函數來完成。
Generator最大的特點就是可以實現函數的暫停、重啟,這個特性非常有利于解決異步操作。將Generator的暫停與promise的異常處理結合起來,可以比較優雅地解決異步編程問題。具體實現參考:Kyle Simpson
2. 異步編程存在的問題
2.1 異常處理
a) 異步事件包括兩個環節:發出異步請求、結果處理,這兩個環節通過event loop來連接起來。那么try catch來進行異常捕獲的時候就需要分來捕獲。
try { asyncEvent(callback); } catch(err) { ...... }上述代碼是無法捕獲callback里面的異常,只能獲取發出請求環節的異常。這樣就存在問題:假如請求的發出和請求的處理是兩個人完成的,那么在異常處理的時候就存在問題?
b)promise實現異常的傳遞,這帶來一些好處,在實際項目中保證代碼不被阻塞。但是如果異步事件比較多的時候,不容易找出到底是那個異步事件產生了異常。
// 場景描述: 在CRM里面展示價格的報警信息,其中包含競對的信息。但是獲取競對的信息時間比較長,后端為了避免慢查詢,就把一條記錄拆成兩塊分別獲取。 // 第一步:獲取價格報警信息,除了競對信息 function getPriceAlarmData() { return new Promise(function(resolve) { Y.io(url, { method: 'get', data: params, on: function() { success: function(id, data) { resolve(alarmData); } } }); }); } // 得到報警信息后,在去獲取競對信息 getPriceAlarmData().then(function(data) { // 數據渲染,除了競對信息 render(data); return new Promise(function(resolve) { Y.io(url, { method: 'get', data: {alarmList: data}, on: function() { success: function(id, compData) { resolve(compData); } } }); }); }) // 獲取完所有數據后進行競對信息的渲染 .then(function(data) { // 渲染競對信息 render(data) }, function(err) { // 異常處理 console.log(err); });可以把上述代碼轉換成如下:
try{ // 獲取除競對以外的報警信息 var alarmData = alarmDataExceptCompare(); render(alarmData); // 根據報警信息查詢競對信息 var compareData = getCompareInfo(alarmData); render(compareData); } catche(err) { console.log(err.message); }在上述例子中把異常處理放到最后進行處理,這樣當其中存在某個環節出現異常,我們無法準確知道到底是哪個事件產生的。
2.2 jQuery.Deferred 的問題
jQuery中也實現了異步操作,但是在實現上不符合promise/A+規范,主要表現在以下幾個方面:
a. 參數的個數:標準的Promise只能接受一個參數,而jQuery中則可以傳遞多個參數
function asyncInJQuery() { var d = new $.Deferred(); setTimeout(function() { d.resolve(1, 2); }, 100); return d.promise() } asyncInJQuery().then(function(val1, val2) { console.log('output: ', val1, val2); }); // output: 1 2b. 結果處理中異常的處理
function asyncInPromise() { return new Promise(function(resolve) { setTimeout(function() { var jsonStr = '{"name": "mt}'; resolve(jsonStr); }, 100); }); } asyncInPromise().then(function(val) { var d = JSON.parse(val); console.log(d.name); }).then(null, function(err) { console.log('show error: ' + err.message); }); // show error: Unexpected end of inputfunction asyncInJQuery() { var d = new $.Deferred(); setTimeout(function() { var jsonStr = '{"name": "mt}'; d.resolve(jsonStr); }, 100); return d.promise() } asyncInJQuery().then(function(val) { var d = JSON.parse(val); console.log(d.name); }).then(function(v) { console.log('success: ', v.name); }, function(err){ console.log('show error: ' + err.message); });
//Uncaught SyntaxError: Unexpected end of input</pre>
從中可以看出,Promise對回調函數進行了結果處理,可以捕獲回調函數執行過程中的異常,而jQuery.Deferred卻不可以。
參考內容
- http://sporto.github.io/blog/2012/12/09/callbacks-listeners-promises/
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- http://wiki.commonjs.org/wiki/Promises/A#Open_Issues
- https://promisesaplus.com/
- https://blog.domenic.me/youre-missing-the-point-of-promises/
- http://www.html5rocks.com/zh/tutorials/es6/promises/
- http://davidwalsh.name/es6-generators </ul> 原文出處: bigbrother1984