回調函數是指令式的,Promise 是函數式的:Node 錯失的最大機會

我以前都有接觸過關於 Promise 的一些文章,可是對它的感受並不大。由於以爲雖然回調風格確實有問題,但我寫的代碼尚未複雜到那種程度,因此,要去使用的感受並不強烈。javascript

可是,後面碰到一個問題真的好像用回調的風格來寫的話,會比較糟糕。加上看到了這一篇從另外一側面來看 Promise 對函數式編程的思惟方面的轉變,以爲很不錯。值得一看,因此在有其它大神也翻譯過的狀況下,本身也譯一次,順便深刻學習。java

原文連接: Callbacks are imperative, promises are functional: Node’s biggest missed opportunitynode

Promise的本質就是他們不隨着環境的變化而變化。程序員

—— Frank Underwood,‘紙牌屋’數據庫

你常常會聽到說 JavaScript 是一門 "函數式" 編程語言。一般咱們這樣描述它的時候是由於函數在它裏面是做爲 "一等公民" 而存在的。可是其它 "函數式" 編程語言裏面的特性,好比:數據不可改變,代數類型系統,使用迭代優於循環,避免反作用都通通忽略了。雖然函數做爲 "一等公民" 是很是有用的,而且決定用戶可以在須要的時候使用函數式風格來編寫代碼。可是 JS 是函數式的觀點卻經常忽略了函數式編程的核心思想:面向值編程。npm

"函數式編程" 的命名其實會產生誤導,以致於人們認爲它的意義在於,相對於 「面向對象編程」 來講,它是 「面向函數編程」。可是若是面向對象編程是把全部東西都從對象角度考慮,那函數式編程就是把全部東西都做爲值來處理,而不只僅是把函數考慮爲值。很明顯,數值固然包含那些數字,字符,列表和其它數據值,但其實它也包含其它面向對象編程的粉絲一般沒有考慮過的一些東西:IO 操做和其它反作用,GUI 事件流,空值檢查,甚至函數調用的順序。若是你曾經據說過 「可編程分號」 的話,你應該知道我想說的是什麼了.編程

函數式編程最大的好處是它是聲明式的。在命令式編程裏面,咱們須要寫一系列的指令來告訴計算機是怎麼去實現咱們想要作的事情的。在函數式編程裏面,咱們只是須要描述值之間的計算關係,計算機就會本身想辦法得出須要的計算指令順序。api

若是你使用過 Excel 的話,你其實已經用過函數式編程了:你只須要描述一個圖表裏面的值,是怎麼相互計算出來的。當有新數據插入的時候,Excel 就會本身得出圖表裏有什麼地方的值和效果要更新,而你並不須要再爲它寫出任何指令,它也能夠幫你計算出來。數組

在闡述了這些基本概念的基礎上,我想說明一下我以爲 Node.js 在設計上最大的失誤是什麼: 這就是在它的設計早期,決定了傾向於使用回調風格的 API 而不是 promise 風格.promise

全部人都使用回調。若是你發佈了一個返回 promise 的模塊,根本沒有人會關注和使用你那個模塊。

若是我寫了一個小模塊,它須要和 Redis 交互,我所須要作的惟一一件事情就是傳遞一個回調函數給 Redis。當咱們遇到回調無底洞的時候,其實這根本不是什麼問題: 由於一樣有協程monad 無底洞。由於若是你把任何一個抽象使用地足夠頻繁的話,都一樣會創造一個無底洞。

在 90% 的狀況下,你只須要作一件事情,回調如此簡單的接口使得你只是須要簡單的縮進一下就能夠了。若是你遇到了很是複雜的用例,你和其它在 npm 裏面的 827 個模塊同樣,使用 async 就行了.

—— Mikeal Rogers,LXJS 2012

這段話是從 Mikeal Rogers 最近的一次涵蓋了好些 Node 設計哲學的演講裏摘取出來的:

在 Node 的初期設計目標裏面,我但願可讓更多的非專家級別的程序員能夠很容易編寫出快速,支持並行的網絡程序,雖然我知道這個想法有點違背生產效率。Promises 其實可使得程序在運行時自動控制數據流動,而不是靠程序員經過顯式指令控制,因此能更加容易組織正確清晰和最大化並行操做的程序.

