同步應(yīng)用
簡(jiǎn)介
基本概念
Generator函數(shù)式ES6提供的一種異步編程解決方案,語(yǔ)法行為和傳統(tǒng)函數(shù)完全不同.
語(yǔ)法上,首先可以把它理解成,Generator函數(shù)是一個(gè)狀態(tài)機(jī),封裝了多個(gè)內(nèi)部狀態(tài)
執(zhí)行Generator函數(shù)會(huì)返回一個(gè)遍歷器對(duì)象,也就是說(shuō),Generator函數(shù)除了狀態(tài)機(jī),還是一個(gè)遍歷器對(duì)象生成函數(shù).返回的遍歷器對(duì)象,可以依次遍歷Generator函數(shù)內(nèi)部的每一個(gè)狀態(tài)
形式上,Generator函數(shù)是一個(gè)普通函數(shù),但是有兩個(gè)特征.一是,function關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào);二是,函數(shù)體內(nèi)部使用yield(產(chǎn)出)表達(dá)式,定義不同的內(nèi)部狀態(tài).
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
調(diào)用Generator函數(shù),該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運(yùn)行結(jié)果,而是一個(gè)指向內(nèi)部狀態(tài)的指針對(duì)象,也就是遍歷器對(duì)象.
下一步必須調(diào)用遍歷器對(duì)象的next方法,使得指針移向下一個(gè)狀態(tài).換言之,Generator函數(shù)是分段執(zhí)行的,yield表達(dá)式是暫停執(zhí)行的標(biāo)記,而next方法可以恢復(fù)執(zhí)行.
每次調(diào)用遍歷器對(duì)象的next方法,就會(huì)返回一個(gè)有著value和done兩個(gè)屬性的對(duì)象.
yield表達(dá)式
yield表達(dá)式就是暫停標(biāo)志
遍歷器對(duì)象的next方法運(yùn)行邏輯:
- 遇到
yield表達(dá)式,就暫停執(zhí)行后面的操作,并將緊跟著yield后面的那個(gè)表達(dá)式的值作為返回的對(duì)象的value屬性值 - 下一次調(diào)用
next方法時(shí),再繼續(xù)向下執(zhí)行,知道遇到yield表達(dá)式 - 如果沒(méi)有遇到新的
yield表達(dá)式,就一直運(yùn)行到函數(shù)結(jié)束,知道return語(yǔ)句為止.并將return語(yǔ)法后面的表達(dá)式的值,作為返回的對(duì)象的value值 - 如果該函數(shù)沒(méi)有
return語(yǔ)句 ,則返回的對(duì)象的value屬性值為undefined
需要注意的是,yield表達(dá)式后面的表達(dá)式,只有調(diào)用next方法,內(nèi)部指針指向該語(yǔ)句時(shí)才會(huì)執(zhí)行,因此等于為JavaScript提供了手動(dòng)的"惰性求職"的語(yǔ)法功能
Generator函數(shù)可以不用yield表達(dá)式,這時(shí)就變成了一個(gè)單純的暫緩執(zhí)行函數(shù)
需要注意,yield表達(dá)式只能用在Generator函數(shù)里面.另外,yield表達(dá)式如果用在另一個(gè)表達(dá)式之中,必須放在圓括號(hào)里面.
yield表達(dá)式用作函數(shù)參數(shù)或放在賦值表達(dá)式的右邊,可以不加括號(hào).
與Iterator接口的關(guān)系
任意一個(gè)對(duì)象的Symbol.iterator方法,等于該對(duì)象的遍歷器生成函數(shù),調(diào)用該函數(shù)會(huì)返回該對(duì)象的一個(gè)遍歷器對(duì)象
由于Generator函數(shù)就是遍歷器生成函數(shù),因此可以把Generator賦值給對(duì)象的Symbol.iterator屬性,從而使得該對(duì)象具有Iterator接口.
Generator函數(shù)執(zhí)行后,返回一個(gè)遍歷器對(duì)象,該對(duì)象本身也具有Symbol.iterator屬性,執(zhí)行后返回自身
next方法的參數(shù)
yield表達(dá)式本身沒(méi)有返回值,或者說(shuō)總是返回undefined.next方法可以帶一個(gè)參數(shù),該參數(shù)就會(huì)被當(dāng)做上一個(gè)yield表達(dá)式的返回值.
這個(gè)功能有很重要的語(yǔ)法意義.Generator函數(shù)從暫停狀態(tài)到恢復(fù)運(yùn)行,它的上下文狀態(tài)是不變的.通過(guò)next方法的參數(shù),就有辦法在Generator函數(shù)開(kāi)始運(yùn)行之后,繼續(xù)向函數(shù)內(nèi)部注入值.也就是說(shuō),可以再Generator函數(shù)運(yùn)行的不同階段,從外部想內(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ù)表示上一個(gè)yield表達(dá)式的返回值,所以在第一次使用next方法時(shí),傳遞參數(shù)是無(wú)效的.只有從第二次使用next方法開(kāi)始,參數(shù)才是有效的
如果想要在第一次調(diào)用next方法時(shí),就能輸入值,可以在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)
可以自動(dòng)遍歷Generator函數(shù)時(shí)生成的Iterator對(duì)象,且此時(shí)不再需要調(diào)用next方法.
這里需要注意:一旦next方法的返回對(duì)象的done屬性,for...of循環(huán)就會(huì)中止,且不包含該返回對(duì)象.
利用Generator函數(shù)和for...of循環(huán),實(shí)現(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),可以寫出遍歷任意對(duì)象的方法.通過(guò)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ù)加到對(duì)象的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)以外,拓展運(yùn)算符(...)解構(gòu)賦值和Array.from方法內(nèi)部調(diào)用的,都是遍歷器接口.他們都可以講Generator函數(shù)返回的Iterator對(duì)象作為參數(shù)
Generator.prototype.throw
Generator函數(shù)返回的遍歷器對(duì)象,都有一個(gè)throw方法,可以在函數(shù)體外拋出錯(cuò)誤,然后在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
第一個(gè)錯(cuò)誤被Generator函數(shù)體內(nèi)的catch語(yǔ)句捕獲.第二次拋出錯(cuò)誤,由于Generator函數(shù)內(nèi)部的catch語(yǔ)句已經(jīng)被執(zhí)行過(guò)了,不會(huì)再捕捉到這個(gè)錯(cuò)誤了,所以被函數(shù)體外的catch語(yǔ)句捕獲.
throw方法可以接受一個(gè)參數(shù),該參數(shù)會(huì)被catch語(yǔ)句接受,建議拋出Error對(duì)象的實(shí)例.
注意區(qū)分遍歷器對(duì)象的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,是因?yàn)楹瘮?shù)體外的catch語(yǔ)句塊捕獲了拋出的a錯(cuò)誤以后,就不會(huì)再繼續(xù)try代碼塊里面剩余的語(yǔ)句了.
如果Generator函數(shù)內(nèi)部沒(méi)有部署try...catch代碼塊,那么throw方法拋出的錯(cuò)誤,將被外部try...catch代碼塊捕獲.
如果內(nèi)外部都沒(méi)有部署try...catch代碼塊,那么程序?qū)?bào)錯(cuò)中斷執(zhí)行.
throw方法被捕獲以后,會(huì)附帶執(zhí)行下一條yield表達(dá)式,也即是說(shuō),會(huì)附帶執(zhí)行一次next方法.
只要Generator函數(shù)內(nèi)部部署了try...catch代碼塊,那么遍歷器的throw方法拋出的錯(cuò)誤,不影響下一次遍歷.
另外,throw命令和遍歷器中的throw方法是無(wú)關(guān)的,兩者互不影響.
Generator函數(shù)體外拋出的錯(cuò)誤可以在函數(shù)體內(nèi)捕獲;反過(guò)來(lái),Generator函數(shù)體內(nèi)拋出的錯(cuò)誤,也可以被函數(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);
//會(huì)有報(bào)錯(cuò)信息,數(shù)值沒(méi)有toUpperCase方法
}
一旦Generator執(zhí)行過(guò)程中拋出錯(cuò)誤,且沒(méi)有被內(nèi)部捕獲,就不會(huì)再執(zhí)行下去了.如果此后還調(diào)用next方法,將返回一個(gè)value屬性等于undefined,done屬性等于true的對(duì)象.
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('第一次運(yùn)行next方法', v);
} catch (err) {
console.log('捕捉錯(cuò)誤', v);
}
try {
v = generator.next();
console.log('第二次運(yùn)行next方法', v);
} catch (err) {
console.log('捕捉錯(cuò)誤', v);
}
try {
v = generator.next();
console.log('第三次運(yùn)行next方法', v);
} catch (err) {
console.log('捕捉錯(cuò)誤', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次運(yùn)行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯(cuò)誤 { value: 1, done: false }
// 第三次運(yùn)行next方法 { value: undefined, done: true }
// caller done
Generator.prototype.return
可以返回給定的值,并且終結(jié)遍歷Generator函數(shù).
如果return方法調(diào)用時(shí),不提供參數(shù),則返回值的value屬性為undefined.
如果Generator函數(shù)內(nèi)部有try...finally代碼塊,那么return方法會(huì)推遲到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的共同點(diǎn)
本質(zhì)上是同一件事,它們的作用都是讓Generator函數(shù)恢復(fù)執(zhí)行,并且使用各不同的語(yǔ)句替換yield表達(dá)式
next是將yield表達(dá)式替換成一個(gè)值
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}
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = 1;
throw是將yield表達(dá)式替換成一個(gè)throw語(yǔ)句
gen.throw(new Error('出錯(cuò)了')); // Uncaught Error: 出錯(cuò)了
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯(cuò)了'));
return是將yield表達(dá)式替換成一個(gè)return`語(yǔ)句
gen.return(2); // Object {value: 2, done: true}
// 相當(dāng)于將 let result = yield x + y
// 替換成 let result = return 2;
yield*表達(dá)式
如果在Generator函數(shù)內(nèi)部,調(diào)用另一個(gè)Generator函數(shù),默認(rèn)情況下是沒(méi)有效果的.這里就需要用到yield*表達(dá)式,用來(lái)在一個(gè)Generator函數(shù)里面執(zhí)行另一個(gè)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ù)(沒(méi)有return語(yǔ)句時(shí)),等同于在Generator函數(shù)內(nèi)部,部署一個(gè)for...of循環(huán)
如果yield*后面跟著一個(gè)數(shù)組,由于數(shù)組原生支持遍歷器,因此就會(huì)遍歷數(shù)組成員
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
上面代碼中,如果yield命令后面如果不加星號(hào),返回的是整個(gè)數(shù)組,加了星號(hào)就表示返回的是數(shù)組的遍歷器對(duì)象.
如果被代理的Generator函數(shù)有return語(yǔ)句,那么就可以向代理它的Generator函數(shù)返回?cái)?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
作為對(duì)象屬性的Generator函數(shù)
//簡(jiǎn)寫形式
let obj = {
* myGeneratorMethod() {
···
}
};
//等同于
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator函數(shù)的this
Generator函數(shù)總數(shù)返回一個(gè)遍歷器,ES6規(guī)定這個(gè)遍歷器是Generator函數(shù)的實(shí)例,也繼承了Generator函數(shù)的prototype對(duì)象上的方法.
Generator函數(shù)內(nèi)部在this對(duì)象上添加一個(gè)屬性,但是返回的遍歷器對(duì)象拿不到這個(gè)屬性.Generator函數(shù)也不能跟new命令一起用,會(huì)報(bào)錯(cuò).
讓Generator函數(shù)返回一個(gè)正常的對(duì)象實(shí)例,既可以用next方法,又可以獲得正常的this的方法:
首先,生成一個(gè)空對(duì)象,使用call方法綁定Generator函數(shù)內(nèi)部的this.這樣,構(gòu)造函數(shù)調(diào)用以后,這個(gè)空對(duì)象就是Generator函數(shù)的實(shí)例對(duì)象了.
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í)行的是遍歷器對(duì)象f,但是生成的對(duì)象實(shí)例是obj,將這個(gè)兩個(gè)對(duì)象統(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ù),就可以對(duì)它執(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)機(jī)
//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實(shí)現(xiàn)與ES5實(shí)現(xiàn)對(duì)比,可以看到少了用來(lái)保存狀態(tài)的外部變量,更符合函數(shù)式編程的思想.
Generator與協(xié)程
協(xié)程是一種程序運(yùn)行的方式,可以理解為"協(xié)作的線程"或"協(xié)作的函數(shù)".協(xié)程既可以用單線程實(shí)現(xiàn),也可以用多線程實(shí)現(xiàn).前者是一種特殊的子例程,后者是一種特殊的線程.
- 協(xié)程與子例程的差異
可以并行執(zhí)行,交換執(zhí)行權(quán)的線程(或函數(shù)),就稱為協(xié)程.
從實(shí)現(xiàn)上看,在內(nèi)存中,子例程只使用一個(gè)棧,而協(xié)程是同事存在多個(gè)棧,但只有一個(gè)棧是在運(yùn)行狀態(tài),也就是說(shuō),協(xié)程是以多占用內(nèi)存為代價(jià),實(shí)現(xiàn)多任務(wù)的并行. - 協(xié)程與普通線程的差異
普通的線程是搶先式的,到底哪個(gè)線程優(yōu)先得到資源,必須有運(yùn)行環(huán)境決定,但是協(xié)程是合作式的,執(zhí)行權(quán)有協(xié)程自己分配.
Generator函數(shù)是ES6對(duì)于協(xié)程的實(shí)現(xiàn),但屬于不完全實(shí)現(xiàn).因?yàn)橹挥蠫enerator函數(shù)的調(diào)用者才能將程序的執(zhí)行權(quán)還給Generator函數(shù).如果是完全執(zhí)行的協(xié)程,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)之心.
Generator與上下文
Javascript執(zhí)行函數(shù)(或塊級(jí)代碼)的時(shí)候,會(huì)在當(dāng)前上下文環(huán)境的上層產(chǎn)生一個(gè)函數(shù)運(yùn)行的上下文,變成當(dāng)前的上下文,由此形成一個(gè)上下文環(huán)境的堆棧.這個(gè)堆棧是"后進(jìn)先出"的數(shù)據(jù)結(jié)構(gòu),最后產(chǎn)生的上下文環(huán)境首先執(zhí)行完成,退出堆棧,然后再執(zhí)行完成它下層的上下文,直至所有代碼執(zhí)行完成,堆棧清空
Generator函數(shù)不是這樣,它執(zhí)行產(chǎn)生的上下文環(huán)境,一旦遇到yield命令,就會(huì)暫時(shí)退出堆棧,但是并不小時(shí),里面的所有變量和對(duì)象會(huì)凍結(jié)在當(dāng)前狀態(tài).等到對(duì)它執(zhí)行next命令時(shí),這個(gè)上下文環(huán)境又會(huì)重新加入調(diào)用棧,凍結(jié)的變量和對(duì)象恢復(fù)執(zhí)行.
應(yīng)用
- 異步操作的同步化表達(dá)
Ajax是典型的異步操作,通過(guò)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();
通過(guò)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
}
}
然后使用一個(gè)函數(shù)按次序自動(dòng)執(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ì)是一個(gè)while循環(huán).
部署Iterator接口
利用Generator函數(shù)可以在任意對(duì)象上部署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)
可以看做一個(gè)數(shù)組結(jié)構(gòu),因?yàn)镚enerator函數(shù)可以返回一系列的值,這意味著它可以對(duì)任意表達(dá)式,提供類似數(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是一個(gè)函數(shù),可以像回調(diào)函數(shù)那樣使用它
}
異步應(yīng)用
傳統(tǒng)方法
ES6誕生以前,異步編程方法大概一下四種:
- 回調(diào)函數(shù)
- 事件監(jiān)聽(tīng)
- 發(fā)布/訂閱
- Promise對(duì)象
基本概念
Promise
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
如果依次讀取兩個(gè)以上的文件,就會(huì)出現(xiàn)多重嵌套.因?yàn)槎鄠€(gè)異步操作形成了搶耦合,只要有一個(gè)操作需要修改,她的上層回調(diào)函數(shù)和下層回調(diào)函數(shù)可能都要跟著修改.這種情況成為"回調(diào)函數(shù)地獄".
Promise就是為了解決這個(gè)問(wèn)題而提出的,它是一種新的寫法,允許將回調(diào)函數(shù)的嵌套,改為鏈?zhǔn)秸{(diào)用.采用Promise,連續(xù)讀取多個(gè)文件:
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èn)題是代碼冗余,原來(lái)的任務(wù)被Promise包裝一下,不管什么操作都是一堆then,原來(lái)的語(yǔ)義變得不清楚
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ù)封裝了一個(gè)異步操作,該操作先讀取一個(gè)遠(yuǎn)程接口,然后從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ù),獲取遍歷器對(duì)象,然后使用next方法,執(zhí)行異步任務(wù)的第一階段.由于fetch模塊返回的是一個(gè)Promise對(duì)象,因此要用then方法調(diào)用下一個(gè)next方法.
可以看到,雖然Generator函數(shù)將異步操作表示的很簡(jiǎn)介,但是流程管理卻不方便.
Thunk函數(shù)
Thunk函數(shù)是自動(dòng)執(zhí)行Generator函數(shù)的一種方法
參數(shù)的求值策略
- "傳值調(diào)用"
在進(jìn)入函數(shù)體之前,就計(jì)算參數(shù)表達(dá)式的值 - "傳名調(diào)用"
直接將表達(dá)式傳入函數(shù)體,只有在用到的時(shí)候求值
Thunk函數(shù)的含義
編譯器的"傳名調(diào)用"實(shí)現(xiàn),往往是將參數(shù)放到一個(gè)臨時(shí)函數(shù)之中,再將這個(gè)臨時(shí)函數(shù)傳入函數(shù)體.這個(gè)臨時(shí)函數(shù)就叫做Thunk函數(shù).
這就是Thunk函數(shù)的定義,它是"傳名調(diào)用"的一種實(shí)現(xiàn)策略,用來(lái)替換某個(gè)表達(dá)式.
JavaScript語(yǔ)言的Thunk函數(shù)
JavaScript語(yǔ)言是傳值調(diào)用的e,它的Thunk函數(shù)含義有所不同.在JavaScript語(yǔ)言中,Thunk函數(shù)替換的不是表達(dá)式,而是多參數(shù)函數(shù),將其替換成一個(gè)只接受回調(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ù)的形式.下面是一個(gè)簡(jiǎn)單的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é)那個(gè)簡(jiǎn)單的轉(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);
}
}
}
};
主要多了一個(gè)檢查機(jī)制,變量called確?;卣{(diào)函數(shù)只運(yùn)行一次.這樣的設(shè)計(jì)與下文的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ù)可以自動(dòng)執(zhí)行
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
但是,這不適合異步操作.以讀取文件為例,下面的Generator函數(shù)封裝了兩個(gè)異步操作
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ù),因?yàn)樗梢栽诨卣{(diào)函數(shù)里,將執(zhí)行權(quán)交還給Generator函數(shù).
var g = gen();
var r1 = g.next();
//這里r1.value相當(dāng)于readFileThunk('/etc/fstab'),此時(shí)該方法還不會(huì)被執(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方法負(fù)責(zé)將指針移動(dòng)到下一步,并返回該步的信息(value和done).
可以知道Generator函數(shù)的執(zhí)行結(jié)果,其實(shí)是將同一個(gè)回調(diào)函數(shù),反復(fù)傳入next方法的value屬性.這使得我們可以用遞歸來(lái)自動(dòng)完成這個(gè)過(guò)程.
Thunk函數(shù)的自動(dòng)流程管理
Thunk函數(shù)真正的威力,在于可以自動(dòng)執(zhí)行Generator函數(shù).下面就是一個(gè)基于Thunk函數(shù)的Generator執(zhí)行器.
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
//此時(shí)result.value已經(jīng)被賦值為上一個(gè)Thunk函數(shù)名,next是一個(gè)回調(diào)函數(shù)
if (result.done) return;
result.value(next);
//當(dāng)上一個(gè)Thunk函數(shù)執(zhí)行完成之后才會(huì)調(diào)用回調(diào)函數(shù),再繼續(xù)執(zhí)行下一個(gè)Thunk函數(shù)
}
next();
//next方法從來(lái)都不會(huì)被傳入?yún)?shù)
}
function* g() {
// ...
}
run(g);
run函數(shù)就是一個(gè)Generator函數(shù)的自動(dòng)執(zhí)行器.內(nèi)部的next函數(shù)就是Thunk的回調(diào)函數(shù).next函數(shù)先將指針移到Generator函數(shù)的下一步(gen.next方法),然后判斷Generator函數(shù)是否結(jié)束(result.done屬性),如果沒(méi)結(jié)束,就將next函數(shù)再傳入Thunk函數(shù)(result.value
屬性),否則就直接退出
使用該執(zhí)行器的前提是每一個(gè)異步操作,都要是Thunk函數(shù),也就是說(shuō),跟在yield命令后面的必須是Thunk函數(shù).
Thunk函數(shù)并不是Generator函數(shù)自動(dòng)執(zhí)行的唯一方案.因?yàn)樽詣?dòng)執(zhí)行的關(guān)鍵是,必須有一種機(jī)制,自動(dòng)控制Generator函數(shù)的流程,接收和交換程序的執(zhí)行權(quán).回調(diào)函數(shù)可以做到這一點(diǎn),Promise對(duì)象也可以做到這一點(diǎn).