同步應(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
方法,就會返回一個有著value
和done
兩個屬性的對象.
yield表達式
yield
表達式就是暫停標志
遍歷器對象的next方法運行邏輯:
- 遇到
yield
表達式,就暫停執(zhí)行后面的操作,并將緊跟著yield
后面的那個表達式的值作為返回的對象的value
屬性值 - 下一次調(diào)用
next
方法時,再繼續(xù)向下執(zhí)行,知道遇到yield
表達式 - 如果沒有遇到新的
yield
表達式,就一直運行到函數(shù)結(jié)束,知道return語句為止.并將return語法后面的表達式的值,作為返回的對象的value
值 - 如果該函數(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).前者是一種特殊的子例程,后者是一種特殊的線程.
- 協(xié)程與子例程的差異
可以并行執(zhí)行,交換執(zhí)行權(quán)的線程(或函數(shù)),就稱為協(xié)程.
從實現(xiàn)上看,在內(nèi)存中,子例程只使用一個棧,而協(xié)程是同事存在多個棧,但只有一個棧是在運行狀態(tài),也就是說,協(xié)程是以多占用內(nèi)存為代價,實現(xiàn)多任務(wù)的并行. - 協(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)用
- 異步操作的同步化表達
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ù)的求值策略
- "傳值調(diào)用"
在進入函數(shù)體之前,就計算參數(shù)表達式的值 - "傳名調(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對象也可以做到這一點.