资讯专栏INFORMATION COLUMN

再读Generator和Co源码

ernest.wang / 2424人阅读

摘要:沿用上面的例子,把包装成一个对象这个回调就是等价于通过在里执行回调函数,获取到上一步操作的结果和交回执行权,并把值传递回函数内部,实现了递归执行进一步封装,可以得到以下的代码递归执行

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/95369.html

相关文章

  • Koa源码阅读笔记(1) -- co

    摘要:正好自己之前也想看的源代码,所以趁着这个机会,一口气将其读完。源码解读的源代码十分简洁,一共才两百余行。结语的源代码读取来不难,但其处理方式却令人赞叹。而且阅读的源代码,是阅读源码的必经之路。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -- ctx对象 起...

    taoszu 评论0 收藏0
  • co源码分析及其实践

    摘要:返回的结果是一个对象,类似于表示本次后面执行之后返回的结果。对象用于一个异步操作的最终完成或失败及其结果值的表示简单点说就是处理异步请求。源码分析主要脉络函数调用后,返回一个实例。参考链接解释对象的用法的源码及其用法 本文始发于我的个人博客,如需转载请注明出处。为了更好的阅读体验,可以直接进去我的个人博客看。 前言 知识储备 阅读本文需要对Generator和Promise有一个基本的...

    vincent_xyb 评论0 收藏0
  • thunkify与co源码解读

    开头 首先本文有将近3000字,阅读可能会占用你20分钟左右。 文笔可能不佳,希望能帮助到阅读此文的人有一些收获 在进行源码阅读前首先抱有一个疑问,thunk函数是什么,thunkify库又是干什么的,co又是干嘛,它有啥用 程序语言有两种求值策略 传名调用 传入参数实际上是传入函数体 传值调用 函数体在进入的时候就进行运算计算值 编译器的传名调用实现,往往是将参数放到一个临时函数之中,再将这个...

    Tangpj 评论0 收藏0
  • 从react-start到co源码(三)

    摘要:第三篇脚手架依赖的核心库的源码解析。该篇是这个系列文章的第三篇主要是对的源码进行分析讲解。的源码十分简单但实现的功能却是十分的强大。源码概括源码主要包含了两部分公共方法和私有方法。 react作为当前十分流行的前端框架,相信很多前端er都有蠢蠢欲动的学习它的想法。工欲善其事,必先利其器。这篇文章就简单的给大家介绍一下如何我快速的搭建一个react前端开发环境。主要针对于react小白,...

    wind5o 评论0 收藏0
  • co-parallel & co-gather源码解析

    摘要:昨天也是好好的看了一下的源码,今天打算自己来做一下解析。源码如下这段代码真的是很短,但是方法真的很巧妙。因为两个方法用到了,这里把的源码也贴出来源码的描述就是为了执行而创建的。最后再次感谢提供的思路。 原文链接,转载请注明出处 最近看了Ma63d关于爬虫的这篇文章,正好自己也在做爬虫,看到他在文中提到了co-parallel和co-gather,就打算改一下自己的代码(本来代码就只是为...

    caozhijian 评论0 收藏0

发表评论

0条评论

ernest.wang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<