要寫出正確的並行程序基本上須要你實現儘量多的並行工做的同時,保證操做指令仍是以正確的順序執行。雖然 JavaScript 是單線程的,但咱們依然有可能由於在異步操做的狀況下觸發了競爭機制: 任何涉及 IO 的操做都會在它等待回調的時候把 CPU 時間騰到其它操做上面。多個併發操做就有可能同時訪問同一段內存數據,或者產生一系列重疊的操做數據庫或者 DOM 的指令。因此,我但願在這篇文章裏能夠告訴你們,promies 可以像 Excel 同樣,提供一種只須要描述值之間的關係模型,你的工具就可以自動尋求最佳解決方案給你。而不是須要你本身控制程序流.

我但願能夠清除掉一個誤區就是 promises 的使用就是爲了讓語法結構看起來比基於回調的異步操做更清晰。其實它們能夠幫助你用一個徹底不一樣的方式來建模。它們的做用比簡化語法來得更深層次。事實上,它們徹底從語意角度改變你解決問題的方式。

首先,我想先重溫一下幾年前寫的一篇文章。它是關於 promises 是怎麼在異步編程上做爲一個 monad 的角色而存在的。這裏的核心思想就是 monad 實際上是幫助你組織函數的工具,好比說,當一個函數的返回值要作爲下一個函數的輸入的時候,創建數據管道。數據關係的結構化是實現的關鍵。

在這裏的,我仍是須要用到 Haskell 的類型註解來幫助說明一下。在 Haskell 裏,註解 foo :: bar 表示 「foo 是 bar 的類型「。註解 foo :: Bar -> Qux 表示 「foo 是一個接受輸入值爲 Bar 類型和返回值爲 Qux 類型的函數「。若是輸入輸出的類別並不重要的話,咱們會用單一小寫字母,foo :: a -> b。若是函數 foo 能夠接受多個參數的化,咱們會添加多個箭頭,好比:「 foo :: a -> b -> c 」 表示 foo 接收兩個分別爲類型 a 和 b 的參數並返回類型 c 的值.

咱們來看一個 Node 函數吧,好比,fs.readFile()。這個函數接收一個 String 類型的路徑參數,還有一個回調函數,而且沒有任何返回值。回調函數會接收一個可能爲空的 Error 類型和一個包含了文件內容的 Buffer 類型的參數,而且也沒有返回值。那咱們就能夠把 readFile 的類型用註解表示爲:

readFile :: String -> Callback -> ()

() 在 Haskell 註解中表示空值類型。這裏的 callback 是另外一個函數,它的註解能夠表示爲:

Callback :: Error -> Buffer -> ()

把它們放在一塊兒的話,咱們能夠說 readFile 接收兩個參數,一個 String 類型,一個是接收 Buffer 參數的函數:

readFile :: String -> (Error -> Buffer -> ()) -> ()

如今,咱們來想象一下假如 Node 使用 promises 會是怎麼樣的。這樣的狀況下,readFile 能夠簡單的接收一個 String 類型參數而後返回一個 Buffer 的 promise:

readFile :: String -> Promise Buffer

通常來講,咱們能夠認爲回調風格的函數接收一些參數和一個函數,這個函數將會被最終調用並傳遞返回值做爲它的輸入;promises 風格的函數就是接收一些參數,和返回一個帶結果的 promise:

callback :: a -> (Error -> b -> ()) -> ()
promise :: a -> Promise b

那些回調風格返回的空值其實就是爲何使用回調風格來編程會很困難的根本緣由: 回調風格不返回任何值,因此難以組合。一個沒有返回值的函數執行的效果實際上是利用它的反作用 – 一個沒有返回值和利用反作用的函數其實就是一個黑洞。因此,使用回調風格來編程沒法避免會是指令式的,它其實是經過把一系列嚴重依賴於反作用的操做安排好執行順序,而不是經過函數的調用來把輸入輸出值對應好。它是經過人手組織程序執行流程而不是靠理順值的關係來解決問題的。這正是編寫正確的並行程序困難的緣由.

