5

以前看过的内容,感觉忘得差不多,最近抽空又看了一次,果然书读百遍其义自见

Generator的执行

Generator函数可以实现函数内外的数据交换执行权交换

从第一次调用next开始,从函数头部开始执行,执行到第一个yield语句时,把执行权交出到函数外部,并返回该yield语句右值,同时在此处暂停函数

在下一次调用next时候(可以传递参数),把执行权返还给函数内部,同时把参数赋值给上一次暂停的yield语句的左值,并从该行到开始执行到下一个yield前,并一直循环该过程

需要注意的是,yield语句的左值,不能由右值赋值,如 let a = yield 3a 的值并不等于3,a 的只能由函数外部调用next时传入的参数赋值。


function test() {
    return 3;
}

function* gen(){
    console.log(0);
    
    let yield1 = yield 1;
    console.log('yield1 value: ', yield1);// yield1: 2
    
    let yield2 = yield test();
    console.log('yield2 value: ', yield2);// yield2: 4
    
    return 3;
}

let gen1 = gen();

let next1 = gen1.next();
console.log('next1 value: ', next1);// next: { value: 1, done: false }

let next2 = gen1.next(2);
console.log('next2 value: ', next2);// next: { value: 3, done: false }

let next3 = gen1.next(4);
console.log('next3 value: ', next3);// next: { value: undefined, done: true }

第一次调用

  • 从函数顶部开始往下执行,所以首先输出 console.log(0)
  • 然后执行 yield1 = yield 1,此时会把表达式右值返回, 即返回 1
  • 所以此时 next1 = {value: 1, done: false}, 接着输出 next1
  • gen函数内部在yield1 = yield 1暂停

第二次调用

  • 从函数内部 yield1 = yield 1 开始执行
  • 注意: 与第一次调用不同,此次调用传入了参数2, 第一次调用已经执行了该yield语句,所以并不会返回右值,而是会进行赋值操作,把传入的参数 2 赋给 yield1
  • 接着执行 console.log('yield1 value: ', yield1), 此时yield1 = 2
  • 然后执行 yield2 = yield test(), 此时会把表达式右值返回, 即返回 3
  • 所以此时 next2 = {value: 3, done: false}, 接着输出 next2
  • gen函数内部在yield2 = yield test()暂停

第三次调用

  • 从函数内部 yield2 = yield test() 开始执行
  • 注意: 传入了参数4, 进行赋值操作,此时yield2 = 4
  • 接着执行 console.log('yield2 value: ', yield2), 此时的 yield2 值为4
  • 因为函数内部已经没有yield语句,所以一直执行执行到函数尾部return 5
  • 所以最后 next3 = {value: 5, done: true}, 接着输出 next2
  • 至此函数执行完毕

我们发现Generator函数的执行就是一个循环调用next的过程,自然的想到使用递归来实现自动执行

function* gen() {
  let a = yield 1;
  let b = yield 2;
  let c = yield 3;
}

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

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

最简单的几行代码,就实现了Generator的"自动执行",但有一个致命的缺点,代码里如果有一步异步操作,并且下一步的操作依赖上一步的结果才能执行,这样的代码就会出错,无法执行,代码如下

function* gen() {
  let file1 = yield fs.readFile('a', () => {});
  let file2 = yield fs.readFile(file1.name, () => {});
}

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

// 异步操作,执行file2的yield时
// file1的值为undefined
while(!res.done){
  res = g.next(res.value);
}

这就十分尴尬了...使用Generator的一个初衷就是为了避免多层次的回调,写出同步代码,而我们现在又卡在了回调上,所以需要使用Thunk函数

函数Thunk化

开发中多数情况都不会单独使用Thunk函数,但是把Thunk和Generator结合在一起使用时,就会发生奇妙的化学反应,可以用来实现Generator函数的自动执行。

Thunk化用一句话总结就是,将一个具有多个参数且有包含一个回调函数的函数转换成一个只接受回调函数作为参数的单参数函数,附一段网上的实现

const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};

具体原理不多赘述,按照个人理解,函数Thunk化,就是把带有回调函数的函数拆分为两步执行

// 普通函数
function func(a, b, callback){
  const sum = a + b;
  callback(sum);
}
// 普通调用
func(1, 2, alert);

// 对函数进行Thunk化
const ft = thunkify(func);
// Thunk化函数调用
ft(1, 2)(alert);

包含异步操作的例子,在执行fs.readFile(fileName)这第一步操作值之后,数据已经拿到,但是不对数据进行操作,而是在第二步的(err, data) => {}回调函数中进行数据操作

let fs = require('fs');
// 正常版本的readFile
fs.readFile(fileName, (err, data) => {});

// Thunk版本的readFile
fs.readFile(fileName)((err, data) => {});

Generator的自动执行

目前结合ThunkPromise都可以实现

Generator + Thunk

上面报错的例子,把readFileThunk化之后,问题就能够得到解决,

let thunkify = require('thunkify');
let readFileThunk = thunkify(fs.readFile);

function* gen() {
  let file1 = yield readFileThunk('a');
  let file2 = yield readFileThunk(file1.name);
}

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

r1.value(function (err, data) { // 这个回调就是readFileThunk('a')的回调
  var r2 = g.next(data);  // 等价于file1 = data;
  r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

执行next后返回对象中的value,不再是一个简单的值,而是一个回调函数,即readFileThunk的第二步操作,在这个回调函数里,可以取得异步操作的结果,更重要的是可以在这个回调函数中继续调用next,把函数的执行权返还给gen函数内部,同时把file1的值通过next的参数传递进去,整个递归就能一直运行。

Generator + Promise

沿用上面的例子,把readFile包装成一个Promise对象


const readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};

function* gen() {
  let file1 = yield readFileThunk('a');
  let file2 = yield readFileThunk(file1.name);
}

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

r1.value.then(function (data) { // 这个回调就是resolve(data)
  var r2 = g.next(data);  // 等价于file1 = data;
  r2.value.then(function ( data) {
    if (err) throw err;
    g.next(data);
  });
});

通过在then里执行回调函数,获取到上一步操作的结果和交回执行权,并把值传递回gen函数内部,实现了递归执行

进一步封装,可以得到以下的代码

let Bluebird = require('bluebird');
let readFileThunk = Bluebird(fs.readFile);

function run(fn) {
  const gen = fn();
  function next(err, data) {
    const result = gen.next(data);
    if (result.done) {
      result.value;
    } else {
      result.value.then((data) => {
        next(data);
      });
    }
  }
  
  // 递归执行
  next();
}

run(function* g() {
  let file1 = yield readFileThunk('a');
  let file2 = yield readFileThunk(file1.name);
});

灰羊忘
253 声望3 粉丝

下一篇 »
PWA初探整理