歡迎光臨
每天分享高質量文章

談談非同步程式設計

作者:big-brother

網址:http://www.cnblogs.com/bigbrother1984/p/4140685.html

目前需求中涉及到大量的非同步操作,實際的頁面越來越傾向於單頁面應用。以後可以會使用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() {

  // 出問題了

  ……

});

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);

}

}

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

1.4.5 穿透作用

Promise.resolve(111).then(Promise.resolve(222)).then(function (val) {

console.log(val)

});

上述程式碼的執行結構是111,而不是222,因為then(fn)要去fn必須為function,否則按照then(null)來解析,相當於忽略了該回呼的執行,promise的value依然是原來promise的value值,所以結果為111。

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 2

b. 結果處理中異常的處理

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 input

function 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

從中可以看出,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/

The Basics Of ES6 Generators

http://fex.baidu.com/blog/2015/07/we-have-a-problem-with-promises/

贊(0)

分享創造快樂