JS-Generator 执行器的实现

Generator 是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,自动交回执行权。

两种方法可以实现执行器:

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

Thunk 函数实现 generator 执行器

在js中实现异步的方式为回调函数,比如读取文件的操作:

fs.readFile(path, function callback(err, data) {
  console.log(data);
});

上面的 fs.readFile 有两个参数,第一个路径 path,第二个为回调函数;如果将 readFile 函数改成单参数的函数,类似于函数柯里化,那么结果如下:

function thunkFile(path) {
  return function(cb) {
    return fs.readFile(path, cb)
  }
}

经过转换,变为了单参数的函数,每次只接受一个参数,调用 thunkFile 函数读取文件:

let thunk = thunkFile('./a.txt');
thunk(function(err, data) {
  if(err) return;
  console.log(data);
});

其中 thunk 就是所谓的 thunk 函数。所谓的 thunk 函数也就是接受一个回调函数为参数的函数。

任何接受回调函数为参数的函数,都可以写成 thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。

// es5版本
function Thunk(fn) {
  return function() {
    let args = [...arguments];
    return function(cb) {
      args.push(cb);
      return fn.apply(this, args);
    };
  }
}

generator 要想实现异步操作,可以在 yield 后面接上异步操作的表达式,但是问题是如何才能够保证前一步 yield 执行完了再执行下一步 yield 呢,仅仅自执行肯定是不行的,例如下面的自执行函数

function* gen() {
  let result1 = yield readFile('./a.txt');
  let result2 = yield readFile(`${result1}.txt`);
  console.log(result1);
  console.log(result2);
}

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
}

上面代码中,Generator 函数gen会自动执行完所有步骤。但是,这不适合异步操作。因为 result1 还没有返回,第二个 yield 可能就已经执行了。必须要让 第二个yield 拿到了第一个 yield 的结果之后再执行,这就要求必须在第一个 yield 的回调函数中(因为回调函数中有结果)交回函数的执行权,即执行 g.next(),这样才能够保证第一个 yield 有了结果之后再执行第二个 yield。

按照这个思路来手动执行上面的 generator 函数:

let g = gen();
let info1 = g.next();
info1.value(function(err, data) {
  if(err) throw err;
  let info2 = g.next(data);
  info2.value(function(err, data) {
    if(err) throw err;
    g.next(data)
  });
})

仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入next方法的value属性。这使得我们可以用递归来自动完成这个过程。

到这里,可以自己写一个通用的基于 Thunk 函数的 generator 执行器。

function run(genFn) {
  let gen = genFn();
  function next(err, data) {
    let result = gen.next();
    if(result.done) return result.value;
    result.value(next);
  }
  next();
}

上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next 方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。

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

var g = function* (){
  var f1 = yield readFileThunk('fileA');
  var f2 = yield readFileThunk('fileB');
  // ...
  var fn = yield readFileThunk('fileN');
};

run(g);

Thunk 函数并不是实现 generator 函数自执行的唯一办法,因为自执行的关键是,必须有一种机制,自动控制 generator 函数的流程,接收和交换函数的执行权。回调函数可以做到这一点, Promise 也可以。

Promise 实现 generator 执行器

Promise 实现 generator 执行器的原理是:将异步操作包装成 Promise 对象,用 then 方法中交回执行权

例如文件读取是异步I/O操作,先将读取操作包装成一个promise对象,然后使用then来获取执行权。

let fs = require('fs');
function readFile(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, (err, data) => {
      if(err) return reject(err);
      resolve(data); 
    });
  });
}

function* genReadFile() {
  let f1 = yield readFile('./a.txt');
  let f2 = yield readFile('./b.txt');
}

基于 promise 的 generator 自执行器。

function run(genFn) {
  let gen = genFn();

  function next(data) {
    let result = gen.next(data);
    result.value.then(_data => {
      next(_data);
    });
  }

  next();
}

// 调用执行器
run(genReadFile);

总结

实现异步 generator 执行器的关键是要确保上一个 yield 返回结果了之后,再继续调用生成器对象的 next() 方法执行下一个 yield。对于异步操作来说,只有在回调函数或者 promise.then 中可以保证当前的异步执行完毕有了结果,所以有了基于 Thunk 函数和promise 对象这两种方式的 generator 执行器。

故,要使用 generator 执行器的话,generator 中的 yield 后面必须接 Thunk 函数或者 promise 对象才行。

你可能感兴趣的:(javaScript)