JavaScript 系列 - Generator 函数的异步应用

78 阅读3分钟

协程

多个线程互相协作,完成异步任务。运行流程大致如下

  • 协程A开始执行
  • 协程A执行到一半,进入暂停,执行权转移到协程B
  • 一段时间后协程B交还执行权
  • 协程A恢复执行

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) {
    g.next(data);
  });

流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)

Thunk 函数

参数的求值策略

传值调用

C 语言

对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。

传名调用

Thunk 函数的含义

编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

function f(m) {
  return m * 2;
}

f(x + 5);

// 等同于

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

function f(thunk) {
  return thunk() * 2;
}

JavaScript 语言的 Thunk 函数

JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,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);
    }
  };
};

Thunkify 模块

thunkify只允许回调函数执行一次

Generator 函数的流程管理

Thunk 函数现在可以用于 Generator 函数的自动流程管理

var fs = require("fs");
var thunkify = require("thunkify");
var readFileThunk = thunkify(fs.readFile);

var gen = function* () {
  var r1 = yield readFileThunk("/etc/fstab");
  console.log(r1.toString());
  var r2 = yield readFileThunk("/etc/shells");
  console.log(r2.toString());
};
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);
  });
});

Thunk 函数的自动流程管理

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

co 模块

Generator 函数的自动执行工具

使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象

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);
co(gen).then(function () {
  console.log("Generator 函数执行完成");
});

co 模块的原理

  • 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
  • Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

基于 Promise 对象的自动执行

var fs = require("fs");

var readFile = function (fileName) {
  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();

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;
    result.value.then(function (data) {
      next(data);
    });
  }

  next();
}

run(gen);

处理并发的异步操作

// 数组的写法
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);

处理 Stream

const co = require("co");
const fs = require("fs");

const stream = fs.createReadStream("./les_miserables.txt");
let valjeanCount = 0;

co(function* () {
  while (true) {
    const res = yield Promise.race([
      new Promise((resolve) => stream.once("data", resolve)),
      new Promise((resolve) => stream.once("end", resolve)),
      new Promise((resolve, reject) => stream.once("error", reject)),
    ]);
    if (!res) {
      break;
    }
    stream.removeAllListeners("data");
    stream.removeAllListeners("end");
    stream.removeAllListeners("error");
    valjeanCount += (res.toString().match(/valjean/gi) || []).length;
  }
  console.log("count:", valjeanCount); // count: 1120
});