ES6 —(Generator 函数的异步应用)

来源:互联网 发布:历史 知乎 编辑:程序博客网 时间:2024/05/16 03:37

1、异步概念

  所谓 “异步”,简单说就是一个任务不是连续完成的,可以理解为该任务被人分成两段,先执行一段,然后转而执行其他任务,等做好了准备,再回头执行第二段。
  相应的,连续的执行其他任务就叫做同步。由于连续执行,不能插入其他任务。

  ES6 诞生以前,异步编程的方式,大概有四种,

  • 回调函数
  • 事件监听
  • 发布 / 订阅
  • Promise 对象

1.1、回调函数

  JavaScript 语言对异步编程的实现,就是回调函数。所谓的回调函数,就是把任务的第二段单独写一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
  一个有趣的问题是,Node 约定,回调函数的第一个参数,必须是错误对象 err (如果没有错误,该参数就是 null)。这是因为执行分为两段,第一段执行完后,任务所在的上下文环境已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕获,只能当做参数,传入第二段。
  回调函数本身没有问题,它的问题出现在多个回调函数嵌套。回调函数多重嵌套,使得多个异步操作形成了强耦合,只要有一种操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为 “回调函数地狱”。

1.2、Promise

  Promise 对象是为了解决 “回调函数地狱” 而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
  Promise 的写法只是回调函数的改进,使用 then 方法后,异步任务的两段执行看的更清楚了,除此之外,并无新意。
  Promise 的最大问题是代码的冗余,原来的任务被 Promise 包装一下,不管什么操作,一眼看过去都是一堆 then,原来的语义变得很不清楚。

2、Generator 函数

2.1、协程

  传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做 “协程” ,意思是多个线程相互协作,完成异步任务。
  协程有点像函数,又有点像线程。它的运行流程大致如下:

  • 第一步,协程 A 开始执行。
  • 第二步,协程 A 执行到一半,进入暂停状态,执行权转移到协程 B。
  • 第三步,(一段时间后)协程 B 交还执行权。
  • 第四步,协程 A 恢复执行。

上面流程的协程 A 就是异步执行,因为它分为两段(或多段)执行。

(1)协程与子例程的差异
  传统的子例程采用堆栈式 “后进先出” 的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)处于暂停状态,线程(或函数)之间可以交换执行权,也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行,交换执行权的线程叫做协程。
  从实际上看,在内存中,子例程只使用一个栈,而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占内存为代价,实现多任务的并行。

(2)协程与普通线程的差异
  不难看出,协程适合用于多任务运行的环境,在这个意义上,它与普通的线程很相似,都有自己的执行上下文,可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
  由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程后,每个任务可以保持自己的调用栈。这样做的好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出现错误,原始的调用栈早就结束了。

(3)协程的 Generator 函数实现
  Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
  整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。

2.2、Generator 函数的数据交换和错误处理

  Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
  next 返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,向 Generator 函数体内输入数据。
  Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

3、Thunk 函数

  Thunk 函数是自动执行 Generator 函数的一种方式。

3.1、Thunk 函数的含义

(1)参数的求值策略
  求值策略,即函数的参数到底应该何时求值。分为两种:

function f(m){ return m + 2; }var x = 1;f(x + 5)
  • 传值调用:即在进入函数体之前,就计算参数表达式的值,再将这个值传入函数。也就是先计算 x + 5 的值,再将 6 传入函数 f。
  • 传名调用:即直接将表达式传入函数,只有在用到它的时候求值,也就是说直接将表达式 x + 5 传入函数 f。

(2)Thunk 函数的含义
  编译器的 “传名调用” 的实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。即 Thunk 函数是一种 “传名调用” 的一种实现策略,用来替换某个表达式。

var thunk = function(){    return x + 5;}function f(thunk){    return thunk() + 2;}

3.2、JavaScript 语言的 Thunk 函数

  JavaScript 语言是传值调用,它的 Thunk 含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

// 正常版本的 readFile (多参数版本)fs.readFile(fileName, callback);// Thunk 版本的 readFile (单参数版本)var Thunk = function(fileName){    return function(callback){        return fs.readFile(fileName, callback);    };};var readFileThunk = Thunk(fileName);readFileThunk(callback);

经过转换器处理,它变成一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数。
  任何函数,只要参数有回调函数,就能写成 Thunk 函数形式。

// ES5 版本var thunk = fuunction(fn){    return function(){        var args = Array.prototype.slice.call(arguments);        return function(callback){            args.push(callback);            return fn.apply(this,args);        }    }}// ES6 版本const Thunk = function(fn){    return function (...args){        return function(callback){            return fn.call(this,...args, callback);        };    };};//生成 fs.readFile 的 Thunk 函数var readFileThunk = Thunk(fs.readFile);readFileThunk(fileName)(callback);

  生成环境的转换器,建议使用 Thunkify 模块。该模块的源码与上一面那个简单的 Thunk 转换器非常像。

