指令式Callback,函數式Promise:對node.js的一聲嘆息

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

promises 天生就不會受不斷變化的狀況影響。
-- Frank Underwood, ‘House of Cards’數據庫

人們常說Javascript是'函數式'編程語言。而這僅僅由於函數是它的一等值,可函數式編程的不少其餘特性,包括不可變數據,遞歸比循環更招人待見,代數類型系統,規避反作用等,它都不俱備。儘管把函數做爲一等公民確實管用,也讓碼農能夠根據本身的須要決定是否採用函數式的風格編程,但宣稱JS是函數式的每每會讓JS碼農們忽略函數式編程的一個核心理念:用值編程。express

'函數式編程'是一個使用不當的詞,由於它會讓人們覺得這是'用函數編程'的意思,把它跟用對象編程相對比。但若是面向對象編程是把一切都看成對象,那函數式編程是把一切都看成值,不只函數是值,而是一切都是值。這其中固然包括顯而易見的數值、字符串、列表和其它數據,還包括咱們這些OOP狗通常不會當作值的其它東西:IO操做和其它反作用,GUI事件流,null檢查,甚至是函數調用序列的概念。若是你曾據說過'可編程的分號'1這個短語,你應該就能明白我在說什麼了。npm

1單子。 In functional programming, a monad is a structure that represents computations. A type with a monad structure defines what it means to chain operations of that type together. This allows the programmer to build pipelines that process data in steps, in which each action is decorated with additional processing rules provided by the monad. As such, monads have been described as "programmable semicolons"; a semicolon is the operator used to chain together individual statements in many imperative programming languages, thus the expression implies that extra code will be executed between the statements in the pipeline. Monads have been also explained with a physical metaphor as assembly lines, where a conveyor belt transports data between functional units that transform it one step at a time. http://en.wikipedia.org/wiki/Monad_(functional_programming)編程

最好的函數式編程是聲明式的。在指令式編程中,咱們編寫指令序列來告訴機器如何作咱們想作的事情。在函數式編程中,咱們描述值之間的關係,告訴機器咱們想計算什麼,而後由機器本身產生指令序列完成計算。api

用過excel的人都作過函數式編程:在其中經過建模把一個問題描繪成一個值圖(如何從一個值推導出另外一個)。當插入新值時,Excel負責找出它對圖會產生什麼影響,並幫你完成全部的更新,而無需你編寫指令序列指導它完成這項工做。數組

有了這個定義作依據,我要指出node.js一個最大的設計失誤,最起碼我是這樣認爲的:在最初設計node.js時,在肯定提供哪一種方式的API式,它選擇了基於callback,而不是基於promise。promise

全部人都在用 [callbacks]。若是你發佈了一個返回promise的模塊,沒人會注意到它。人們甚至不會去用那樣一個模塊。 服務器

若是我要本身寫個小庫,用來跟Redis交互,而且這是它所作的最後一件事,我能夠把傳給個人callback轉給Redis。並且當咱們真地遇到callback hell之類的問題時,我會告訴你一個祕密:這裏還有協同hell和單子hell,而且對於你所建立的任何抽象工具,只要你用得足夠多,總會遇到某個hell。網絡

在90%的狀況下咱們都有這種超級簡單的接口,因此當咱們須要作某件事的時候,只要小小的縮進一下,就能夠搞定了。而在遇到複雜的狀況時,你能夠像npm裏的其它827個模塊同樣,裝上async。

--Mikeal Rogers, LXJS 2012

Node宣稱它的設計目標是讓碼農中的屌絲也能輕鬆寫出反應迅速的併發網絡程序,但我認爲這個美好的願望撞牆了。用Promise可讓運行時肯定控制流程,而不是讓碼農絞盡腦汁地明確寫出來,因此更容易構建出正確的、併發程度最高的程序。

編寫正確的併發程序歸根結底是要讓儘量多的操做同步進行,但各操做的執行順序仍能正確無誤。儘管Javascript是單線程的,但因爲異步,咱們仍然會遇到競態條件:全部涉及到I/O操做的操做在等待callback時都要把CPU時間讓給其餘操做。多個併發操做都能訪問內存中的相同數據,對數據庫或DOM執行重疊的命令序列。藉助promise,咱們能夠像excel那樣用值之間的相互關係來描述問題,從而讓工具幫你找出最優的解決方案,而不是你親自去肯定控制流。

