代码改变世界

深入理解koa中的co源码

2019-03-10 15:31  龙恩0707  阅读(735)  评论(0编辑  收藏  举报

阅读目录

一:理解Generator

在看co源码之前,我们先来理解下Generator函数。Generator函数是在ES6中实现的。其函数最大的优点是可以让函数执行权,即可以让函数暂停执行,也可以让函数恢复执行。

1. 什么是Generator呢?

如果从语法上来讲的话,可以把它理解成为一个状态机,它里面封装了很多的内部状态。
如果从形式上来,它就是一个普通函数,它和普通函数唯一的区别是 function 关键字和函数之间多了一个*号。
在该函数内部,它是由 yield表达式,来表示不同的内部状态。
在调用generator函数之后,该函数并不会执行,返回的也不是该函数运行的结果,而是一个指向内部状态的指针对象。

比如一个简单的generator函数代码如下:

function* testGeneratorFunc(x) {
  yield t = x + 1;
  yield 'kongzhi, hello world';
  yield 'end';
  return t;
  yield 'xxx';
} 
// 调用Generator函数
const testFn = testGeneratorFunc(1);

console.log(testFn);

打印结果如下:

继续打印信息如下代码:

console.log(testFn.next()); // 输出结果为:{value: 2, done: false}

console.log(testFn.next()); // 输出结果为: {value: 'kongzhi,hello world', done: false}

console.log(testFn.next()); // 输出结果为:{value: 'end', done: false}

console.log(testFn.next()); // 输出结果为:{value: 2, done: true}

console.log(testFn.next()); // 输出结果为: {value: undefined, done: true}

如上是一个Generator函数,它与普通函数的区别是 function关键字后面多一个 * 号来区分该函数是generator函数。

如上代码,调用Generator函数,会返回一个内部指针 testFn对象,调用该指针的next()方法,会移动该内部指针,直到遇到yield语句就会暂停下来。并且会返回一个对象,表示当前阶段的信息 {value: xx, done: Boolean}, 这样的。value属性是 yield语句后面表达式的值,表示当前返回的值。
done属性是一个布尔值,表示Generator函数是否执行完毕,如果为true说明执行完毕了,为false说明没有执行完毕。如果执行完毕后,你再执行 next()方法的话,该返回值就是 undefined了。

2. 理解Generator中的yield表达式和next()方法。

Generator函数被调用之后返回的是一个遍历器对象,比如如上的 testFn对象,我们只有调用next()方法才会去遍历下一个内部状态。而 yield表达式就是暂停标志。yield后面的表达式,只有当我们调用next方法,内部指针才会往下执行。

3. return方法和next()方法的区别:

return语句是终结遍历的含义,之后的yield语句都会失效,比如上面的 yield 'xxx' 在return语句之后是不会被执行的。next()方法返回的是本次yield语句的返回值的。

注意:
1. return 没有参数的时候,返回的是 {value: undefined, done: true}, next 没有参数的时候返回的是本次 yield语句的返回值。

2. return 有参数的时候,返回的是 {value: 参数,done: true}, next()方法有参数的时候会覆盖上一次yield语句的返回值的。

4. Generator异步操作同步化表达。

Generator函数可以使用上面介绍的 yield关键字暂停函数执行的特性,可以将异步操作放在yield关键字之后,这样当我们使用next()方法再继续往下执行的话,那么异步操作也就变成同步方式操作了。
比如常见的ajax操作代码如下:

const $ = require('jquery');

function ajaxRequest(url) {
  $.ajax({
    url: url,
    type: 'get',
    dataType: 'json',
    success: function(response) {
      it.next(response);
    }
  })
}

function* getLocalNews() {
  // url 在本地服务会跨域,使用了proxy代理。具体如何使用webpack代理get请求,看如下博客:
  // https://www.cnblogs.com/tugenhua0707/p/9418526.html#_labe1_11
  // 这里是百度的新闻接口:http://news.baidu.com/widget?id=LocalNews&ajax=json&t=1551279778122
  const result = yield ajaxRequest('/api/widget?id=LocalNews&ajax=json&t=1551279778122');
  console.log(result);
}

const it = getLocalNews();
it.next();

如上代码,定义了一个Generator函数getLocalNews,yield关键字后面跟着一个调用ajax方法。当我们调用Generator函数的时候,它返回的是一个遍历对象,当我们调用该遍历对象的next方法的时候,它就会开始执行 yield 表达式后的 ajaxRequest方法。该方法内部ajax又执行了一次next()方法。
并且把返回值 response作为参数传入next()。因此返回的结果 result 就是接口返回的数据了。

5. 使用Generator解决回调函数嵌套的问题。

如上是一个简单的ajax异步对象,但是当页面变成复杂的时候,比如我们现在有三个ajax请求叫 ajaxFun1, ajaxFun2, ajaxFun3, 先执行 ajaxFun1, 然后将执行的函数之后得到的值value1需要传递给ajaxFun2, 再将执行的ajaxFun2之后得到的结果值value2作为参数传递给函数 ajaxFun3的话,如果使用回调函数嵌套的方法,比如如下代码:

ajaxFun1((value1) => {
  ajaxFun2(value1, (value2)=> {
    ajaxFun3(value2, (value3)=> {

    })
  })
})

如果我们现在讲上面的代码改成Promise的写法就会变成如下代码:

ajaxFun1().then((value1) => {
  return ajaxFun2(value1);
}).then((value2) => {
  return ajaxFun3(value2);
}).then((value3) => {
  // ....
});

如上代码,我们会先执行 ajaxFun1 函数,把该函数返回的值 value1,传给 ajaxFun2 函数,然后把该函数返回的值为 value2,传给函数ajaxFun3.

现在我们使用Generator函数来改造上面的代码,变成如下:

function* generatorFunc(value1) {
  try {
    const value2 = yield ajaxFun1(value1);
    const value3 = yield ajaxFun2(value2);
    const value4 = yield ajaxFun3(value3);

  } catch(e) {

  }
}

现在下面我们要定义一个调度函数一次执行如上三个任务。如下代码:

scheduler(generatorFunc(1));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函数未结束,就继续调用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

等等类似这样的代码。

二:理解js函数柯里化

1. 什么是函数柯里化?

可以这样理解为把接收多个参数的函数变成接收一个单一的参数的函数。那么剩余的参数作为新函数的参数,然后进行返回。

可能上面的含义不好理解,我们先来看个简单的demo,比如现在我们想实现一个加法函数,那么加法函数肯定需要传入多个参数进行运算。如下最初的代码:

function add(a, b) {
  return a + b;
}

// 调用如下:
console.log(add(1, 2)); // 打印 3

如上是一个简单的函数加法,但是现在我们想要实现使用函数柯里化来实现的话,就想把传递的多个参数改成一个参数的函数了。

function curry(a) {
  return function(b) {
    return a + b;
  }
}
// 调用方式如下:
const add2 = curry(1);
console.log(add2(2)); // 返回 3

因此下面我们可以把 add 函数作为参数传递进去,去封装一个更通用的方法。如下代码:

const curry = (fn, ...arg) => {
  let all = arg;
  return (...rest) => {
    all.push(...rest);
    return fn.apply(null, all);
  }
}

function add (a, b) {
  return a + b;
}

let add2 = curry(add, 2);

console.log(add2(3)); // 5 

通过上面的函数柯里化的demo,我们可以看得出,函数柯里化的真正用途是做一件事,只是传递给函数一部分参数来调用它,
让它返回一个函数去处理剩下的参数。其实简单的理解可以是:将函数的变量拆分开来调用;因此我们可以总结出一个简单的公式:如下:

fn(x, y, z) ---> fn(x)(y)(z);
fn(x, y, z) ---> fn(x, y)(z);
fn(x, y, z) ---> fn(x)(y, z);

如下测试代码:

function add (a, b) {
  return a + b;
}
const curry = (fn, ...arg) => {
  let all = arg || [];
  let len = fn.length;
  return (...rest) => {
    let _all = all.slice(0); // 拷贝一份,避免改动全局的all属性
    _all.push(...rest);
    if (_all.length < len) {
      // 递归调用该函数
      return curry.call(this, fn, ..._all);
    } else {
      return fn.apply(this, _all);
    }
  }
}

let add2 = curry(add, 1);
console.log(add2(2)); // 输出:3

add2 = curry(add);
console.log(add2(1, 2)); // 输出:3

console.log(add2(1)(2)); // 输出 3

function testFunc(a, b, c) {
  return a + b + c;
}

/*
 第一种情况:fn(x, y, z) ---> fn(x)(y)(z);
*/
let test = curry(testFunc, 1);
console.log(test(2)(3)); // 输出:6

/*
 第二种情况:fn(x, y, z) ---> fn(x, y)(z);
*/
console.log(test(2, 3)); // 输出: 6

/*
 第三种情况:fn(x, y, z) ---> fn(x)(y, z);
 其实和上面的一样的
*/
console.log(test(2, 3)); // 输出: 6 

三:理解Thunk函数

1. 什么是thunk函数?
thunk函数是将参数放到一个临时函数中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

比如如下代码:

const testThunk = function() {
  return 1;
};

function f(thunk) {
  return thunk() * 2;
}

console.log(f(testThunk)); // 打印出 2

如上代码中 testThunk 函数就是一个 thunk 函数。它把 testThunk 临时函数传入到 函数 f 中,然后再将这个临时函数 testThunk 传入函数体。

2. Javascript语言中的Thunk函数

javascript是传值调用。在javascript中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

什么意思呢?比如如下demo的列子:

文件读取的代码,它接收两个参数,第一个是读取文件的文件名fileName, 第二个是callback的回调函数。如下代码:

const fs = require('fs');
fs.readFile(fileName, callback);

现在我改成 Thunk版本的readFile, 代码将会变成如下:

const thunk = function(fileName) {
  return function(callback) {
    return fs.readFile(fileName, callback);
  };
};

const readFileThunk = thunk(fileName);
readFileThunk(callback);

如上代码 fs模块读取文件readFile方法是一个多参数函数,该方法接收两个参数,第一个是fileName文件名,第二个参数是回调函数callback。然后我们把它改成Thunk函数,使它变成一个单参数函数,thunk函数接收了一个fileName作为参数,然后在内部返回了一个函数,该函数带有callback参数,然后在内部使用 return 返回 读取文件的方法。然后我们在外部调用 readFileThunk返回即可。

如上就是一个Thunk函数,那么Thunk函数的作用是什么呢?Thunk函数可以用于Generator函数的自动流程管理。

首先我们简单的来看下generator函数,基本代码如下:

function* gen() {
  const res = yield 1+2;
  yield 2+3;
  yield 3+4;
}

const genFun = gen();

console.log(genFun.next()); // 输出 {value: 3, done: false}
console.log(genFun.next()); // 输出 {value: 5, done: false}
console.log(genFun.next()); // 输出 {value: 7, done: false}
console.log(genFun.next()); // 输出 {value: undefined, done: true}

执行完成后,就会如上所示。现在我们再来看看使用Thunk函数来自动执行generator函数。基本代码如下:

function* gen() {
  const res = yield 1+2;
  yield 2+3;
  yield 3+4;
}
function run(genFun) {
  const next = function() {
    const result = genFun.next();
    /*
     就会依次执行打印 
     yield后面的表达式值是: 3
     yield后面的表达式值是: 5
     yield后面的表达式值是: 7
     yield后面的表达式值是: undefined
    */
    console.log('yield后面的表达式值是:', result.value);
    if (result.done) {
      return;
    }
    next(); 
  }
  next();
}

const testGen = gen();
run(testGen);

如上代码,先调用 gen函数,然后把testGen作为Thunk函数,作为参数传递到 run函数方法内,然后在代码内部依次调用即可。

四:理解CO源码

co函数的作用是:将Generator函数转成一个promise对象。然后自动执行该函数的代码。co使用了es6中generator的特性。

我们正常的使用 fs.readFile读取一个文件的方法代码是如下:

const fs = require('fs');

fs.readFile('./package.json', (err, d) => {
  if (err) {
    return console.log(err);
  }
  console.log('fs异步模块调用');
  console.log(d.toString());
});

如上 fs.readFile是一个回调函数,所有的代码放到回调函数内部。但是我们下面是使用co函数来继续编写如下代码:

const fs = require('fs');
const co = require('co');

co(function *() {
  console.log('打印co模块调用');
  let a = yield fs.readFile.bind(null, './package.json');
  console.log(a.toString());
  let b = yield [1,2];
  console.log(b);
}).then(function(value) {
  console.log(1111);
}, function(err) {
  console.log(err);
});

如上是co的基本使用方法,下面我们来看下 co的部分源码如下:

/**
 * Execute the generator function or a generator
 * and return a promise.
 *
 * @param {Function} fn
 * @return {Promise}
 * @api public
 */

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
  return new Promise(function(resolve, reject) {
    // 如果是generatorFunction函数的话,就初始化generator函数。
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    // 初始化入口函数。
    onFulfilled();

    /**
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        // 拿到第一个yield返回的对象值保存到 ret参数中
        ret = gen.next(res);
      } catch (e) {
        // 如果异常的话,则直接调用reject把promise设置为失败状态。
        return reject(e);
      }
      // 然后继续把generator的指针指向下一个状态。
      next(ret);
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        // 抛出错误,使用generator对象throw. 在try catch里面可以捕获到该异常。
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    function next(ret) {
      // 如果generator函数执行完成后,该done会为true,因此直接调用resolve把promise设置为成功状态。
      if (ret.done) return resolve(ret.value);
      // 1. 把yield返回的值转换成promise
      var value = toPromise.call(ctx, ret.value);
      /*
       如果有返回值的话,且该返回值是一个promise对象的话,如果成功的话就会执行onFulfilled回调函数。
       如果失败的话,就会调用 onRejected 回调函数。
      */
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);

      // 否则的话,说明有异常,就调用 onRejected 函数给出错误提示。
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

如上是co函数源码,该函数传入一个generater的参数,比如我们上面的demo所示。然后内部代码:

var ctx = this;
var args = slice.call(arguments, 1);

其中 ctx 是上下文对象,args 是获取该co函数中除了generator函数以外中的其他参数。然后使用Array.prototype.slice.call(arguments, 1); 获取所有的参数转化成数组的方式。最后会返回一个Promise对象。
1. 返回一个promise对象。如果我们传入的参数是generator的函数的话,则执行generator的初始化。
代码:gen = gen.apply(ctx, args);
2. 如果它不是gennerator的函数的话,或者说 gen.next !== 'function' 的话,直接 resolve(gen); 就执行执行该函数进行返回值。
3. 自动执行 onFulfilled函数,然后会调用 var ret = gen.next();因此Generator函数就会停在第一次遇到yield关键字的地方。
4. 然后我们获取yield后边的值,即:ret,然后传入 next()函数内,将该值转化为一个promise对象。然后进行执行。
next()函数方法如下:

function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
}

注意:Generator函数中yield是返回的是一个对象,如:{value: 'xxx', done: false/true} 这样的,因此我们首先需要判断 ret中的对象的参数done是否为true,如果为true的话,说明generator函数已经执行到最后了,因此就会直接使用 return resolve(ret.value), 直接自动执行该函数返回该值。

5. 如果generator函数没有执行完,因此就会把该value转换成promise对象。即代码如下:
var value = toPromise.call(ctx, ret.value);

toPromise 源码如下:

/**
 * Convert a `yield`ed value into a promise.
 *
 * @param {Mixed} obj
 * @return {Promise}
 * @api private
 */

function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}