// 安装npm install thunkify// 使用var thunkify = require('thunkify');var fs = require('fs');var read = thunkify(fs.readFile);read('package.json')(fucntion(err, str){ .... });

Thunkify 源码:

function thunkify(fn){    return function(){        var args = new Array(arguments.length);        var ctx = this;        for(var i = 0; i < args.length; ++i){            args[i] = arguments[i];        }        return function(done){            var called;            args.push(function(){                if(called) return;                called = true;                done.apply(null, argments);            });            try{                fn.apply(ctx, args);            } catch (err){                done(err);            }        }    }}

Thunkify 的源码主要多了一个检查机制,变量 called 确保回调函数只运行一次。这样的设计与上下文的 Generator 函数相关。

3.3、Thunk 函数的自动流程管理

  Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于Thunk 函数的 Generator 执行器。

function run(fn){    var gen = fn();    function next(err, data){        var result = gen.next(data);        if(result.done) return;        result.value(next);    }    next();}function* g(){    ...}run(g);

上面代码的 run 函数,就是一个 Generator 函数的自动执行器,内部的 next 函数就是 Thunk 的回调函数。 next 函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没有结束,就将 next 函数再传入 Thunk 函数(result.value属性),否则就直接退出。
  有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入 run 函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在 yield 命令后面的必须是 Thunk 函数。

var fs = require('fs');var thunkify = require('thunkify');var readFileThunk = thunkify(fs.readFile);  var g = function* (){    var f1 = yield readFileThunk('fileA');    var f2 = yield readFileThunk('fileB');    var f3 = yield readFileThunk('fileC');    ....}run(g);

4、co 模块

  co 模块也是用于 Generator 函数的自动执行。下面是一个 Generator 函数,用于依次读取两个文件。

var gen = function* (){    var f1 = yield readFile('/etc/fstab');    var f2 = yield readFile('/etc/shells');    console.log(f1.toString());    console.log(f2.toString());};var co = require('co');co(gen).then(function(){    console.log('Generator 函数执行完成');});

从上面代码可以看出,co 模块可以让你不用编写 Generator 函数的执行器。只要将 Generator 函数传入 co 模块就会自动执行。co 函数返回一个 Promise 对象,因此可以用 then 方法添加回调函数。

4.1、co 模块的原理

  Generator 就是一个异步操作的容器,它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点:
(1)回调函数:将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象:将异步操作包装成 Promise 对象,用 then 方法交回执行权。

  co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块,使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。

4.2、基于 Promise 对象的自动执行

  还是沿用上面的例子。首先,把 fs 模块的 readFile 方法包装成一个 Promise 对象。

var fs = require('fs');var readFile = function (fileName){    return new Promise(function (resolve, reject){        fs.readFile(fileName, function(err, data){            if(error) return reject(error);            resolve(data);        });    });};var gen = function* (){    var f1 = yield readFile('/etc/fstab');    var f2 = yield readFile('/etc/shells');    console.log(f1.toString());    console.log(f2.toString());};var g = gen();g.next().value.then(function(data){    g.next(data).value.then(function(data){        g.next(data);    });});

手动执行其实就是用 then 方法,层层添加回调函数。理解这一点,就可以写一个自动执行器。

function run(gen){    var g = gen();    function next(data){        var result = g.next(data);        if(result.done) return resule.value;        result.value.then(function(data){            next(data);        });    }    next();}run(gen);

4.3、co 模块源码

function co(gen) {  var ctx = this;    // 第一部分  return new Promise(function(resolve, reject) {    // 第二部分    if (typeof gen === 'function') gen = gen.call(ctx);    if (!gen || typeof gen.next !== 'function') return resolve(gen);    // 第三部分    onFulfilled();    function onFulfilled(res) {      var ret;      try {        ret = gen.next(res);      } catch (e) {        return reject(e);      }      next(ret);    }  });    // 第四部分    function next(ret) {      if (ret.done) return resolve(ret.value);      var value = toPromise.call(ctx, ret.value);      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);      return onRejected(        new TypeError(          'You may only yield a function, promise, generator, array, or object, '          + 'but the following object was passed: "'          + String(ret.value)          + '"'        )      );    }}

第一部分,co 模块接受 Generator 函数作为参数,返回一个 Promise对象。
第二部分,在返回的 Promise 对象里面,co 先检查参数 gen 是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将 Promise 对象的状态改为 resolved。
第三部分,co 将 Generator 函数的内部指针对象的 next 方法,包装成 onFulfilled 函数。这主要是为了能够捕捉抛出的错误。
第四部分,就是最关键的 next 函数,它会反复调用自身。next 函数只有四行命令。
  第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。
  第二行,确保每一步的返回值,是 Promise 对象。
  第三行,使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
  第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 Rejected ,从而终止执行。

阮一峰:ECMAScript 6入门

原创粉丝点击