相反,基於 promise 的函數老是讓你把函數返回值做爲一個不依賴於時間的值來考慮的。當你調用一個回調風格的函數時,在你的函數調用和它的回調函數被調用之間,在程序裏面咱們沒辦法找到一個最終結果的表現形式.

fs.readFile('file1.txt',
  // some time passes...
  function(error,buffer) {
    // the result now pops into existence
  }
);

從基於回調和事件的函數裏面取得結果基本上意味着 「你必須在恰當的時間和地點」。若是你在事件被觸發以後才綁定你的事件監聽器,或者你沒有在恰當的地方回調你的函數,那麼恭喜你,你將沒法獲得你要的結果了。這些事情使得人們在 Node 裏寫 HTTP 服務器至關困難。若是你的控制流不對,你的程序就沒法定期望運行.

相反,Promises 並不關心執行的順序。你能夠在 promise 兌現前或後註冊監聽器,但你總能拿到它的返回值。所以,那些立刻返回的 promises 實際上是給了你一個表明結果的值,讓你能夠把它看成一等公民,而後傳遞給其它函數。中間不須要等待一個回調或任何丟失事件的可能性。只要你手中拿着一個 promise 的引用,你就能從它獲得你想要的值.

var p1 = new Promise();
p1.then(console.log);
p1.resolve(42);

var p2 = new Promise();
p2.resolve(2013);
p2.then(console.log);

// prints:
// 42
// 2013

即使 then() 這個方法彷佛隱含一些關於操做順序 – 事實上這只是它的反作用 – 你能夠把它想象成叫作 unwrap。Promise 是一個未知值的容器,那麼 then 的工做就是從 promise 中把值取出來並交給另外一個函數: 它實際上是 monad 的 bind 函數。其實上面的代碼裏沒有任何地方說起何時這個值是存在的,或事情是按照什麼順序發生的,它只是表達了一些依賴關係在裏面: 你必須首先知道那個值是什麼,而後纔可以把它打印出來。程序的順序是從值的依賴關係中衍生出來的。這裏其實只有很小的區別,咱們在後面討論到延遲 promise 的時候會看得更清楚.

到目前爲止,這些區別都很微小;不多函數單單和其它函數交互。咱們如今來處理一些複雜一點的問題,以便看到 promises 更增強大之處。假設咱們如今有一些代碼,經過使用 fs.stat() 來取得一些文件的 mtimes。若是是同步的操做,咱們只是須要調用 paths.map(fs.stat) 就能夠了,可是由於用 mapping 來處理異步的問題是很困難的,咱們看看用上 async 模塊是什麼樣子.

var async = require('async'),
    fs    = require('fs');

var paths = ['file1.txt','file2.txt','file3.txt'];

async.map(paths,fs.stat,function(error,results) {
  // use the results
});

(是的,我知道 fs 的函數有同步版本,但大多數涉及 I/O 的操做都無法這麼作,就陪我玩一玩吧。)

這樣看起來都還不錯,直到咱們決定要拿到 file1 的大小來作其它不相關的任務的時候。固然,咱們能夠再拿一次那個文件的狀態:

var paths = ['file1.txt','file2.txt','file3.txt'];

async.map(paths,fs.stat,function(error,results) {
  // use the results
});

fs.stat(paths[0],function(error,stat) {
  // use stat.size
});

這樣顯然沒有問題,可是咱們如今取了那個文件的狀態兩次。固然,本地的文件操做是沒有問題的,但若是咱們正在經過 https 來獲取大文件的時候,那麻煩就大了。因此,咱們只能訪問文件一次。這樣,咱們就要修改一下前面的代碼來特殊處理一下第一個文件:

var paths = ['file1.txt','file2.txt','file3.txt'];

async.map(paths,fs.stat,function(error,results) {
  var size = results[0].size;
  // use size
  // use the results
});

這初看也沒有問題,可是獲取文件大小的任務就必須等到整個列表都處理完了纔可以開始。若是其中任何一個文件處理出錯,咱們就沒法獲得第一個文件的結果了。這種方案並很差,那咱們來試一試另外一種方式: 咱們把第一個文件分開單獨處理.

var paths = ['file1.txt','file2.txt','file3.txt'],
    file1 = paths.shift();

fs.stat(file1,function(error,stat) {
  // use stat.size
  async.map(paths,fs.stat,function(error,results) {
    results.unshift(stat);
    // use the results
  });
});