该函数要做的事情如下:
1. 如果该值没有的话,或者为null的话,直接返回该值;即代码:if (!obj) return obj;

2. 如果该参数obj是一个Promise对象的话,就直接返回该obj。判断是否是Promise对象的代码如下:

function isPromise(obj) {
   return 'function' == typeof obj.then;
}

也就是说判断该obj中的then的类型是否是函数。

3. 如果该对象是gennerater函数的话,代码判断是:if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);

就递归调用co函数,继续判断代码。

检查 obj参数是否是Generator函数的话,代码如下:

/**
 * Check if `obj` is a generator function.
 *
 * @param {Mixed} obj
 * @return {Boolean}
 * @api private
 */
function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
  return isGenerator(constructor.prototype);
}

如上代码的含义是:该obj是否有构造函数 constructor,没有的话,直接返回。继续判断 constructor.name 是否是 'GeneratorFunction', 或者是 'GeneratorFunction' === constructor.displayName,如果有其中一项是的话,就直接返回true。

比如generator函数如下测试 constructor如下:

function* gen(){
    var a = yield 'hello';
    console.log(a)
    var b = yield 'world';
    return b;
}
console.log(gen.constructor.name === 'GeneratorFunction'); // 返回 true

4. 如果该对象obj的类型是function的话,比如代码:if ('function' == typeof obj) return thunkToPromise.call(this, obj); 就返回调用 thunkToPromise 方法,该方法的源码如下:

