Generator函數(shù)的語法和異步應(yīng)用

同步應(yīng)用

簡介

基本概念

Generator函數(shù)式ES6提供的一種異步編程解決方案,語法行為和傳統(tǒng)函數(shù)完全不同.
語法上,首先可以把它理解成,Generator函數(shù)是一個狀態(tài)機,封裝了多個內(nèi)部狀態(tài)
執(zhí)行Generator函數(shù)會返回一個遍歷器對象,也就是說,Generator函數(shù)除了狀態(tài)機,還是一個遍歷器對象生成函數(shù).返回的遍歷器對象,可以依次遍歷Generator函數(shù)內(nèi)部的每一個狀態(tài)
形式上,Generator函數(shù)是一個普通函數(shù),但是有兩個特征.一是,function關(guān)鍵字與函數(shù)名之間有一個星號;二是,函數(shù)體內(nèi)部使用yield(產(chǎn)出)表達式,定義不同的內(nèi)部狀態(tài).

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

調(diào)用Generator函數(shù),該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運行結(jié)果,而是一個指向內(nèi)部狀態(tài)的指針對象,也就是遍歷器對象.
下一步必須調(diào)用遍歷器對象的next方法,使得指針移向下一個狀態(tài).換言之,Generator函數(shù)是分段執(zhí)行的,yield表達式是暫停執(zhí)行的標記,而next方法可以恢復(fù)執(zhí)行.
每次調(diào)用遍歷器對象的next方法,就會返回一個有著valuedone兩個屬性的對象.

yield表達式

yield表達式就是暫停標志
遍歷器對象的next方法運行邏輯:

  1. 遇到yield表達式,就暫停執(zhí)行后面的操作,并將緊跟著yield后面的那個表達式的值作為返回的對象的value屬性值
  2. 下一次調(diào)用next方法時,再繼續(xù)向下執(zhí)行,知道遇到yield表達式
  3. 如果沒有遇到新的yield表達式,就一直運行到函數(shù)結(jié)束,知道return語句為止.并將return語法后面的表達式的值,作為返回的對象的value
  4. 如果該函數(shù)沒有return語句 ,則返回的對象的value屬性值為undefined
    需要注意的是,yield表達式后面的表達式,只有調(diào)用next方法,內(nèi)部指針指向該語句時才會執(zhí)行,因此等于為JavaScript提供了手動的"惰性求職"的語法功能
    Generator函數(shù)可以不用yield表達式,這時就變成了一個單純的暫緩執(zhí)行函數(shù)
    需要注意,yield表達式只能用在Generator函數(shù)里面.另外,yield表達式如果用在另一個表達式之中,必須放在圓括號里面.
    yield表達式用作函數(shù)參數(shù)或放在賦值表達式的右邊,可以不加括號.
與Iterator接口的關(guān)系

任意一個對象的Symbol.iterator方法,等于該對象的遍歷器生成函數(shù),調(diào)用該函數(shù)會返回該對象的一個遍歷器對象
由于Generator函數(shù)就是遍歷器生成函數(shù),因此可以把Generator賦值給對象的Symbol.iterator屬性,從而使得該對象具有Iterator接口.
Generator函數(shù)執(zhí)行后,返回一個遍歷器對象,該對象本身也具有Symbol.iterator屬性,執(zhí)行后返回自身

next方法的參數(shù)

yield表達式本身沒有返回值,或者說總是返回undefined.next方法可以帶一個參數(shù),該參數(shù)就會被當做上一個yield表達式的返回值.
這個功能有很重要的語法意義.Generator函數(shù)從暫停狀態(tài)到恢復(fù)運行,它的上下文狀態(tài)是不變的.通過next方法的參數(shù),就有辦法在Generator函數(shù)開始運行之后,繼續(xù)向函數(shù)內(nèi)部注入值.也就是說,可以再Generator函數(shù)運行的不同階段,從外部想內(nèi)部注入不同的值,從而調(diào)整函數(shù)行為.

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