我但願澄清你們對promise的誤解,它的做用不只是給基於callback的異步實現找一個語法更清晰的寫法。promise以一種全新的方式對問題建模;它要比語法層面的變化更深刻,其實是在語義層上改變了解決問題的方式。

我在兩年前曾寫過一篇文章,promises是異步編程的單子。那篇文章的核心理念是單子是組建函數的工具,好比構建一個以上一個函數的輸出做爲下一個函數輸入的管道。這是經過使用值之間的結構化關係來達成的,它的值和彼此之間的關係在這裏仍要發揮重要做用。

我仍將藉助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類型的路徑名和一個callback函數,它沒有任何返回值。callback函數有兩個參數,Error(可能爲null)和包含文件內容的Buffer,也是沒有任何返回值。咱們能夠把readFile的類型表示爲:

readFile :: String -> Callback -> ()

() 在 Haskell 中表示 null 類型。callback 自己是另外一個函數,它的類型簽名是:

Callback :: Error -> Buffer -> ()

把這些都放到一塊兒,則能夠說readFile以一個String和一個帶着Buffer調用的函數爲參數:

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

好,如今請想象一下Node使用promises是什麼狀況。對於readFile而言,就是簡單地接受一個String類型的值,並返回一個Buffer的promise值。

readFile :: String -> Promise Buffer

說得更歸納一點,就是基於callback的函數接受一些輸入和一個callback,而後用它的輸出調用這個callback函數,而基於promise的函數接受輸入,返回輸出的promise值:

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

基於callback的函數返回的那些null值就是基於callback編程之因此艱難的源頭:基於callback的函數什麼都不返回,因此難以把它們組裝到一塊兒。沒有返回值的函數,執行它僅僅是由於它的反作用 -- 沒有返回值或反作用的函數就是個黑洞。因此用callback編程天生就是指令式的,是編寫以反作用爲主的過程的執行順序,而不是像函數應用那樣把輸入映射到輸出。是手工編排控制流,而不是經過定義值之間的關係來解決問題。所以使編寫正確的併發程序變得艱難。

而基於promise的函數與之相反,你總能把函數的結果看成一個與時間無關的值。在調用基於callback的函數時,在你調用這個函數和它的callback被調用之間要通過一段時間,而在這段時間裏,程序中的任何地方都找不到表示結果的值。

fs.readFile('file1.txt',
  // 時光流逝...
  function(error, buffer) {
    // 如今,結果忽然跌落在凡間
  }
);

從基於callback或事件的函數中獲得結果基本上就意味着你「要在正確的時間正確的地點」出現。若是你是在事件已經被觸發以後才把事件監聽器綁定上去,或者把callback放錯了位置,那上帝也罩不了你,你只能看着結果從眼前溜走。這對於用Node寫HTTP服務器的人來講就像瘟疫同樣。若是你搞錯了控制流,那你的程序就只能崩潰。

而Promises與之相反,它不關心時間或者順序。不管你在promise被resolve以前仍是以後附上監聽器,都不要緊,你總能從中獲得結果值。所以,返回promises的函數立刻就能給你一個表示結果的值,你能夠把它看成一等數據來用,也能夠把它傳給其它函數。不用等着callback,也不會錯過任何事件。只要你手中握有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中提取出來,把它交給另外一個函數:從單子的角度來看就是bind函數。在上面的代碼中,咱們徹底看不出來該值什麼時候可用,或代碼執行的順序是什麼,它只表達了某種依賴關係:要想在日誌中輸出某個值,那你必須先知道這個值是什麼。程序執行的順序是從這些依賴信息中推導出來的。二者的區別其實至關微妙,但隨着咱們討論的不斷深刻,到文章末尾的lazy promises時,這個區別就會變得越發明顯。