這樣固然可行,可是如今咱們的程序並非並行的了: 它將須要更長的時間去運行,由於咱們必須等到第一個文件處理完才能開始處理那個列表裏的文件。以前,它們都是同步進行的。還有,咱們如今還必須對第一個文件特殊處理而引入一些數組的操做.

好吧,最後一擊。咱們如今要作的是獲得全部文件的詳情,每一個文件只讀取一次,若是第一個文件讀取成功了咱們要作些特殊處理,而且若是整個列表裏的文件都處理成功,咱們要對整個列表再進行一些操做。讓咱們用 async 來在代碼裏表達出這個需求的依賴關係看看.

var paths = ['file1.txt','file2.txt','file3.txt'],
    file1 = paths.shift();

async.parallel([
  function(callback) {
    fs.stat(file1,function(error,stat) {
      // use stat.size
      callback(error,stat);
    });
  },
  function(callback) {
    async.map(paths,fs.stat,callback);
  }
],function(error,results) {
  var stats = [results[0]].concat(results[1]);
  // use the stats
});

好了,這樣就達到要求了: 每一個文件只讀取一次,全部的工做都是並行處理的,咱們也能夠獨立的訪問第一個文件的結果,而且相互依賴的任務都是儘早執行完畢的。搞定!

其實,並不能說徹底搞定了。我認爲這樣的代碼真的很醜陋,而且當問題變的複雜的時候,這樣的代碼很難擴展。爲了讓它正常工做,咱們須要考慮大量的代碼執行順序問題。 並且設計意圖並不明顯以致於後面的維護極可能會不經意把它破壞掉。當咱們引入了一個特殊需求後,本來問題的解決策略被迫同一些後續的跟進操做混雜在一塊兒,而且咱們還要對數組做出那麼噁心的操做。

全部的問題其實都來自於咱們嘗試經過控制程序流來做爲主要的解決問題的手段,而不是依賴於數據之間的關係。不是說 「爲了可以運行這個任務,我須要這個數據」,並讓運行環境去尋找優化手段,而是顯式聲明運行時什麼應該並行,什麼應該串行,因此致使咱們的解決方案是如此脆弱.

那麼,promises 如何改善這種狀況呢? 咱們須要一些操做文件系統的函數是能夠返回 promises 而不是接收一個回調函數的。可是與其手寫一個這樣的函數,咱們能夠用元編程的方式寫一個函數,使得它能夠轉換任何其它函數返回 promises。好比說,它能夠接收以下一個函數定義爲

String -> (Error -> Stat -> ()) -> ()

而且返回如下類型

String -> Promise Stat

下面就是這個元編程的函數:

// promisify :: (a -> (Error -> b -> ()) -> ()) -> (a -> Promise b)
var promisify = function(fn,receiver) {
  return function() {
    var slice   = Array.prototype.slice,
        args    = slice.call(arguments,0,fn.length - 1),
        promise = new Promise();

    args.push(function() {
      var results = slice.call(arguments),
          error   = results.shift();

      if (error) promise.reject(error);
      else promise.resolve.apply(promise,results);
    });

    fn.apply(receiver,args);
    return promise;
  };
};

(這還不是一個通用方案,可是足夠在咱們的場景裏使用了.)

咱們如今能夠從新對咱們的業務問題建模。咱們基本上要作的就把一個列表的文件路徑,轉換爲一個列表的文件狀態 promises:

var fs_stat = promisify(fs.stat);

var paths = ['file1.txt','file2.txt','file3.txt'];

// [String] -> [Promise Stat]
var statsPromises = paths.map(fs_stat);

從這裏就能夠看出分別了: 經過使用 async.map() , 你必須等到整個列表處理完了,你才能拿到數據進行處理。可是若是你有了一個列表的 promises,你能夠直接拿第一個 promise 來操做:

statsPromises[0].then(function(stat) { /* use stat.size */ });

因此,經過使用 promise,咱們把大部分問題都解決了: 咱們並行獲得全部文件的狀態,而且能夠獨立訪問並不止第一個文件,能夠是任何一個文件,而這隻須要指定某個數組位就能夠了。經過前一種方法,咱們須要顯式寫邏輯特殊處理第一個文件,並且考慮怎麼拿到那個文件還很是費事。可是,經過一個列表的 promises 就很容易了.