/**
 * Convert a thunk to a promise.
 *
 * @param {Function}
 * @return {Promise}
 * @api private
 */

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

如上代码的含义是把 thunk函数转换成一个Promise对象。首先会传入一个函数fn作为thunkToPromise的参数。然后会返回一个Promise对象,直接调用该fn函数,使用 resolve(res)即可,当然如果该fn有大于2个参数的话,就获取第一个参数到最后的参数,然后转换成数组的形式,最后也使用 resolve(res) 这个函数调用返回即可。

5. 如果该obj对象是一个数组的话,比如如下判断代码:
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);

就把数组转换为Promise对象,arrayToPromise函数代码如下:

/**
 * Convert an array of "yieldables" to a promise.
 * Uses `Promise.all()` internally.
 *
 * @param {Array} obj
 * @return {Promise}
 * @api private
 */

function arrayToPromise(obj) {
  // 直接调用Promise的静态方法包装一个新的promise对象。然后对于每个value调用toPromise进行递归的包装
  return Promise.all(obj.map(toPromise, this));
}

6. 如果该obj是一个对象的话,就把该对象obj转换成Promise对象,如下代码:

if (isObject(obj)) return objectToPromise.call(this, obj);

objectToPromise函数代码如下:

/**
 * Convert an object of "yieldables" to a promise.
 * Uses `Promise.all()` internally.
 *
 * @param {Object} obj
 * @return {Promise}
 * @api private
 */

