回调函数是指令式的,Promise 是函数式的:Node 错失的最大时机

我之前都有打仗过关于 Promise 的一些文章,然则对它的以为并不大。由于以为虽然回调作风确切有题目,但我写的代码还没有庞杂到那种水平,所以,要去运用的以为并不猛烈。

然则,背面碰到一个题目真的彷佛用回调的作风来写的话,会比较蹩脚。加上看到了这一篇从另一正面来看 Promise 对函数式编程的头脑方面的转变,以为很不错。值得一看,所以在有别的大神也翻译过的状况下,自身也译一次,趁便深切进修。

原文链接: Callbacks are imperative, promises are functional: Node’s biggest missed opportunity

Promise的实质就是他们不跟着环境的变化而变化。

—— Frank Underwood,‘纸牌屋’

你经常会听到说 JavaScript 是一门 “函数式” 编程言语。一般我们如许形貌它的时刻是由于函数在它内里是作为 “一等国民” 而存在的。然则别的 “函数式” 编程言语内里的特征,比方:数据不可转变,代数范例体系,运用迭代优于轮回,防止副作用都一切疏忽了。虽然函数作为 “一等国民” 黑白常有用的,而且决议用户能够在须要的时刻运用函数式作风来编写代码。然则 JS 是函数式的看法却经常疏忽了函数式编程的中心头脑:面向值编程。

“函数式编程” 的定名实在会发作误导,以至于人们以为它的意义在于,相对于 “面向对象编程” 来讲,它是 “面向函数编程”。然则如果面向对象编程是把一切东西都从对象角度斟酌,那函数式编程就是把一切东西都作为值来处置惩罚,而不仅仅是把函数斟酌为值。很显著,数值固然包括那些数字,字符,列表和别的数据值,但实在它也包括别的面向对象编程的粉丝一般没有斟酌过的一些东西:IO 操纵和别的副作用,GUI 事宜流,空值搜检,以至函数挪用的递次。如果你曾听说过 “可编程分号” 的话,你应当晓得我想说的是什么了.

函数式编程最大的优点是它是声明式的。在敕令式编程内里,我们须要写一系列的指令来通知盘算机是怎样去完成我们想要做的事变的。在函数式编程内里,我们只是须要形貌值之间的盘算关联,盘算机就会自身想办法得出须要的盘算指令递次。

如果你运用过 Excel 的话,你实在已用过函数式编程了:你只须要形貌一个图表内里的值,是怎样互相盘算出来的。当有新数据插进去的时刻,Excel 就会自身得出图内外有什么处所的值和效果要更新,而你并不须要再为它写出任何指令,它也能够帮你盘算出来。

在论述了这些基础概念的基础上,我想申明一下我以为 Node.js 在设想上最大的失误是什么: 这就是在它的设想早期,决议了倾向于运用回调作风的 API 而不是 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.

    原文作者:鹄思乱想
    原文地址: https://segmentfault.com/a/1190000000356347
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