到目前爲止,你看到的都是些無足輕重的東西;一些彼此之間幾乎沒什麼互動的小函數。爲了讓你瞭解promises爲何比callback更強大,咱們來搞點更須要技巧性的把戲。假設咱們要寫段代碼,用fs.stat()取得一堆文件的mtimes屬性。若是這是異步的,咱們只須要調用paths.map(fs.stat),但既然跟異步函數映射難度較大,因此咱們把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的函數都有sync版本,但不少其它I/O操做都沒有這種待遇。因此,請淡定地坐下來看我把戲法變完。)

一切都很美好,可是,新需求來了,咱們還須要獲得file1的size。只要再stat就能夠了:

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

需求知足了,但這個跟size有關的任務要等着前面整個列表中的文件都處理完纔會開始。若是前面那個文件列表中的任何一項出錯了,很不幸,咱們根本就不可能獲得第一個文件的size。這可就大大地壞了,因此,咱們要試試別的辦法:把第一個文件從文件列表中拿出來單獨處理。

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

這樣也行,但如今咱們已經不能把這個程序稱爲並行化的了:它要用更長的時間,由於在處理完第一個文件以前,文件列表的請求處理得一直等着。以前它們還都是併發運行的。另外咱們還不得不處理下數組,以即可以把第一個文件提出來作特別的處理。

Okay,最後的成功一擊。咱們知道須要獲得全部文件的stats,每次命中一個文件,若是成功,則在第一個文件上作些工做,而後若是整個文件列表都成功了,則要在那個列表上作些工做。帶着對問題中這些依賴關係的認識,用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
});

這就對了:每次一個文件,全部工做都是並行的,第一個文件的結果跟其餘的不要緊,而相關任務能夠儘早執行。Mission accomplished!

好吧,實際上並不盡然。這個太醜了,而且當問題變得更加複雜後,這個顯然不易於擴展。爲了正確解決問題,要考慮不少東西,並且這個設計意圖也不顯眼,後期維護時極可能會把它破壞掉,後續任務跟如何完成所需工做的策略混雜在一塊兒,並且咱們不得不動用一些比較複雜的數組分割操做來應對這個特殊情況。啊哦!

這些問題的根源都在於咱們用控制流做爲解決辦法的主體,若是用數據間的依賴關係,就不會這樣了。咱們的思路不是「要運行這個任務,我須要這個數據」,沒有把找出最優路徑的工做交給運行時,而是明確地向運行時指出哪些應該並行,哪些應該順行,因此咱們獲得了一個特別脆弱的解決方案。

那promises怎麼幫你脫離困境?嗯,首先要有能返回promises的文件系統函數,用callback作參數的那套東西不行。但在這裏咱們不要手工打造一套文件系統函數,經過元編程做個能轉換一切函數的東西就行。好比,它應該接受類型爲:

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

(這不是特別通用,但對咱們來講夠了.)

如今咱們能夠對問題從新建模。咱們須要作的所有工做基本就是將一個路徑列表映射到一個stats的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的列表以後,你能夠徑直挑出第一個文件的stat作些處理:

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

因此在用上promise值後,咱們已經解決了大部分問題:全部文件的stat都是併發進行的,而且訪問全部文件的stat都和其餘的無關,能夠從數組中直接挑咱們想要的任何一個,不止是第一個了。在前面那個方案中,咱們必須在代碼裏明確寫明要處理第一個文件,想換文件時改起來不是那麼容易,但用promises列表就容易多了。

謎底尚未徹底揭曉,在獲得全部的stat結果以後,咱們該作什麼?在以前的程序中,咱們最終獲得的是一個Stat對象的列表,而如今咱們獲得的是一個Promise Stat 對象的列表。咱們想等着全部這些promises都被兌現(resolve),而後生出一個包含全部stats的列表。換句話說,咱們想把一個promises列表變成一個列表的promise。

閒言少敘,咱們如今就給這個列表加上promise方法,那這個包含promises的列表就會變成一個promise,當它所包含的全部元素都兌現後,它也就兌現了。

// 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;
};

(這個函數跟 jQuery.when() 相似, 以一個promises列表爲參數,返回一個新的promise,當參數中的全部promises都兌現後,這個新的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
});

