再讀Generator和Co源碼

之前看過的內容,感受忘得差很少,最近抽空又看了一次,果真書讀百遍其義自見javascript

Generator的執行

Generator函數能夠實現函數內外的數據交換執行權交換java

從第一次調用next開始,從函數頭部開始執行,執行到第一個yield語句時,把執行權交出到函數外部,並返回該yield語句右值,同時在此處暫停函數異步

在下一次調用next時候(能夠傳遞參數),把執行權返還給函數內部,同時把參數賦值給上一次暫停的yield語句的左值,並從該行到開始執行到下一個yield前,並一直循環該過程函數

須要注意的是,yield語句的左值,不能由右值賦值,如 let a = yield 3a 的值並不等於3,a 的只能由函數外部調用next時傳入的參數賦值。ui

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的過程,天然的想到使用遞歸來實現自動執行this

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的"自動執行",但有一個致命的缺點,代碼裏若是有一步異步操做,而且下一步的操做依賴上一步的結果才能執行,這樣的代碼就會出錯,沒法執行,代碼以下code

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化用一句話總結就是,將一個具備多個參數且有包含一個回調函數的函數轉換成一個只接受回調函數做爲參數的單參數函數,附一段網上的實現ip

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);
});
相關文章
相關標籤/搜索