CO框架分析

  es6       2016-08-29

ES6带来了很多新的特性,其中生成器、yield等能对js金字塔式的异步回调做到很好地解决,而基于此封装的co框架能让我们完全以同步的方式来编写异步代码。这篇文章对生成器函数(Generator Function)及框架thunkify、co的核心代码做了比较彻底的分析。co的使用还是比较广泛的,除了我们日常的编码要用到使我们的代码逻辑更清晰易懂外,一些知名框架也是基于co实现的,比如被称为下一代的Nodejs web框架的koa等。

Generate 函数

有关 generate 函数的内容可以参考ES6的相关文档,这里仅以一个例子说明使用:

  1. function* gen1(){
  2. yield 2;
  3. yield 3;
  4. }
  5. function* gen2(){
  6. yield 1;
  7. yield* gen1();
  8. yield 4;
  9. }
  10. var g2 = gen2();
  11. console.log(g2.next().value); // 1
  12. console.log(g2.next().value); // 2
  13. console.log(g2.next().value); // 3
  14. console.log(g2.next().value); // 4

Thunk 函数

在co的应用中,为了能像写同步代码那样书写异步代码,比较多的使用方式是使用thunk函数(但不是唯一方式,还可以是:Promise)。比如读取文件内容的一步函数fs.readFile()方法,转化为thunk函数的方式如下:

  1. function readFile(path, encoding){
  2. return function(cb){
  3. fs.readFile(path, encoding, cb);
  4. };
  5. }

thunk函数具备以下两个要素:(1) 有且只有一个参数是callback的函数;(2) callback的第一个参数是error。

使用thunk函数,同时结合co我们就可以像写同步代码那样来写书写异步代码,先来个例子感受下:

  1. var co = require('co'),
  2. fs = require('fs'),
  3. Promise = require('es6-promise').Promise;
  4. function readFile(path, encoding){
  5. return function(cb){ // thunk函数
  6. fs.readFile(path, encoding, cb);
  7. };
  8. }
  9. co(function* (){
  10. // 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式
  11. var a = yield readFile('a.txt', {encoding: 'utf8'});
  12. console.log(a); // a
  13. var b = yield readFile('b.txt', {encoding: 'utf8'});
  14. console.log(b); // b
  15. var c = yield readFile('c.txt', {encoding: 'utf8'});
  16. console.log(c); // c
  17. return yield Promise.resolve(a+b+c);
  18. }).then(function(val){
  19. console.log(val); // abc
  20. }).catch(function(error){
  21. console.log(error);
  22. });

其实,对于每次都去自己书写一个thunk函数还是比较麻烦的,有一个框架thunkify可以帮我们轻松实现,修改后的代码如下:

  1. var co = require('co'),
  2. thunkify = require('thunkify'),
  3. fs = require('fs'),
  4. Promise = require('es6-promise').Promise;
  5. var readFile = thunkify(fs.readFile);
  6. co(function* (){
  7. // 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式
  8. var a = yield readFile('a.txt', {encoding: 'utf8'});
  9. console.log(a); // a
  10. var b = yield readFile('b.txt', {encoding: 'utf8'});
  11. console.log(b); // b
  12. var c = yield readFile('c.txt', {encoding: 'utf8'});
  13. console.log(c); // c
  14. return yield Promise.resolve(a+b+c);
  15. }).then(function(val){
  16. console.log(val); // abc
  17. }).catch(function(error){
  18. console.log(error);
  19. });

对于thunkify框架的理解注释如下:

  1. /**
  2. * Module dependencies.
  3. */
  4. var assert = require('assert');
  5. /**
  6. * Expose `thunkify()`.
  7. */
  8. module.exports = thunkify;
  9. /**
  10. * Wrap a regular callback `fn` as a thunk.
  11. *
  12. * @param {Function} fn
  13. * @return {Function}
  14. * @api public
  15. */
  16. function thunkify(fn) {
  17. assert('function' == typeof fn, 'function required');
  18. // 返回一个包含thunk函数的函数,返回的thunk函数用于执行yield,而外围这个函数用于给thunk函数传递参数
  19. return function() {
  20. var args = new Array(arguments.length);
  21. // 缓存当前上下文环境,给fn提供执行环境
  22. var ctx = this;
  23. // 将参数类数组转化为数组(实现方式略显臃肿,可直接用Array.prototype.slice.call(arguments)实现)
  24. for (var i = 0; i < args.length; ++i) {
  25. args[i] = arguments[i];
  26. }
  27. // 真正的thunk函数(有且只有一个参数是callback的函数,且callback的第一个参数为error)
  28. // 类似于:
  29. // function(cb) {fs.readFile(path, {encoding: 'utf8}, cb)}
  30. return function(done) {
  31. var called;
  32. // 将回调函数再包裹一层,避免重复调用;同时,将包裹了的真正的回调函数push进参数数组
  33. args.push(function() {
  34. if (called) return;
  35. called = true;
  36. done.apply(null, arguments);
  37. });
  38. try {
  39. // 在ctx上下文执行fn(一般是异步函数,如:fs.readFile)
  40. // 并将执行thunkify之后返回的函数的参数(含done回调)传入,类似于执行:
  41. // fs.readFile(path, {encoding: 'utf8}, done)
  42. // 关于done是做什么用,则是在co库内
  43. fn.apply(ctx, args);
  44. } catch (err) {
  45. done(err);
  46. }
  47. }
  48. }
  49. };

本文最后更新于2017-01-08 00:34:29