资讯专栏INFORMATION COLUMN

thunkify与co源码解读

Tangpj / 2711人阅读

开头 首先本文有将近3000字,阅读可能会占用你20分钟左右。 文笔可能不佳,希望能帮助到阅读此文的人有一些收获

在进行源码阅读前
首先抱有一个疑问,thunk函数是什么,thunkify库又是干什么的,co又是干嘛,它有啥用

程序语言有两种求值策略 传名调用 传入参数实际上是传入函数体 传值调用 函数体在进入的时候就进行运算计算值
编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数

这几句话来自阮一峰老师的blog文章

试想下我们在node环境下要使用fs.readfile

fs.readfile("filename",function(err,data){
     if(err){
          console.log(err)
          return
     }
})

而使用thunk简单改造之后我们的函数可以变成这样子的形式

var Thunk = function(filename){
    return function (callback){
        return fs.readfile(fileName,callback)
    }
}

此时调用readfile的话,我们可以这么调用

var read = Thunk("filename")
read(callback);

thunkify出自tj大神之手

thunkify源码解析
var assert = require("assert");
module.exports = thunkify;
function thunkify(fn) {
    assert("function" == typeof fn, "function required");
    // 引入断言库判断是不是函数
    // 返回一个包含thunk函数的匿名函数
    return function () {
        var args = new Array(arguments.length);
        // 创建一个数组空间
        var ctx = this;
        // 获取上下文环境用于后面绑定上下文

        for (var i = 0; i < args.length; ++i) {
            args[i] = arguments[i];
        }
        // 迭代传参,因为有内存泄漏bug
        // 返回真正的thunk函数
        return function (done) {
            // done相当于是执行后的callback
            var called;
            // 声明一个called保证只执行一次这个回调函数
            // 压入一个数组中进行这种隔断,防止被多次执行
            args.push(function () {
                if (called) return;
                called = true;
                done.apply(null, arguments);
            });
            // 用try catch 在执行失败也走一次callback 传入err信息
            try {
                fn.apply(ctx, args);
            } catch (err) {
                done(err);
            }
        }
    }
};

代码并不难懂
乍一看,这好像没什么用吧。

但 js后来有一个Generator函数,thunk此时仿佛有了作用

Generator函数

使用yield 就是将控制权放出暂停执行
然后返回一个当前指针(遍历器对象)

所以我们是否需要有一种方法接受并且可以继续返回这种控制权
显式的调用next固然没有问题。但是我们要自动的话?该怎么办

基于自动流程管理,我们利用thunk函数的特性,调用回调函数callback
回调函数里面递归调用generator的next方法
直到状态值为done generator函数结束
这时候整个generator就可以很优雅地被解决

然后我们想象,这个流程thunk函数可以干什么

主要的功能其实是通过封装多层使得我们可以在回调函数内获得控制权
返回控制权
因为一般按照正常写法
我们需要显式地调用next next来使得我们的Generator一步步完成
那么我们只需要一种机制,可以帮助我们获得控制权,并且返回控制权
都可以实现自动化

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

var gen = function* (){
  var r1 = yield readFile("xxxfilename");
  console.log(r1.toString());
  var r2 = yield readFile(" xxxfilename ");
  console.log(r2.toString());
};

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

这是一个简单的demo利用thunkify实现自动化generator

thunk函数回调调用next是一种方法
Pormise的then调用next 同时也是一种解决办法
区别在于thunk可控(指的是在回调中我们可以可控执行),promise立即执行

co是什么

Generator based control flow goodness for nodejs and the browser, using promises, letting you write non-blocking code in a nice-ish way.

基于Generator,使用promise,让你用一种更好的方式书写异步代码

co的源码也并不多
大概两百行
https://github.com/tj/co/blob...
要读懂co源码建议还得看看promise规范与用法