固然,這裏缺乏的部分是當全部的文件狀態信息都拿到後,咱們應該怎麼處理。經過前面,咱們獲得了一個列表的 文件狀態值對象,但這是一個列表的 promises。咱們須要等到全部的 promises 都處理完後,拿到一個列表的文件狀態。也就是說,咱們要把一個列表的 promises 轉化成一個 promise 對應於整個列表.

讓咱們看看一個簡單的 list 方法是怎麼作到能夠把一個包含了 promises 的列表轉化成一個 promise,並且當它裏面全部的 promises 都處理完後,它本身也處理了.

// list :: [Promise a] -> Promise [a]
var list = function(promises) {
  var listPromise = new Promise();
  for (var k in listPromise) promises[k] = listPromise[k];

  var results = [],done = 0;

  promises.forEach(function(promise,i) {
    promise.then(function(result) {
      results[i] = result;
      done += 1;
      if (done === promises.length) promises.resolve(results);
    },function(error) {
      promises.reject(error);
    });
  });

  if (promises.length === 0) promises.resolve(results);
  return promises;
};

(譯者注:這裏感受好像 promises 和 listPromise 幾個地方反了。做者沒開評論,沒法確認,不過有時間試一下代碼就知道了。)

(這個方法其實和 jQuery.when() 函數相似,它一樣接收一個列表的 promises 並返回一個新的 promise。當這個 promise 全部的輸入都處理完後,它本身也處理了.)

咱們如今就能夠經過把數組包裝成一個 promise,而後等全部的處理結果出來就能夠了:

list(statsPromises).then(function(stats) { /* use the stats */ });

那麼咱們完整的解決方案就會是這樣:

var fs_stat = promisify(fs.stat);

var paths = ['file1.txt','file2.txt','file3.txt'],
    statsPromises = list(paths.map(fs_stat));

statsPromises[0].then(function(stat) {
  // use stat.size
});

statsPromises.then(function(stats) {
  // use the stats
});

這個解決方案的表達就至關的簡潔清晰了。經過一些通用的輔助函數和既有的數組操做函數,咱們用一種正確的,有效而且容易調整的方法來實現了。咱們也不須要 async 模塊的特殊集合類函數,咱們只須要讓 promises和數組二者的思想相互獨立,並經過一種強大的方式把它們組合使用就能夠了.

特別要注意的是,咱們的程序在這裏並無說任何部分是應該是並行仍是串行處理的。咱們只是描述了咱們想要什麼,任務之間的關係是怎麼樣的,剩下的都是 promise 組件幫咱們優化的.

事實上,不少在 async 的集合類模塊能夠很容易用一個列表的 promises 來替代。咱們已經看到過 map 是怎麼工做的了; 下面的代碼:

async.map(inputs,fn,function(error,results) {});

和下面的是同樣的:

list(inputs.map(promisify(fn))).then(
    function(results) {},
    function(error) {}
);

async.each() 其實就是用 async.map(),而後利用那些被執行的函數的反作用,而把它們的返回值捨棄掉; 你用 map() 就能夠了.

async.mapSeries() (如前所述,async.eachSeries()) 其實就是對一個列表的 promises 上調用 reduce()。那就是,它你的輸入列表,使用 reduce 來獲得一個依賴於前面 promise 的操做成功後再執行的 promise。咱們來舉個例子: 實現一個基於 fs.rmdir() 的程序來實現和 rm -rf 相同的功能。下面的代碼:

var dirs = ['a/b/c','a/b','a'];
async.mapSeries(dirs,fs.rmdir,function(error) {});

和下面的是同樣的:

var dirs     = ['a/b/c','a/b','a'],
    fs_rmdir = promisify(fs.rmdir);

var rm_rf = dirs.reduce(function(promise,path) {
  return promise.then(function() { return fs_rmdir(path) });
},unit());

rm_rf.then(
    function() {},
    function(error) {}
);

這裏的 unit() 只是一個簡單的返回一個已經處理的 promise 來開始整個操做鏈 (若是你知道什麼是 monads,這個就是 promises 的返回函數):