function objectToPromise(obj) {
  // 克隆生成一个和obj一样类型的空对象 
  /*
    var obj = {'xx': 11}; 
    var results = new obj.constructor(); 
    console.log(results); // 返回 {}
  */
  var results = new obj.constructor();
  // 拿到对象的所有key,返回key的集合数组
  var keys = Object.keys(obj);
  var promises = [];
  // 遍历所有的key
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    // 拿到对应key的值,依次调用及转换成Promise对象。使用toPromise方法。
    var promise = toPromise.call(this, obj[key]);
    // 如果返回的是一个promise的对象的话,就调用下面的defer方法进行异步赋值。
    // 最后把结果都存到 promises数组里面去。
    if (promise && isPromise(promise)) defer(promise, key);
    // 如果不能转换,说明是纯粹的值。就直接赋值
    else results[key] = obj[key];
  }
  // 监听队列里面的所有promise对象,等待所有的promise对象成功的话,就可以调用then函数,最后返回
  // 结果。
  return Promise.all(promises).then(function () {
    return results;
  });

  function defer(promise, key) {
    // predefine the key in the result
    // 先占位初始化
    results[key] = undefined;
    // 把当前promise加入待监听promise数组队列
    promises.push(promise.then(function (res) {
      // 等当前promise变成成功态的时候赋值
      results[key] = res;
    }));
  }
}