第33章 Generator 函数的异步应用

1 传统方法

1.1 概念

        比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步

        连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。

 1.2 异步编程

        ES6 诞生以前,异步编程的方法,大概有下面四种。

(1)回调函数

        所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。

它的问题出现在多个回调函数嵌套。多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为"回调函数地狱"。

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {//等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行
  if (err) throw err;
  console.log(data);
});

//读取A文件之后,再读取B文件
fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
});

(2)Promise 对象 

将回调函数的嵌套,改成链式调用

var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {//最大问题是代码冗余
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

(3)事件监听

(4)发布/订阅

2 Generator 函数

2.1 协程

        "协程"(coroutine),意思是多个线程互相协作,完成异步任务。协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作。

协程有点像函数,又有点像线程。它的运行流程大致如下。

  • 第一步,协程A开始执行。
  • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B
  • 第三步,(一段时间后)协程B交还执行权。
  • 第四步,协程A恢复执行。
//读取文件的协程
function* asyncJob() {
  // ...其他代码
  var f = yield readFile(fileA);//yield命令是异步两个阶段的分界线
  // ...其他代码
}

2.2 协程的 Generator 函数

2.2.1 实现

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

function* gen(x) {
  var y = yield x + 2;
  return y;
}
var g = gen(1);//返回一个内部指针(即遍历器)g
g.next() // { value: 3, done: false }:next方法的作用是分阶段执行Generator函数
g.next() // { value: undefined, done: true }

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

next返回值的 value 属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。

function* gen(x){
  var y = yield x + 2;
  return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

 Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。出错的代码与处理错误的代码,实现了时间和空间上的分离

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

2.2.3 异步任务的封装

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因

虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

var fetch = require('node-fetch');
function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}
var g = gen();
var result = g.next();//执行异步任务的第一阶段
result.value.then(function(data){
  return data.json();
}).then(function(data){//Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法
  g.next(data);
});

3 Thunk 函数

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

3.1 参数的求值策略

传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失

var x = 1;
function f(m) {
  return m * 2;
}
f(x + 5)

//"传值调用"(call by value),即在进入函数体之前,就计算x + 5的值,再将这个值传入函数f。
// 传值调用时,等同于
f(6)

//“传名调用”(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。
// 传名调用时,等同于
(x + 5) * 2

3.2 Thunk 函数

3.2.1 含义

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

function f(m) {
  return m * 2;
}
f(x + 5);

// 等同于
var thunk = function () {
  return x + 5;
};
function f(thunk) {
  return thunk() * 2;
}

3.2.2 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 函数的形式。

// ES5版本
var Thunk = function(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(fileA)(callback);

function f(a, cb) {
  cb(a);
}
const ft = Thunk(f);
ft(1)(console.log) // 1

3.2.3 Generator 函数的流程管理

Thunk 函数现在可以用于 Generator 函数的自动流程管理。使得Generator 函数可以自动执行。生产环境的转换器,建议使用 Thunkify 模块。

function* gen() {
  // ...
}
var g = gen();//gen会自动执行完所有步骤,这不适合异步操作
var res = g.next();
while(!res.done){
  console.log(res.value);
  res = g.next();
}

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var gen = function* (){
  var r1 = yield readFileThunk('/etc/fstab');//yield命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

//Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数。 Generator 函数的执行过程,其实是将同一个回调函数,反复传入next方法的value属性。这使得我们可以用递归来自动完成这个过程。
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {
  if (err) throw err;
  var r2 = g.next(data);
  r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

3.2.4 Thunk 函数的自动流程管理

自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。

不管内部有多少个异步操作,直接把 Generator 函数传入run函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数

function run(fn) {//Generator 函数的自动执行器
  var gen = fn();
  function next(err, data) {
    var result = gen.next(data);//next函数就是 Thunk 的回调函数
    if (result.done) return;//判断 Generator 函数是否结束
    result.value(next);//没结束,就将next函数再传入 Thunk 函数(result.value属性)
  }
  next();
}
function* g() {
  // ...
}
run(g);

var g = function* (){
  var f1 = yield readFileThunk('fileA');
  var f2 = yield readFileThunk('fileB');
  // ...
  var fn = yield readFileThunk('fileN');//g封装了n个异步的读取文件操作
};
run(g);//异步操作就会自动完成

4 co 模块

4.1 基本用法

Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点。

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。

用于 Generator 函数的自动执行。Generator 函数只要传入co函数,就会自动执行co函数返回一个Promise对象,因此可以用then方法添加回调函数。

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 函数执行完成');
});

4.2 基于 Promise 对象的自动执行

var fs = require('fs');
var readFile = function (fileName){//把fs模块的readFile方法包装成一个 Promise 对象
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, 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();
//手动执行上面的 Generator 函数,其实就是用then方法,层层添加回调函数。
g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
});
//自动执行器
function run(gen){
  var g = gen();
  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;//Generator 函数还没执行到最后一步,next函数就调用自身
    result.value.then(function(data){
      next(data);
    });
  }
  next();
}
run(gen);

4.3 处理并发的异步操作

co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。这时,要把并发的操作都放在数组或对象里面,跟在yield语句后面。

// 数组的写法
co(function* () {
  var res = yield [
    Promise.resolve(1),
    Promise.resolve(2)
  ];
  console.log(res);
}).catch(onerror);

// 对象的写法
co(function* () {
  var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
  };
  console.log(res);
}).catch(onerror);

co(function* () {
  var values = [n1, n2, n3];
  yield values.map(somethingAsync);
});
function* somethingAsync(x) {//允许并发三个somethingAsync异步操作,等到它们全部完成,才会进行下一步。
  // do something async
  return y
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值