co源码解析
var slice = Array.prototype.slice;
module.exports = co["default"] = co.co = co;
co.wrap = function (fn) {
  //兼容有参数的generator函数
  //利用柯里化将generator转换成普通函数
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};
function co(gen) {
  var ctx = this;
  //获得当前上下文环境
  var args = slice.call(arguments, 1);
  //获得多参数(如果有的话)
  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // see https://github.com/tj/co/issues/180
  //会内存泄漏
  //返回一个promise相当于将一切都包裹在promise里面。使得我们co返回的可以使用promise的方法
  // co的返回值是Promise对象。为什么可以then和catch的根源
  return new Promise(function(resolve, reject) {
    //做类型的判断。
    if (typeof gen === "function") gen = gen.apply(ctx, args);
    //Generator函数执行之后会是typeof会是对象。
    //默认执行调用一次Generator返回一个遍历器对象Generator
    if (!gen || typeof gen.next !== "function") return resolve(gen);
    //判断类型 如果不符合  promise就进入resolved
    // 看看是不是Generator指针
    //传入的不是Generators函数,没有next,
    // 就直接resolve返回结果;这里是错误兼容而已,因为co就是基于generator function的,传入其他的没有意义

    //执行onFulfilled
    onFulfilled();
    //返回一个promise

    //onFulfilled干了什么。其实跟我们之前的一样,只是这里涉及到了promise的状态。如果出错了。状态返回是reject
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
        //初始化启动一遍Generator next
      } catch (e) {
        return reject(e);
        //一有错误的话就抛出错误转向rejected
      }
      // 初始化即将第一次yield的·值·传给next
      next(ret);
      //将这个指针对象转交next函数处理
      // 实现自动化的关键
      return null;
    }
    function onRejected(err) {
      //接受error错误
      var ret;
      //这块其实就是处理整个流程的错误控制
      try {
        ret = gen.throw(err);
        //利用Generator throw错误给try catch捕获
      } catch (e) {
        return reject(e);
        //使得Promise进入rejected
      }
      next(ret);
    }
    function next(ret) {
      //接受指针对象

      if (ret.done) return resolve(ret.value);
      //显示对ret指针状态做判断,done为true证明generator已经结束
      //此时进入resolved结束整个Generator
      var value = toPromise.call(ctx, ret.value);
      //将yield 的值进行Promise转换

      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      //value在我们允许的范围内,那么value.then注入onFulfilled与onRejected,来执行下一次gen.next。
      //在onFulfilled又将调用next从而使得next不停的利用then做调用
      //如果值是存在并且可以进行promise的转换。(也就是不是基本类型/或假值)
      return onRejected(new TypeError("You may only yield a function, promise, generator, array, or object, "
        + "but the following object was passed: "" + String(ret.value) + """));
      //如果没有经过值转换或者value为空的时候。此时将抛出错误。
      //因为那就是所谓的基本类型不支持了
      //function, promise, generator, array, or object只支持这几种的
    }
  });
}
//注意我们就只允许这几种类型转换。
//那么进入判断的时候我们就可以很简单地判断了,然后决定promise的状态
function toPromise(obj) {
  if (!obj) return obj;
  //如果obj undefined 或者别的假值返回这个undefined
  if (isPromise(obj)) return obj;
  //如果是个Promise的话就返回这个值
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  //判断是不是Generator function是的话用co处理
  if ("function" == typeof obj) return thunkToPromise.call(this, obj);
  //如果是函数的话,使用thunk to promise转换
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  //如果是数组 使用array to promise
  if (isObject(obj)) return objectToPromise.call(this, obj);
  //如果是对象 使用object to promise 转换
  return obj;
  //如果都不是 就返回`值`
}
// co关于yield后边的值也是有一定的要求的,只能是一个 Function|Promise|Generator|Generator Function | Array | Object;
// 而 yield Array和Object中的item也必须是  Function|Promise|Generator | Array | Object;
// 如果不符合的话就将Promise rejected掉并发出警告

//下面是一些工具函数

//使用thunk后的fnction 我们只允许它有一个参数callbak
//允许有多个参数 第一个参数为error
//在node环境下 第一个为error对象
function thunkToPromise(fn) {
  var ctx = this;
  return new Promise(function (resolve, reject) {
    fn.call(ctx, function (err, res) {
      if (err) return reject(err);
      if (arguments.length > 2) res = slice.call(arguments, 1);
      resolve(res);
    });
  });
}
// thunkToPromise传入一个thunk函数
// 函数返回一个Promise对象
// promise里面执行这个函数
// nodejs的回调函数 第一个参数都是err
// 如果有错误就进入rejected(前面我们可以看到 value.then(onFulfilled, onRejected); )
// 如果有error就rejected了
// 如果没有的话就调用resolve( 后面onFulfilled )


//将数组中的所有值均promise化后执行,Promise.all会等待数组内所有promise均fulfilled、或者有一个rejected,才会执行其后的then。
//对一些基本类型 例如数字 字符串之类的,是不会被toPromise转换的
//最后在resolve(res)的时候 res就是存有所有异步操作执行完的值数组
function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}
//对象通过key进行遍历,
//对于每个被promise化好的value
//都将其存储于promises中,最后Promise.all,
//生成results。
//objectToPromise实现实在是太可怕了=-=
//所以很多字企图把它讲顺了
function objectToPromise(obj){
  var results = new obj.constructor();
  var keys = Object.keys(obj);
  var promises = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]);
    // 确保obj[key]为promise对象
    // 然后调用defer推入 promises等待value的promise resolved之后将key放入results
    // 否则直接将 results[key] = obj[key](也就是无须promise化的)
    if (promise && isPromise(promise)) defer(promise, key);
    else results[key] = obj[key];
  }