注意:由于next方法的參數(shù)表示上一個yield表達式的返回值,所以在第一次使用next方法時,傳遞參數(shù)是無效的.只有從第二次使用next方法開始,參數(shù)才是有效的
如果想要在第一次調(diào)用next方法時,就能輸入值,可以在generator函數(shù)外面再包一層

function wrapper(generatorFunction) {
  return function (...args) {
    let generatorObject = generatorFunction(...args);
    generatorObject.next();
    return generatorObject;
  };
}

const wrapped = wrapper(function* () {
  console.log(`First input: ${yield}`);
  return 'DONE';
});

wrapped().next('hello!')
// First input: hello!

for...of循環(huán)

可以自動遍歷Generator函數(shù)時生成的Iterator對象,且此時不再需要調(diào)用next方法.
這里需要注意:一旦next方法的返回對象的done屬性,for...of循環(huán)就會中止,且不包含該返回對象.
利用Generator函數(shù)和for...of循環(huán),實現(xiàn)斐波那契數(shù)列:

function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

利用for...of循環(huán),可以寫出遍歷任意對象的方法.通過Generator函數(shù)為它加上遍歷器接口

function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

加上遍歷器接口的另一種寫法是,將Generator函數(shù)加到對象的Symbol.iterator屬性上

function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

除了for...of循環(huán)以外,拓展運算符(...)解構(gòu)賦值和Array.from方法內(nèi)部調(diào)用的,都是遍歷器接口.他們都可以講Generator函數(shù)返回的Iterator對象作為參數(shù)

Generator.prototype.throw

Generator函數(shù)返回的遍歷器對象,都有一個throw方法,可以在函數(shù)體外拋出錯誤,然后在Generator函數(shù)體內(nèi)捕獲

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('內(nèi)部捕獲', e);
  }
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 內(nèi)部捕獲 a
// 外部捕獲 b

第一個錯誤被Generator函數(shù)體內(nèi)的catch語句捕獲.第二次拋出錯誤,由于Generator函數(shù)內(nèi)部的catch語句已經(jīng)被執(zhí)行過了,不會再捕捉到這個錯誤了,所以被函數(shù)體外的catch語句捕獲.
throw方法可以接受一個參數(shù),該參數(shù)會被catch語句接受,建議拋出Error對象的實例.
注意區(qū)分遍歷器對象的throw方法和全局的throw命令.

var g = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      if (e != 'a') throw e;
      console.log('內(nèi)部捕獲', e);
    }
  }
};

var i = g();
i.next();

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕獲', e);
}
// 外部捕獲 [Error: a]

上面代碼之所以只捕獲了a,是因為函數(shù)體外的catch語句塊捕獲了拋出的a錯誤以后,就不會再繼續(xù)try代碼塊里面剩余的語句了.
如果Generator函數(shù)內(nèi)部沒有部署try...catch代碼塊,那么throw方法拋出的錯誤,將被外部try...catch代碼塊捕獲.
如果內(nèi)外部都沒有部署try...catch代碼塊,那么程序?qū)箦e中斷執(zhí)行.
throw方法被捕獲以后,會附帶執(zhí)行下一條yield表達式,也即是說,會附帶執(zhí)行一次next方法.
只要Generator函數(shù)內(nèi)部部署了try...catch代碼塊,那么遍歷器的throw方法拋出的錯誤,不影響下一次遍歷.
另外,throw命令和遍歷器中的throw方法是無關(guān)的,兩者互不影響.
Generator函數(shù)體外拋出的錯誤可以在函數(shù)體內(nèi)捕獲;反過來,Generator函數(shù)體內(nèi)拋出的錯誤,也可以被函數(shù)體外的catch捕獲

function* foo() {
  var x = yield 3;
  var y = x.toUpperCase();
  yield y;
}

var it = foo();

it.next(); // { value:3, done:false }

try {
  it.next(42);
} catch (err) {
  console.log(err);
  //會有報錯信息,數(shù)值沒有toUpperCase方法
}