該方案的這種表示方式看起來要清楚得多了。藉助一點通用的粘合劑(咱們的promise輔助函數),以及已有的數組方法,咱們就能用正確、有效、修改起來很是容易的辦法解決這個問題。不須要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() 實質上是同樣的,只不過each()只是要執行效果,不關心返回值。徹底能夠用map()代替。

async.mapSeries() (如前所述,包括 async.eachSeries()) 至關於在promises列表上調用 reduce()。也就是說,你拿到輸入列表,並用reduce產生一個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的return 函數):

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

reduce()只是取出路徑列表中的每對目錄,用promise.then()根據上一步操做是否成功來執行路徑刪除操做。這樣能夠處理非空目錄:若是上一個promise因爲某種錯誤被rejecte了,操做鏈就會終止。用值之間的依賴關係限定執行順序是函數式語言藉助monads處理反作用的核心思想。

最後這個例子的代碼比async版本繁瑣得多,但不要被它騙了。關鍵是領會精神,要將彼此不相干的promise值和list操做結合起來組裝程序,而不是依賴定製的流程控制庫。如您所見,前一種方式寫出來的程序更容易理解。

準確地講,它之因此容易理解,是由於咱們把一部分思考的過程交給機器了。若是用async模塊,咱們的思考過程是這樣的:

  • A.程序中這些任務間的依賴關係是這樣的
  • B.所以各操做的順序必須是這樣
  • C.而後咱們把B所表達的意思寫成代碼吧

用promises依賴圖能夠跳過步驟B。代碼只要表達任務之間的依賴關係,而後讓電腦去設定控制流。換種說法,callback用顯式的控制流把不少細小的值粘到一塊兒,而promises用顯式的值間關係把不少細小的控制流粘到一塊兒。Callback是指令式的,promises是函數式的。

若是最終沒有一個完整的promises應用,而且是體現函數式編程核心思想 laziness的應用,咱們對這個話題的討論就不算完整。Haskell是一門懶語言,也就是說它不會把程序當成從頭運行到尾的腳本,而是從定義程序輸出的表達式開始,向stdio、數據庫中寫了什麼等等,以此向後推導。它尋找最終表達式的輸入所依賴的那些表達式,按圖反向探索,直到計算出程序產生輸出所需的一切。只有程序爲完成任務而須要計算的東西纔會計算。

解決計算機科學問題的最佳解決方案一般都是找到能夠對其建模的準確數據結構。Javascript有一個與之很是類似的問題:模塊加載。咱們只想加載程序真正須要的模塊,並且想盡量高效地完成這個任務。

在 CommonJS 和 AMD出現以前,咱們確實就已經有依賴的概念了,腳本加載庫有一大把。大多數的工做方式都跟前面的例子差很少,明確告訴腳本加載器哪些文件能夠並行下載,哪些必須按順序來。基本上都必須寫出下載策略,要想作到正確高效,那是至關困難,跟簡單描述腳本間的依賴關係,讓加載器本身決定順序比起來簡直太坑人了。

接下來開始介紹LazyPromise的概念。這是一個promise對象,其中會包含一個可能作異步工做的函數。這個函數只在調用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,兩個都到位後加載D。可是,咱們但願工具能本身找出這個順序,而不是由咱們本身寫出來。

這很容易實現,咱們把模塊看成LazyPromise的子類型來建模。它的工廠只要用咱們前面那個list 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;不在依賴關係圖中的模塊不會加載。

因此咱們幾乎沒怎麼寫代碼就建立了一個正確的優化模塊加載器,只要用lazy promises的圖就好了。咱們用函數式編程中值間關係的方式代替了顯式聲明控制流的方式,比咱們本身寫控制流容易得多。對於任何一個非循環得依賴關係圖,這個庫都能用來替你優化控制流。

這就是promises真正強大的地方。它不只能在語法層面上規避縮進金字塔,還能讓你在更高層次上對問題建模,而把底層工做交給工具完成。真的,那應該是咱們全部碼農對咱們的軟件提出的要求。若是Node真的想讓併發編程更容易,他們應該再好好看看promises。

相關文章
相關標籤/搜索