標籤:0.11 這一 exception txt 例子 寫入 pre event api
本文轉自:http://www.cnblogs.com/nullcc/p/5841182.html
至少在語言層級上,Javascript是單線程的,因此非同步編程對其尤為重要。
拿nodejs來說,外殼是一層js語言,這是使用者操作的層面,在這個層次上它是單線程啟動並執行,也就是說我們不能像Java、Python這類語言在語言層級使用多線程能力。取而代之的是,nodejs編程中大量使用了非同步編程技術,這是為了高效使用硬體,同時也可以不造成同步阻塞。不過nodejs在底層實現其實還是用了多線程技術,只是這一層使用者對使用者來說是透明的,nodejs幫我們做了幾乎全部的管理工作,我們不用擔心鎖或者其他多線程編程會遇到的問題,只管寫我們的非同步代碼就好。
二. Javascript非同步編程方法
ES 6以前:
* 回呼函數
* 事件監聽(事件發布/訂閱)
* Promise對象
ES 6:
* Generator函數(協程coroutine)
ES 7:
* async和await
PS:如要運行以下例子,請安裝node v0.11以上版本,在命令列下使用 node [檔案名稱.js] 的形式來運行,有部分代碼需要開啟特殊選項,會在具體例子裡說明。
1.回呼函數
回呼函數在Javascript中非常常見,一般是需要在一個耗時操作之後執行某個操作時可以使用回呼函數。
example 1:
1 //一個定時器 2 function timer(time, callback){ 3 setTimeout(function(){ 4 callback(); 5 }, time); 6 } 7 8 timer(3000, function(){ 9 console.log(123);10 })
example 2:
//讀檔案後輸出檔案內容var fs = require(‘fs‘);fs.readFile(‘./text1.txt‘, ‘utf8‘, function(err, data){ if (err){ throw err; } console.log(data);});
example 3:
1 //嵌套回調,讀一個檔案後輸出,再讀另一個檔案,注意檔案是有序被輸出的,先text1.txt後text2.txt2 var fs = require(‘fs‘);3 4 fs.readFile(‘./text1.txt‘, ‘utf8‘, function(err, data){5 console.log("text1 file content: " + data);6 fs.readFile(‘./text2.txt‘, ‘utf8‘, function(err, data){7 console.log("text2 file content: " + data);8 });9 });
example 4:
//callback helldoSomethingAsync1(function(){ doSomethingAsync2(function(){ doSomethingAsync3(function(){ doSomethingAsync4(function(){ doSomethingAsync5(function(){ // code... }); }); }); });});
通過觀察以上4個例子,可以發現一個問題,在回呼函數嵌套層數不深的情況下,代碼還算容易理解和維護,一旦嵌套層數加深,就會出現“回調金字塔”的問題,就像example 4那樣,如果這裡面的每個回呼函數中又包含了很多商務邏輯的話,整個代碼塊就會變得非常複雜。從邏輯正確性的角度來說,上面這幾種回呼函數的寫法沒有任何問題,但是隨著商務邏輯的增加和趨於複雜,這種寫法的缺點馬上就會暴露出來,想要維護它們實在是太痛苦了,這就是“回調地獄(callback hell)”。
一個衡量回調層次複雜度的方法是,在example 4中,假設doSomethingAsync2要發生在doSomethingAsync1之前,我們需要忍受多少重構的痛苦。
回呼函數還有一個問題就是我們在回呼函數之外無法捕獲到回呼函數中的異常,我們以前在處理異常時一般這麼做:
example 5:
1 try{2 //do something may cause exception..3 }4 catch(e){5 //handle exception...6 }
在同步代碼中,這沒有問題。現在思考一下下面代碼的執行情況:
example 6:
1 var fs = require(‘fs‘); 2 3 try{ 4 fs.readFile(‘not_exist_file‘, ‘utf8‘, function(err, data){ 5 console.log(data); 6 }); 7 } 8 catch(e){ 9 console.log("error caught: " + e);10 }
你覺得會輸出什嗎?答案是undefined。我們嘗試讀取一個不存在的檔案,這當然會引發異常,但是最外層的try/catch語句卻無法捕獲這個異常。這是非同步代碼的執行機制導致的。
Tips: 為什麼非同步代碼回呼函數中的異常無法被最外層的try/catch語句捕獲?
非同步呼叫一般分為兩個階段,提交請求和處理結果,這兩個階段之間有事件迴圈的調用,它們屬於兩個不同的事件迴圈(tick),彼此沒有關聯。
非同步呼叫一般以傳入callback的方式來指定非同步作業完成後要執行的動作。而非同步呼叫本體和callback屬於不同的事件迴圈。
try/catch語句只能捕獲當次事件迴圈的異常,對callback無能為力。
也就是說,一旦我們在非同步呼叫函數中扔出一個非同步I/O請求,非同步呼叫函數立即返回,此時,這個非同步呼叫函數和這個非同步I/O請求沒有任何關係。
2.事件監聽(事件發布/訂閱)
事件監聽是一種非常常見的非同步編程模式,它是一種典型的邏輯分離方式,對代碼解耦很有用處。通常情況下,我們需要考慮哪些部分是不變的,哪些是容易變化的,把不變的部分封裝在組件內部,供外部調用,需要自訂的部分暴露在外部處理。從某種意義上說,事件的設計就是組件的介面設計。
example 7:
1 //發布和訂閱事件 2 3 var events = require(‘events‘); 4 var emitter = new events.EventEmitter(); 5 6 emitter.on(‘event1‘, function(message){ 7 console.log(message); 8 }); 9 10 emitter.emit(‘event1‘, "message for you");
這種使用事件監聽處理的非同步編程方式很適合一些需要高度解耦的情境。例如在之前一個遊戲服務端項目中,當人物屬性變化時,需要寫入到持久層。解決方案是先寫一個訂閱者,訂閱‘save‘事件,在需要儲存資料時讓發布方對象(這裡就是人物對象)上直接用emit發出一個事件名並攜帶相應參數,訂閱者收到這個事件資訊並處理。
3.Promise對象
ES 6中原生提供了Promise對象,Promise對象代表了某個未來才會知道結果的事件(一般是一個非同步作業),並且這個事件對外提供了統一的API,可供進一步處理。
使用Promise對象可以用同步操作的流程寫法來表達非同步作業,避免了層層嵌套的非同步回調,代碼也更加清晰易懂,方便維護。
Promise.prototype.then()
Promise.prototype.then()方法返回的是一個新的Promise對象,因此可以採用鏈式寫法,即一個then後面再調用另一個then。如果前一個回呼函數返回的是一個Promise對象,此時後一個回呼函數會等待第一個Promise對象有了結果,才會進一步調用。
example 8:
1 //ES 6原生Promise樣本 2 var fs = require(‘fs‘) 3 4 var read = function (filename){ 5 var promise = new Promise(function(resolve, reject){ 6 fs.readFile(filename, ‘utf8‘, function(err, data){ 7 if (err){ 8 reject(err); 9 }10 resolve(data);11 })12 });13 return promise;14 }15 16 read(‘./text1.txt‘)17 .then(function(data){18 console.log(data);19 }, function(err){20 console.log("err: " + err);21 });
以上代碼中,read函數是Promise化的,在read函數中,執行個體化了一個Promise對象,Promise的建構函式接受一個函數作為參數,這個函數的兩個參數分別是resolve方法和reject方法。如果非同步作業成功,就是用resolve方法將Promise對象的狀態從“未完成”變為“完成”(即從pending變為resolved),如果非同步作業出錯,則是用reject方法把Promise對象的狀態從“未完成”變為“失敗”(即從pending變為rejected),read函數返回了這個Promise對象。Promise執行個體產生以後,可以用then方法分別指定resolve方法和reject方法的回呼函數。
上面這個例子,Promise建構函式的參數是一個函數,在這個函數中我們寫非同步作業的代碼,在非同步作業的回調中,我們根據err變數來選擇是執行resolve方法還是reject方法,一般來說調用resolve方法的參數是非同步作業擷取到的資料(如果有的話),但還可能是另一個Promise對象,表示非同步作業的結果有可能是一個值,也有可能是另一個非同步作業,調用reject方法的參數是非同步回調用的err參數。
調用read函數時,實際上返回的是一個Promise對象,通過在這個Promise對象上調用then方法並傳入resolve方法和reject方法來指定非同步作業成功和失敗後的操作。
example 9:
1 //原生Primose順序嵌套回調樣本 2 var fs = require(‘fs‘) 3 4 var read = function (filename){ 5 var promise = new Promise(function(resolve, reject){ 6 fs.readFile(filename, ‘utf8‘, function(err, data){ 7 if (err){ 8 reject(err); 9 }10 resolve(data);11 })12 });13 return promise;14 }15 16 read(‘./text1.txt‘)17 .then(function(data){18 console.log(data);19 return read(‘./text2.txt‘);20 })21 .then(function(data){22 console.log(data);23 });
在Promise的順序嵌套回調中,第一個then方法先輸出text1.txt的內容後返回read(‘./text2.txt‘),注意這裡很關鍵,這裡實際上返回了一個新的Promise執行個體,第二個then方法指定了非同步讀取text2.txt檔案的回呼函數。這種形似同步調用的Promise順序嵌套回調的特點就是有一大堆的then方法,代碼冗餘略多。
異常處理
Promise.prototype.catch()
Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的別名,用於指定發生錯誤時的回呼函數。
example 9:
1 var fs = require(‘fs‘) 2 3 var read = function (filename){ 4 var promise = new Promise(function(resolve, reject){ 5 fs.readFile(filename, ‘utf8‘, function(err, data){ 6 if (err){ 7 reject(err); 8 } 9 resolve(data);10 })11 });12 return promise;13 }14 15 read(‘./text1.txt‘)16 .then(function(data){17 console.log(data);18 return read(‘not_exist_file‘);19 })20 .then(function(data){21 console.log(data);22 })23 .catch(function(err){24 console.log("error caught: " + err);25 })26 .then(function(data){27 console.log("completed");28 })
。。。。。。
Javascript非同步編程