天天看點

深入探析koa之異步回調處理篇

深入探析koa之異步回調處理篇

在上一篇中我們梳理了koa當中中間件的洋蔥模型執行原理,并實作了一個可以讓洋蔥模型自動跑起來的流程管理函數。這一篇,我們再來研究一下koa當中異步回調同步化寫法的原理,同樣的,我們也會實作一個管理函數,是的我們能夠通過同步化的寫法來寫異步回調函數。

1. 回調金字塔及理想中的解決方案

我們都知道javascript是一門單線程異步非阻塞語言。異步非阻塞當然是它的一個優點,但大量的異步操作必然涉及大量的回調函數,特别是當異步嵌套的時候,就會出現回調金字塔的問題,使得代碼的可讀性非常差。比如下面一個例子:

var fs = require('fs'); 

fs.readfile('./file1', function(err, data) { 

  console.log(data.tostring()); 

  fs.readfile('./file2', function(err, data) { 

    console.log(data.tostring()); 

  }) 

})  

這個例子是先後讀取兩個檔案内容并列印,其中file2的讀取必須在file1讀取結束之後再進行,是以其操作必須要在file1讀取的回調函數中執行。這是一個典型的回調嵌套,并且隻有兩層而已,在實際程式設計中,我們可能會遇到更多層的嵌套,這樣的代碼寫法無疑是不夠優雅的。

在我們想象中,比較優雅的一種寫法應該是看似同步實則異步的寫法,類似下面這樣:

var data; 

data = readfile('./file1'); 

//下面的代碼是第一個readfile執行完畢之後的回調部分 

console.log(data.tostring()); 

//下面的代碼是第二個readfile的回調 

data = readfile('./file2'); 

console.log(data.tostring());  

這樣的寫法,就完全避免回調地獄。事實上,koa就讓我們可以使用這樣的寫法來寫異步回調函數:

var koa = require('koa'); 

var app = koa(); 

var request=require('some module'); 

app.use(function*() { 

  var data = yield request('http://www.baidu.com'); 

  //以下是異步回調部分 

  this.body = data.tostring(); 

}) 

app.listen(3000); 

那麼,究竟是什麼讓koa有這麼神奇的魔力呢?

2. generator配合promise實作異步回調同步寫法

關鍵的一點,其實前一篇也提到了,就是generator具有類似"打斷點"這樣的效果。當遇到yield的時候,就會暫停,将控制權交給yield後面的函數,當下次傳回的時候,再繼續執行。

而在上面的那個koa例子中,yield後面的可不是任何對象都可以哦!必須是特定類型。在co函數中,可以支援promise, thunk函數等。

今天的文章中,我們就以promise為例來進行分析,看看如何使用generator和promise配合,實作異步同步化。

依舊以第一個讀取檔案例子來分析。首先,我們需要将讀檔案的函數進行改造,将其封裝成為一個promise對象:

var readfile = function(filename) { 

  return new promise(function(resolve, reject) { 

    fs.readfile(filename, function(err, data) { 

      if (err) { 

        reject(err); 

      } else { 

        resolve(data); 

      } 

    }) 

//下面是readfile使用的示例 

var tmp = readfile('./file1'); 

tmp.then(function(data) { 

關于promise的使用,如果不熟悉的可以去看看es6中的文法。(近期我也會寫一篇文章來教大家如何用es5的文法來自己實作一個具備基本功能的promise對象,敬請期待呦^_^)

簡單來講,promise可以實作将回調函數通過 promise.then(callback)的形式來寫。但是我們的目标是配合generator,真正實作如絲般順滑的同步化寫法,如何配合呢,看這段代碼:

//将讀檔案的過程放在generator中 

var gen = function*() { 

  var data = yield readfile('./file1'); 

  data = yield readfile('./file2'); 

//手動執行generator 

var g = gen(); 

var another = g.next(); 

//another.value就是傳回的promise對象 

another.value.then(function(data) { 

  //再次調用g.next從斷點處執行generator,并将data作為參數傳回 

  var another2 = g.next(data); 

  another2.value.then(function(data) { 

    g.next(data); 

上述代碼中,我們在generator中yield了readfile,回調語句代碼寫在yield之後的代碼中,完全是同步的寫法,實作了文章一開頭的設想。

而yield之後,我們得到的是一個another.value是一個promise對象,我們可以使用then語句定義回調函數,函數的内容呢,則是将讀取到的data傳回給generator并繼續讓generator從斷點處執行。

基本上這就是異步回調同步化最核心的原理,事實上如果大家熟悉python,會知道python中有"協程"的概念,基本上也是使用generator來實作的(我想當懷疑es6的generator就是借鑒了python~)

不過呢,上述代碼我們依然是手動執行的。那麼同上一篇一樣,我們還需要實作一個run函數,用于管理generator的流程,讓它能夠自動跑起來!

3. 讓同步化回調函數自動跑起來:一個run函數的編寫

仔細觀察上一段代碼中手動執行generator的部分,也能發現一個規律,這個規律讓我們可以直接寫一個遞歸的函數來代替:

var run=function(gen){ 

  var g; 

  if(typeof gen.next==='function'){ 

    g=gen; 

  }else{ 

    g=gen(); 

  } 

  function next(data){ 

    var tmp=g.next(data); 

    if(tmp.done){ 

      return ; 

    }else{ 

      tmp.value.then(next); 

    } 

  next(); 

}  

函數接收一個generator,并讓其中的異步能夠自動執行。使用這個run函數,我們來讓上一個異步代碼自動執行:

var run = function(gen) { 

  if (typeof gen.next === 'function') { 

    g = gen; 

  } else { 

    g = gen(); 

  function next(data) { 

    var tmp = g.next(data); 

    if (tmp.done) { 

      return; 

    } else { 

//下面隻需要将gen放入run當中即可自動執行 

run(gen);  

執行上述代碼,即可看到終端依次列印出了file1和file2的内容。

需要指出的是,這裡的run函數為了簡單起見隻支援promise,而實際的co函數還支援thunk等。

這樣一來,co函數的兩大功能基本就完整介紹了,一個是洋蔥模型的流程控制,另一個是異步同步化代碼的自動執行。在下一篇文章中,我将帶大家對這兩個功能進行整合,寫出我們自己的一個co函數!

作者:勇敢的半導體

來源:51cto