// unit :: a -> Promise a
var unit = function(a) {
  var promise = new Promise();
  promise.resolve(a);
  return promise;
};

這個使用 reduce() 的方案簡單的使用接收列表中的兩個路徑值,並使用 promise.then() 來確保前面的文件夾刪除成功以後,再刪除後面的文件夾。這其實還幫你處理了非空文件夾的狀況: 若是前面的 promise 由於任何錯誤而沒法處理,那麼整個處理流程就中止了。使用值的依賴關係來強制某種執行順序是函數式編程使用 monads 來處理反作用的核心思想.

最後的代碼彷佛比一樣功能的 async 代碼更囉嗦,但別由於這樣矇騙了你。最重要的思想是咱們經過使用 promise 數值和列表操做來組合程序,而不是依賴於特別的庫來控制程序流。正如咱們前面看到的,前一種方式能夠寫出更容易理解的程序.

前一種程序更容易理解是由於咱們把咱們思考流程的一部分交給機器去作了。當使用 async 模塊的時候,咱們的思考流程是這樣的:

A. 在程序裏,咱們的任務應該是這樣相互依賴的,
B. 所以,應該要這樣把操做組織好,
C. 那麼,咱們如今用代碼來表現 B 所描述的流程.

利用相互依賴的 promises 可讓你徹底把 B 那步拋棄掉。你的代碼只須要表達出任務的相互關係就能夠了,而後讓電腦來決定處理流程。換另外一個說法就是,回調風格是顯式的控制處理流程來把不少值組織在一塊兒,而 promises 是顯式表達出值的關係來把控制流的各個組件鏈接在一塊兒。回調是指令式的,promises 是函數式的.

這個主題的討論只有當咱們談到 promises 的最後一個使用場景,也就是函數式編程的核心思想,延時性,纔算完整。Haskell 是一種惰性語言。它和那些從上往下執行的腳本程序不同,它是從定義了程序最終輸出的表達式開始的 – 有什麼須要寫到標準輸出,數據庫等,而後反回來向前執行。它首先看最終的表達式是依賴哪些表達式來取得它們的輸入值的,而後一直往前遍歷整棵樹圖,直到整個程序爲了它的輸出結果反過來計算出所需的全部數據爲止。只有須要用到的數據纔會在程序裏計算出來.

不少時候,計算機領域的問題,最後找到的最佳解決方案都是須要找到最佳的數據結構來建模而得出來的。JavaScript 裏有一個跟我剛纔描述的狀況很是類似的問題: 模塊加載。你只想加載那些你的程序須要用到的模塊,而且但願越快越好.

在咱們有 CommonJS 和 AMD 這類有了依賴關係意識的規範前,咱們有好一些腳本加載庫。它們基本的工做原理都是像咱們上面的例子同樣,經過顯式向加載器聲明你要加載的腳本哪些是能夠並行下載的,哪些是必定要按某種順序下載。你基本上都要說清楚下載的策略,要正確並有效的作好的是至關困難的。相反,經過描述腳本之間的依賴關係來讓加載器優化下載策略就會容易不少.

如今讓咱們來看看怎麼實現 LazyPromise 的。這是一個 Promise,包含了一個可能會作異步操做的函數。這個函數只有在被調用 then() 這個方法的時候會被執行一次: 咱們只有在有須要獲得返回結果的時候纔會開始執行。咱們經過重寫 then() 來判斷一下若是尚未開始過的話就執行操做.

var Promise = require('rsvp').Promise,
    util    = require('util');

var LazyPromise = function(factory) {
  this._factory = factory;
  this._started = false;
};
util.inherits(LazyPromise,Promise);

LazyPromise.prototype.then = function() {
  if (!this._started) {
    this._started = true;
    var self = this;

    this._factory(function(error,result) {
      if (error) self.reject(error);
      else self.resolve(result);
    });
  }
  return Promise.prototype.then.apply(this,arguments);
};

好比說,下面這個程序什麼也不會作: 由於咱們沒有向 promise 取值,沒有須要執行任何操做:

var delayed = new LazyPromise(function(callback) {
  console.log('Started');
  setTimeout(function() {
    console.log('Done');
    callback(null,42);
  },1000);
});

