首頁 >web前端 >js教程 >深入探析koa之非同步回呼處理篇

深入探析koa之非同步回呼處理篇

高洛峰
高洛峰原創
2016-11-19 15:44:441048瀏覽

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 fs = require('fs');

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) {
  console.log(data.toString());
})

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

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

var fs = require('fs');

var readFile = function(fileName) {
  return new Promise(function(resolve, reject) {
    fs.readFile(fileName, function(err, data) {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    })
  })
}

//将读文件的过程放在generator中
var gen = function*() {
  var data = yield readFile('./file1');
  console.log(data.toString());
  data = yield readFile('./file2');
  console.log(data.toString());
}

//手动执行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 fs = require('fs');

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

var readFile = function(fileName) {
  return new Promise(function(resolve, reject) {
    fs.readFile(fileName, function(err, data) {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    })
  })
}

//将读文件的过程放在generator中
var gen = function*() {
  var data = yield readFile('./file1');
  console.log(data.toString());
  data = yield readFile('./file2');
  console.log(data.toString());
}
//下面只需要将gen放入run当中即可自动执行
run(gen);

執行上述程式碼,即可看到終端依序列印出了file1和file2的內容。

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

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

這篇文章的程式碼同樣可以在github上面找到:https://github.com/mly-zju/async-js-demo,其中promise_generator.js就是本篇的範例原始碼。


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn