深入理解koa中的co原始碼

龍恩0707發表於2019-03-10

閱讀目錄

一:理解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;
    }));
  }
}

相關文章