一旦Generator執(zhí)行過程中拋出錯誤,且沒有被內(nèi)部捕獲,就不會再執(zhí)行下去了.如果此后還調(diào)用next方法,將返回一個value屬性等于undefined,done屬性等于true的對象.

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第二次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  try {
    v = generator.next();
    console.log('第三次運行next方法', v);
  } catch (err) {
    console.log('捕捉錯誤', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次運行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯誤 { value: 1, done: false }
// 第三次運行next方法 { value: undefined, done: true }
// caller done

Generator.prototype.return

可以返回給定的值,并且終結(jié)遍歷Generator函數(shù).
如果return方法調(diào)用時,不提供參數(shù),則返回值的value屬性為undefined.
如果Generator函數(shù)內(nèi)部有try...finally代碼塊,那么return方法會推遲到finally代碼塊執(zhí)行完再執(zhí)行

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

next,throw,return的共同點

本質(zhì)上是同一件事,它們的作用都是讓Generator函數(shù)恢復(fù)執(zhí)行,并且使用各不同的語句替換yield表達式
next是將yield表達式替換成一個值

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相當于將 let result = yield x + y
// 替換成 let result = 1;

throw是將yield表達式替換成一個throw語句

gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 相當于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));

return是將yield表達式替換成一個return`語句

gen.return(2); // Object {value: 2, done: true}
// 相當于將 let result = yield x + y
// 替換成 let result = return 2;

yield*表達式

如果在Generator函數(shù)內(nèi)部,調(diào)用另一個Generator函數(shù),默認情況下是沒有效果的.這里就需要用到yield*表達式,用來在一個Generator函數(shù)里面執(zhí)行另一個Generator函數(shù).

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同于
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

yield*后面的Generator函數(shù)(沒有return語句時),等同于在Generator函數(shù)內(nèi)部,部署一個for...of循環(huán)
如果yield*后面跟著一個數(shù)組,由于數(shù)組原生支持遍歷器,因此就會遍歷數(shù)組成員

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }

上面代碼中,如果yield命令后面如果不加星號,返回的是整個數(shù)組,加了星號就表示返回的是數(shù)組的遍歷器對象.
如果被代理的Generator函數(shù)有return語句,那么就可以向代理它的Generator函數(shù)返回數(shù)據(jù).
yield*命令可以很方便的取出嵌套數(shù)組的所有成員

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let i=0; i < tree.length; i++) {
      yield* iterTree(tree[i]);
    }
  } else {
    yield tree;
  }
}

const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

for(let x of iterTree(tree)) {
  console.log(x);
}
// a
// b
// c
// d
// e

作為對象屬性的Generator函數(shù)

//簡寫形式
let obj = {
  * myGeneratorMethod() {
    ···
  }
};
//等同于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

Generator函數(shù)的this

Generator函數(shù)總數(shù)返回一個遍歷器,ES6規(guī)定這個遍歷器是Generator函數(shù)的實例,也繼承了Generator函數(shù)的prototype對象上的方法.
Generator函數(shù)內(nèi)部在this對象上添加一個屬性,但是返回的遍歷器對象拿不到這個屬性.Generator函數(shù)也不能跟new命令一起用,會報錯.
讓Generator函數(shù)返回一個正常的對象實例,既可以用next方法,又可以獲得正常的this的方法:
首先,生成一個空對象,使用call方法綁定Generator函數(shù)內(nèi)部的this.這樣,構(gòu)造函數(shù)調(diào)用以后,這個空對象就是Generator函數(shù)的實例對象了.

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3

上面代碼中,執(zhí)行的是遍歷器對象f,但是生成的對象實例是obj,將這個兩個對象統(tǒng)一的辦法:
將obj換成F.prototype

function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

再將F改成構(gòu)造函數(shù),就可以對它執(zhí)行new命令了

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

含義

Generator和狀態(tài)機
//ES5
var ticking = true;
var clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}
//ES6
var clock = function* () {
  while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

上面的Generator實現(xiàn)與ES5實現(xiàn)對比,可以看到少了用來保存狀態(tài)的外部變量,更符合函數(shù)式編程的思想.

Generator與協(xié)程

協(xié)程是一種程序運行的方式,可以理解為"協(xié)作的線程"或"協(xié)作的函數(shù)".協(xié)程既可以用單線程實現(xiàn),也可以用多線程實現(xiàn).前者是一種特殊的子例程,后者是一種特殊的線程.

  1. 協(xié)程與子例程的差異
    可以并行執(zhí)行,交換執(zhí)行權(quán)的線程(或函數(shù)),就稱為協(xié)程.
    從實現(xiàn)上看,在內(nèi)存中,子例程只使用一個棧,而協(xié)程是同事存在多個棧,但只有一個棧是在運行狀態(tài),也就是說,協(xié)程是以多占用內(nèi)存為代價,實現(xiàn)多任務(wù)的并行.
  2. 協(xié)程與普通線程的差異
    普通的線程是搶先式的,到底哪個線程優(yōu)先得到資源,必須有運行環(huán)境決定,但是協(xié)程是合作式的,執(zhí)行權(quán)有協(xié)程自己分配.
    Generator函數(shù)是ES6對于協(xié)程的實現(xiàn),但屬于不完全實現(xiàn).因為只有Generator函數(shù)的調(diào)用者才能將程序的執(zhí)行權(quán)還給Generator函數(shù).如果是完全執(zhí)行的協(xié)程,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)之心.
Generator與上下文

Javascript執(zhí)行函數(shù)(或塊級代碼)的時候,會在當前上下文環(huán)境的上層產(chǎn)生一個函數(shù)運行的上下文,變成當前的上下文,由此形成一個上下文環(huán)境的堆棧.這個堆棧是"后進先出"的數(shù)據(jù)結(jié)構(gòu),最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成,退出堆棧,然后再執(zhí)行完成它下層的上下文,直至所有代碼執(zhí)行完成,堆棧清空
Generator函數(shù)不是這樣,它執(zhí)行產(chǎn)生的上下文環(huán)境,一旦遇到yield命令,就會暫時退出堆棧,但是并不小時,里面的所有變量和對象會凍結(jié)在當前狀態(tài).等到對它執(zhí)行next命令時,這個上下文環(huán)境又會重新加入調(diào)用棧,凍結(jié)的變量和對象恢復(fù)執(zhí)行.

應(yīng)用

  1. 異步操作的同步化表達
    Ajax是典型的異步操作,通過Generator函數(shù)部署Ajax操作:
function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
    //回調(diào)成功之后再調(diào)用next方法,并傳入?yún)?shù)賦值給result
  });
}

var it = main();
it.next();

通過Generator函數(shù)逐行讀取文本文件

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}
控制流管理
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

Promise改寫

Promise.resolve(step1)
  .then(step2)
  .then(step3)
  .then(step4)
  .then(function (value4) {
    // Do something with value4
  }, function (error) {
    // Handle any error from step1 through step4
  })
  .done();

Generator函數(shù)改寫

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

然后使用一個函數(shù)按次序自動執(zhí)行所有步驟

scheduler(longRunningTask(initialValue));

function scheduler(task) {
  var taskObj = task.next(task.value);
  // 如果Generator函數(shù)未結(jié)束,就繼續(xù)調(diào)用
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

這種做法只能用于所有步驟都是同步操作的情況,不能有異步操作的步驟.
for...of的本質(zhì)是一個while循環(huán).

部署Iterator接口

利用Generator函數(shù)可以在任意對象上部署Iterator接口

function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// foo 3
// bar 7
作為數(shù)據(jù)結(jié)構(gòu)

可以看做一個數(shù)組結(jié)構(gòu),因為Generator函數(shù)可以返回一系列的值,這意味著它可以對任意表達式,提供類似數(shù)組的接口.

function* doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

for (task of doStuff()) {
  // task是一個函數(shù),可以像回調(diào)函數(shù)那樣使用它
}

異步應(yīng)用

傳統(tǒng)方法

ES6誕生以前,異步編程方法大概一下四種:

  • 回調(diào)函數(shù)
  • 事件監(jiān)聽
  • 發(fā)布/訂閱
  • Promise對象

基本概念

Promise
fs.readFile(fileA, 'utf-8', function (err, data) {
  fs.readFile(fileB, 'utf-8', function (err, data) {
    // ...
  });
});

如果依次讀取兩個以上的文件,就會出現(xiàn)多重嵌套.因為多個異步操作形成了搶耦合,只要有一個操作需要修改,她的上層回調(diào)函數(shù)和下層回調(diào)函數(shù)可能都要跟著修改.這種情況成為"回調(diào)函數(shù)地獄".
Promise就是為了解決這個問題而提出的,它是一種新的寫法,允許將回調(diào)函數(shù)的嵌套,改為鏈式調(diào)用.采用Promise,連續(xù)讀取多個文件:

var readFile = require('fs-readfile-promise');

readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

Promise最大問題是代碼冗余,原來的任務(wù)被Promise包裝一下,不管什么操作都是一堆then,原來的語義變得不清楚

Generator函數(shù)

異步任務(wù)的封裝

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

上面代碼中,Generator函數(shù)封裝了一個異步操作,該操作先讀取一個遠程接口,然后從JSON格式的數(shù)據(jù)解析信息

//執(zhí)行這段代碼
var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

上面代碼中,首先執(zhí)行Generator函數(shù),獲取遍歷器對象,然后使用next方法,執(zhí)行異步任務(wù)的第一階段.由于fetch模塊返回的是一個Promise對象,因此要用then方法調(diào)用下一個next方法.
可以看到,雖然Generator函數(shù)將異步操作表示的很簡介,但是流程管理卻不方便.

Thunk函數(shù)

Thunk函數(shù)是自動執(zhí)行Generator函數(shù)的一種方法

參數(shù)的求值策略
  1. "傳值調(diào)用"
    在進入函數(shù)體之前,就計算參數(shù)表達式的值
  2. "傳名調(diào)用"
    直接將表達式傳入函數(shù)體,只有在用到的時候求值
Thunk函數(shù)的含義

編譯器的"傳名調(diào)用"實現(xiàn),往往是將參數(shù)放到一個臨時函數(shù)之中,再將這個臨時函數(shù)傳入函數(shù)體.這個臨時函數(shù)就叫做Thunk函數(shù).
這就是Thunk函數(shù)的定義,它是"傳名調(diào)用"的一種實現(xiàn)策略,用來替換某個表達式.

JavaScript語言的Thunk函數(shù)

JavaScript語言是傳值調(diào)用的e,它的Thunk函數(shù)含義有所不同.在JavaScript語言中,Thunk函數(shù)替換的不是表達式,而是多參數(shù)函數(shù),將其替換成一個只接受回調(diào)函數(shù)作為參數(shù)的單參數(shù)函數(shù).

// 正常版本的readFile(多參數(shù)版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(單參數(shù)版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

任何參數(shù),只要參數(shù)有回調(diào)函數(shù),就能寫成Thunk函數(shù)的形式.下面是一個簡單的Thunk函數(shù)轉(zhuǎn)換器:

// ES5版本
var Thunk = function(fn){
  return function (){
    var args = Array.prototype.slice.call(arguments);
    return function (callback){
      args.push(callback);
      return fn.apply(this, args);
    }
  };
};

// ES6版本
const Thunk = function(fn) {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback);
    }
  };
};
Thunkify模塊

生產(chǎn)環(huán)境的轉(zhuǎn)換器,建議使用Thunkify模塊
使用方法:

var thunkify = require('thunkify');
var fs = require('fs');

var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
  // ...
});

Thunkify的源碼與上一節(jié)那個簡單的轉(zhuǎn)換器非常像

function thunkify(fn) {
  return function() {
    var args = new Array(arguments.length);
    var ctx = this;

    for (var i = 0; i < args.length; ++i) {
      args[i] = arguments[i];
    }

    return function (done) {
      var called;

      args.push(function () {
        if (called) return;
        called = true;
        done.apply(null, arguments);
      });

      try {
        fn.apply(ctx, args);
      } catch (err) {
        done(err);
      }
    }
  }
};

主要多了一個檢查機制,變量called確保回調(diào)函數(shù)只運行一次.這樣的設(shè)計與下文的Generator函數(shù)相關(guān).下面例子:

function f(a, b, callback){
  var sum = a + b;
  callback(sum);
  callback(sum);
}

var ft = thunkify(f);
var print = console.log.bind(console);
ft(1, 2)(print);
// 3

由于thunkify只允許回調(diào)函數(shù)執(zhí)行一次,所以只輸出一行結(jié)果.

Generator函數(shù)的流程管理

Generator函數(shù)可以自動執(zhí)行

function* gen() {
  // ...
}

var g = gen();
var res = g.next();

while(!res.done){
  console.log(res.value);
  res = g.next();
}

但是,這不適合異步操作.以讀取文件為例,下面的Generator函數(shù)封裝了兩個異步操作

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* (){
  var r1 = yield readFileThunk('/etc/fstab');
  console.log(r1.toString());
  var r2 = yield readFileThunk('/etc/shells');
  console.log(r2.toString());
};

上面代碼中,yield命令用于將程序的執(zhí)行權(quán)移出Generator函數(shù),那么就需要一種方法將執(zhí)行權(quán)再交還給Generator函數(shù)
這種方法就是Thunk函數(shù),因為它可以在回調(diào)函數(shù)里,將執(zhí)行權(quán)交還給Generator函數(shù).

var g = gen();

var r1 = g.next();
//這里r1.value相當于readFileThunk('/etc/fstab'),此時該方法還不會被執(zhí)行
//readFileThunk('/etc/fstab')(callback)
r1.value(function (err, data) {
  if (err) throw err;
  var r2 = g.next(data);
  //這里的r2.value已經(jīng)被賦值為Thunk函數(shù)名
  r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
  });
});

上面代碼中,變量g是Generator函數(shù)的內(nèi)部指針,表示目前執(zhí)行到哪一步.next方法負責(zé)將指針移動到下一步,并返回該步的信息(value和done).
可以知道Generator函數(shù)的執(zhí)行結(jié)果,其實是將同一個回調(diào)函數(shù),反復(fù)傳入next方法的value屬性.這使得我們可以用遞歸來自動完成這個過程.

Thunk函數(shù)的自動流程管理

Thunk函數(shù)真正的威力,在于可以自動執(zhí)行Generator函數(shù).下面就是一個基于Thunk函數(shù)的Generator執(zhí)行器.

function run(fn) {
  var gen = fn();

  function next(err, data) {
    var result = gen.next(data);
    //此時result.value已經(jīng)被賦值為上一個Thunk函數(shù)名,next是一個回調(diào)函數(shù)
    if (result.done) return;
    result.value(next);
    //當上一個Thunk函數(shù)執(zhí)行完成之后才會調(diào)用回調(diào)函數(shù),再繼續(xù)執(zhí)行下一個Thunk函數(shù)
  }

  next();
  //next方法從來都不會被傳入?yún)?shù)
}

function* g() {
  // ...
}

run(g);

run函數(shù)就是一個Generator函數(shù)的自動執(zhí)行器.內(nèi)部的next函數(shù)就是Thunk的回調(diào)函數(shù).next函數(shù)先將指針移到Generator函數(shù)的下一步(gen.next方法),然后判斷Generator函數(shù)是否結(jié)束(result.done屬性),如果沒結(jié)束,就將next函數(shù)再傳入Thunk函數(shù)(result.value
屬性),否則就直接退出
使用該執(zhí)行器的前提是每一個異步操作,都要是Thunk函數(shù),也就是說,跟在yield命令后面的必須是Thunk函數(shù).
Thunk函數(shù)并不是Generator函數(shù)自動執(zhí)行的唯一方案.因為自動執(zhí)行的關(guān)鍵是,必須有一種機制,自動控制Generator函數(shù)的流程,接收和交換程序的執(zhí)行權(quán).回調(diào)函數(shù)可以做到這一點,Promise對象也可以做到這一點.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末卵史,一起剝皮案震驚了整個濱河市咽弦,隨后出現(xiàn)的幾起案子院水,更是在濱河造成了極大的恐慌鬼贱,老刑警劉巖吧秕,帶你破解...
    沈念sama閱讀 222,865評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異兆蕉,居然都是意外死亡羽戒,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,296評論 3 399
  • 文/潘曉璐 我一進店門虎韵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來易稠,“玉大人,你說我怎么就攤上這事包蓝∈簧纾” “怎么了?”我有些...
    開封第一講書人閱讀 169,631評論 0 364
  • 文/不壞的土叔 我叫張陵测萎,是天一觀的道長亡电。 經(jīng)常有香客問我,道長硅瞧,這世上最難降的妖魔是什么份乒? 我笑而不...
    開封第一講書人閱讀 60,199評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮腕唧,結(jié)果婚禮上或辖,老公的妹妹穿的比我還像新娘。我一直安慰自己枣接,他們只是感情好颂暇,可當我...
    茶點故事閱讀 69,196評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著但惶,像睡著了一般耳鸯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上膀曾,一...
    開封第一講書人閱讀 52,793評論 1 314
  • 那天县爬,我揣著相機與錄音,去河邊找鬼添谊。 笑死捌省,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的碉钠。 我是一名探鬼主播,決...
    沈念sama閱讀 41,221評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼卷拘,長吁一口氣:“原來是場噩夢啊……” “哼喊废!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起栗弟,我...
    開封第一講書人閱讀 40,174評論 0 277
  • 序言:老撾萬榮一對情侶失蹤污筷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓣蛀,經(jīng)...
    沈念sama閱讀 46,699評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡陆蟆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,770評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了惋增。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叠殷。...
    茶點故事閱讀 40,918評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诈皿,靈堂內(nèi)的尸體忽然破棺而出林束,到底是詐尸還是另有隱情稽亏,我是刑警寧澤壶冒,帶...
    沈念sama閱讀 36,573評論 5 351
  • 正文 年R本政府宣布咸作,位于F島的核電站,受9級特大地震影響凉逛,放射性物質(zhì)發(fā)生泄漏性宏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,255評論 3 336
  • 文/蒙蒙 一状飞、第九天 我趴在偏房一處隱蔽的房頂上張望毫胜。 院中可真熱鬧,春花似錦诬辈、人聲如沸酵使。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,749評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽口渔。三九已至,卻和暖如春穿撮,著一層夾襖步出監(jiān)牢的瞬間缺脉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,862評論 1 274
  • 我被黑心中介騙來泰國打工悦穿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留攻礼,地道東北人。 一個月前我還...
    沈念sama閱讀 49,364評論 3 379
  • 正文 我出身青樓栗柒,卻偏偏與公主長得像礁扮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,926評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 在此處先列下本篇文章的主要內(nèi)容 簡介 next方法的參數(shù) for...of循環(huán) Generator.prototy...
    醉生夢死閱讀 1,452評論 3 8
  • 異步編程對JavaScript語言太重要太伊。Javascript語言的執(zhí)行環(huán)境是“單線程”的雇锡,如果沒有異步編程,根本...
    呼呼哥閱讀 7,313評論 5 22
  • 簡介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案僚焦,語法行為與傳統(tǒng)函數(shù)完全不同锰提。本章詳細介紹...
    呼呼哥閱讀 1,076評論 0 4
  • 一、什么是生成器 Generator叠赐? 生成器對象是由一個 Generator 函數(shù)返回的欲账,并且她符合 可迭代協(xié)議...
    貴在隨心閱讀 1,115評論 0 3
  • 半夜起床,怒氣沖頂芭概,看著婆婆的信息很想用力地反擊回去赛不,有想到回過去除了撒氣什么作用也沒有,把編輯好的內(nèi)容給老公發(fā)了...
    游游游游上天的魚閱讀 233評論 0 0