可是若是咱們添加了下面這一行代碼,那麼程序就會打印出 Started,而後一秒後再打印出Done,最後打印出42:

delayed.then(console.log);

由於中間的異步操做是隻處理一次的,因此調用 then() 屢次會打印最終結果屢次,但不會每次再執行異步操做:

delayed.then(console.log);
delayed.then(console.log);
delayed.then(console.log);

// prints:
// Started
// -- 1 second delay --
// Done
// 42
// 42
// 42

經過把以上簡單的通用操做抽象出來,咱們很容易就能夠打造一個模塊優化系統。想象一下咱們要把一系列的模塊這樣處理一下: 每個模塊建立時都綁定了一個名字,一個它依賴的模塊列表,和一個構造函數。這個構造函數會在執行時被傳入所依賴的模塊做爲參數,而後返回自己這個模塊的 API。這其實和 AMD 工做模式相似.

var A = new Module('A',[],function() {
  return {
    logBase: function(x,y) {
      return Math.log(x) / Math.log(y);
    }
  };
});

var B = new Module('B',[A],function(a) {
  return {
    doMath: function(x,y) {
      return 'B result is: ' + a.logBase(x,y);
    }
  };
});

var C = new Module('C',[A],function(a) {
  return {
    doMath: function(x,y) {
      return 'C result is: ' + a.logBase(y,x);
    }
  };
});

var D = new Module('D',[B,C],function(b,c) {
  return {
    run: function(x,y) {
      console.log(b.doMath(x,y));
      console.log(c.doMath(x,y));
    }
  };
});

如今咱們有了一個鑽石模型圖: D 依賴於 B 和 C,而它們兩個又依賴於 A。這就意味着咱們能夠加載 A,而後並行加載 B 和 C,當 B 和 C 都加載完後,咱們就能夠加載 D 了。可是,咱們但願咱們的工具能夠幫咱們計算出來,而不是咱們本身來實現這個策略.

咱們能夠經過把模塊建模爲 LazyPromise 的子類後很容易的實現。它的構造函數能夠經過使用前面的列表 promise 輔助函數來取得它的依賴模塊,而後在某一個延時後建立這些依賴模塊來模擬異步加載的延時效果.

var DELAY = 1000;

var Module = function(name,deps,factory) {
  this._factory = function(callback) {
    list(deps).then(function(apis) {
      console.log('-- module LOAD: ' + name);
      setTimeout(function() {
        console.log('-- module done: ' + name);
        var api = factory.apply(this,apis);
        callback(null,api);
      },DELAY);
    });
  };
};
util.inherits(Module,LazyPromise);

由於 Module 是一個 LazyPromise,單純定義模塊並不會加載任何東西回來。只有當咱們須要開始使用的時候,加載纔會執行:

D.then(function(d) { d.run(1000,2) });

// prints:
//
// -- module LOAD: A
// -- module done: A
// -- module LOAD: B
// -- module LOAD: C
// -- module done: B
// -- module done: C
// -- module LOAD: D
// -- module done: D
// B result is: 9.965784284662087
// C result is: 0.10034333188799373

正如你所見到的,A 首先加載,當它完成後 B 和 C 開始同時下載,而後當它們都加載完後 D 開始加載,正如咱們想要的那樣。若是你只是執行 C.then(function() {}),你能夠看到只有 A 和 C 加載; 關係圖裏沒須要用到的是沒有加載的.

因此,基本上不須要太多代碼,只須要定義好懶 promises 的關係圖,咱們就實現了一個正確的模塊加載器。咱們使用的是函數式編程裏面的定義值的依賴關係這種方式,而不是顯式控制程序執行順序的方式來解決問題,而且這種方式比起本身控制執行流程更加容易。你能夠給出任何非循環依賴關係圖來讓這個模塊加載庫幫你優化執行順序.

這纔是 promises 的真正強大之處。它們並不只僅從語法層面減小代碼嵌套。它們讓你再更高的層面來爲你的問題抽象建模,和讓你的工具幫你作更多的工做。事實上,那應該是咱們必須向咱們的軟件提出的要求。若是 Node 真的但願把並行編程更容易的話,它們應該從新考慮一下 promises.

相關文章
相關標籤/搜索