// 利用promise.all来使用异步并行调用我们的promises
// 如果执行后进入resolved然后压入results对象
// 最后当然是返回这个results对象
// 然后后面的then在获得时候 onFulfilled onRejected的参数将是这个results
// 这样子我们每个promise的结果都会存在result对象对应的key内
// 返回的是一个promise 后面也就可以接着.then(onFulfilled)
  return Promise.all(promises).then(function () {
    return results;
  });

  function defer(promise, key) {
    // predefine the key in the result
    results[key] = undefined;
    promises.push(promise.then(function (res) {
      results[key] = res;
    }));
  }
}

//检查是不是promise
//·鸭子类型·判断。
function isPromise(obj) {
  return "function" == typeof obj.then;
}
//判断是不是Generator迭代器
function isGenerator(obj) {
  return "function" == typeof obj.next && "function" == typeof obj.throw;
}
 //判断是不是generator函数
function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  if ("GeneratorFunction" === constructor.name || "GeneratorFunction" === constructor.displayName) return true;
  return isGenerator(constructor.prototype);
}
//判断是不是对象
//plain object是指用JSON形式定义的普通对象或者new Object()创建的简单对象
function isObject(val) {
  return Object == val.constructor;
}

co大概就是干的,将generator自动化,更好的将异步流转换同步写法

ES7的async await
其实就是generator的语法糖 再加上一个内置自动执行的混合体
也就是究极体
await的返回值是一个promise

是不是很像co包裹的generator

参考内容:
阮一峰网络日志
co 源码分析 co 与 co.wrap
co 源码分析

结语

有两种方法可以使Generator自动化,thunk与Promise

Generator自动化可以使得我们的异步代码编写得更像同步代码(回调地狱是在太可怕了)

涉及异步的,如今很多都是利用Promise,所以掌握Promise是很重要的

希望阅读此文的人可以有一些收获。如果有什么错误的地方也希望可以谈出来或者私信我,一起探讨。
渴望成长。^v^

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

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

相关文章

  • 秒杀 tj/co 的 hprose 协程库

    摘要:而这对于回调函数只有一个返回值参数的函数,或者回调函数的第一个参数不表示错误的函数来说,库就无法使用了。当对函数进行时,如果回调函数第一个参数是类型的对象才会被当做错误处理。因此,第二个回合,仍然是完胜和。 ES6 中引入了 Generator,Generator 通过封装之后,可以作为协程来进行使用。 其中对 Generator 封装最为著名的当属 tj/co,但是 tj/co 跟 ...

    EddieChan 评论0 收藏0
  • 再读Generator和Co源码

    摘要:沿用上面的例子,把包装成一个对象这个回调就是等价于通过在里执行回调函数,获取到上一步操作的结果和交回执行权,并把值传递回函数内部,实现了递归执行进一步封装,可以得到以下的代码递归执行 以前看过的内容,感觉忘得差不多,最近抽空又看了一次,果然书读百遍其义自见 Generator的执行 Generator函数可以实现函数内外的数据交换和执行权交换。 从第一次调用next开始,从函数头部开始...

    ernest.wang 评论0 收藏0
  • 理解async

    摘要:写在前面本文将要实现一个顺序读取文件的最优方法,实现方式从最古老的回调方式到目前的,也会与大家分享下本人对于库与库的理解。其实的任何异步编程的解决方案的目标都是要达到同步的语义,异步的执行。 写在前面 本文将要实现一个顺序读取文件的最优方法,实现方式从最古老的回调方式到目前的async,也会与大家分享下本人对于thunk库与co库的理解。实现的效果:顺序读取出a.txt与b.txt,将...

    Jackwoo 评论0 收藏0
  • 理解thunk函数的作用及co的实现

    摘要:从形式上将函数的执行部分和回调部分分开,这样我们就可以在一个地方执行执行函数,在另一个地方执行回调函数。这样做的价值就在于,在做异步操作的时候,我们只需要知道回调函数执行的顺序和嵌套关系,就能按顺序取得执行函数的结果。 thunk thunk 从形式上将函数的执行部分和回调部分分开,这样我们就可以在一个地方执行执行函数,在另一个地方执行回调函数。这样做的价值就在于,在做异步操作的时候,...

    张巨伟 评论0 收藏0

发表评论

0条评论

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