筆記,總結摘錄自阮一峰
筆記中有不少自己看書的總結
基本概念
核心目的:異步編程解決方案
關鍵概念:狀態(tài)機,執(zhí)行權限的傳遞,數(shù)據(jù)的傳入傳出
- 寫法
-
function
關鍵字和函數(shù)名之間有一個星號 - 函數(shù)內部,使用
yield
語句,定義內部狀態(tài) - 作為對象屬性,可以簡寫
let obj = {
* myGeneratorMethod() {
···
}
};
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
調用Generator函數(shù),函數(shù)并不執(zhí)行,而是返回一個遍歷器對象。必須調用遍歷器對象的next
方法,將函數(shù)執(zhí)行到下一個yield
語句的地方。
當遇到yield
語句,就暫停執(zhí)行,并將yield
后邊的表達式的值,作為返回對象的value
屬性值。
yield
語句如果用在一個表達式之中,必須放在圓括號里面
yield
語句用作函數(shù)參數(shù)或放在賦值表達式的右邊,可以不加括號
return
語句,將返回表達式后邊的值,并結束函數(shù)。
- 傳入與傳出
通過yield
后的表達式,可以實現(xiàn)傳出
通過next(value)
方法中,傳入?yún)?shù),可以實現(xiàn)傳入。value將作為上一個yield
語句的返回值。
我們都知道,一個運算符,都有返回值,比如
+
號,比如=
號,比如,
號。正常情況下,yield
語句的返回值,是undefined
,當我們?yōu)?code>next方法傳入?yún)?shù)時候,在恢復執(zhí)行之前,會將yield
的返回值替換為我們傳入的參數(shù)。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
所以,我們是可以對函數(shù)的行為進行控制的。這就非常方便了,比如,函數(shù)執(zhí)行權的交替。
碎知識
- 與
for...of
配合
比如,實現(xiàn)對象的遍歷
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
也可以直接加到對象的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
-
Generator.prototype.throw()
Generator函數(shù)返回的遍歷器對象,都有一個throw
方法,可以在函數(shù)體外拋出錯誤,然后在Generator函數(shù)體內捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
throw
方法可以接受一個參數(shù),該參數(shù)會被catch
語句接收,建議拋出Error
對象的實例。
不管內外,必須有try...catch
語句來捕獲錯誤,不然程序將報錯。
throw
方法被捕獲以后,會附帶執(zhí)行到下一條yield
語句。(他就是它本身帶了一個next
方法)
一旦Generator執(zhí)行過程中拋出錯誤,且沒有被內部捕獲,就不會再執(zhí)行下去了。如果此后還調用next
方法,將返回一個value
屬性等于undefined
、done
屬性等于true
的對象,即JavaScript引擎認為這個Generator已經運行結束了。
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()
Generator函數(shù)返回的遍歷器對象,還有一個return
方法,可以返回給定的值,并且終結遍歷Generator函數(shù)。
如果return(value)
方法傳值了,那就返回傳入的那個值,如果沒有傳值,就返回undefined
如果Generator函數(shù)內部有try...finally
代碼塊,那么當遇到return
時候,不會立刻結束,而是會把finally
代碼塊中的執(zhí)行完,然后再return
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 }
- yield*
在Generator函數(shù)調用另一個Generator函數(shù)時候,要用yield*
等價于,在yield*位置,展開另一個Generator函數(shù)的狀態(tài)。
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"
任何數(shù)據(jù)結構只要有Iterator接口,就可以被yield*遍歷
如果被代理的Generator函數(shù)有return
語句,那么就可以向代理它的Generator函數(shù)返回數(shù)據(jù)。
function *foo() {
yield 2;
yield 3;
return "foo";
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
yield*命令可以很方便地取出嵌套數(shù)組的所有成員。
// 下面是二叉樹的構造函數(shù),
// 三個參數(shù)分別是左樹、當前節(jié)點和右樹
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍歷函數(shù)。
// 由于返回的是一個遍歷器,所以要用generator函數(shù)。
// 函數(shù)體內采用遞歸算法,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉樹
function make(array) {
// 判斷是否為葉節(jié)點
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
- Generator函數(shù)中的this
Generator函數(shù),不能跟new
操作符一起使用,會報錯
把Generator函數(shù),當成構造函數(shù),會失效,因為它總是返回遍歷器對象,而不是那個新創(chuàng)建的對象。
一個方法解決
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
Generator 與 協(xié)程
協(xié)程(coroutine)是一種程序運行的方式,可以理解成“協(xié)作的線程”或“協(xié)作的函數(shù)”。
協(xié)程既可以用單線程實現(xiàn),是一種特殊的子例程。
也可以用多線程實現(xiàn),是一種特殊的線程。
- 協(xié)程與子例程的差異
傳統(tǒng)的“子例程”(subroutine)采用堆棧式“后進先出”的執(zhí)行方式,只有當調用的子函數(shù)完全執(zhí)行完畢,才會結束執(zhí)行父函數(shù)。協(xié)程與其不同,多個線程(單線程情況下,即多個函數(shù))可以并行執(zhí)行,但是只有一個線程(或函數(shù))處于正在運行的狀態(tài),其他線程(或函數(shù))都處于暫停態(tài)(suspended),線程(或函數(shù))之間可以交換執(zhí)行權。也就是說,一個線程(或函數(shù))執(zhí)行到一半,可以暫停執(zhí)行,將執(zhí)行權交給另一個線程(或函數(shù)),等到稍后收回執(zhí)行權的時候,再恢復執(zhí)行。這種可以并行執(zhí)行、交換執(zhí)行權的線程(或函數(shù)),就稱為協(xié)程。
從實現(xiàn)上看,在內存中,子例程只使用一個棧(stack),而協(xié)程是同時存在多個棧,但只有一個棧是在運行狀態(tài),也就是說,協(xié)程是以多占用內存為代價,實現(xiàn)多任務的并行。
- 協(xié)程與普通線程的差異
不難看出,協(xié)程適合用于多任務運行的環(huán)境。在這個意義上,它與普通的線程很相似,都有自己的執(zhí)行上下文、可以分享全局變量。它們的不同之處在于,同一時間可以有多個線程處于運行狀態(tài),但是運行的協(xié)程只能有一個,其他協(xié)程都處于暫停狀態(tài)。此外,普通的線程是搶先式的,到底哪個線程優(yōu)先得到資源,必須由運行環(huán)境決定,但是協(xié)程是合作式的,執(zhí)行權由協(xié)程自己分配。
由于ECMAScript是單線程語言,只能保持一個調用棧。引入?yún)f(xié)程以后,每個任務可以保持自己的調用棧。這樣做的最大好處,就是拋出錯誤的時候,可以找到原始的調用棧。不至于像異步操作的回調函數(shù)那樣,一旦出錯,原始的調用棧早就結束。
Generator函數(shù)是ECMAScript 6對協(xié)程的實現(xiàn),但屬于不完全實現(xiàn)。Generator函數(shù)被稱為“半?yún)f(xié)程”(semi-coroutine),意思是只有Generator函數(shù)的調用者,才能將程序的執(zhí)行權還給Generator函數(shù)。如果是完全執(zhí)行的協(xié)程,任何函數(shù)都可以讓暫停的協(xié)程繼續(xù)執(zhí)行。
如果將Generator函數(shù)當作協(xié)程,完全可以將多個需要互相協(xié)作的任務寫成Generator函數(shù),它們之間使用yield語句交換控制權。
應用
改寫異步操作——同步化表達
例子一: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);
});
}
var it = main();
it.next();
注意request方法里邊,請求到后,調用next
方法,要傳參數(shù)進去。
例子二:逐行讀取文本
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
控制流管理
例子一:同步方法的流管理
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
}
}
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函數(shù)未結束,就繼續(xù)調用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
例子二:利用for...of
let steps = [step1Func, step2Func, step3Func];
function *iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
以上將任務,分成多個步驟
let jobs = [job1, job2, job3];
function *iterateJobs(jobs){
for (var i=0; i< jobs.length; i++){
var job = jobs[i];
yield *iterateSteps(job.steps);
}
}
又將項目,分成多個任務。
最后,用for...of
循環(huán),一次性執(zhí)行所有任務的所有步驟。
for (var step of iterateJobs(jobs)){
console.log(step.id);
}
部署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ù)
- 事件監(jiān)聽
- 發(fā)布/訂閱模式
- Promise 對象
ES6,有了Generator
幾種方法的分析
回調函數(shù),在寫法上,會出現(xiàn)多重嵌套的問題(回調地獄)
Promise解決了這個問題,但是引入了大量的Promise語法。總體來說,沒有新意。
Generator函數(shù)是協(xié)程的ES6實現(xiàn),通過執(zhí)行權的交替,實現(xiàn)了異步編程。
Generator的自動流程管理
- 回到函數(shù),封裝Thunk函數(shù)
- Promise
Thunk 函數(shù)
參數(shù)的求職策略
即,函數(shù)的參數(shù)到底何時求值?
- 傳值調用
在進入函數(shù)體之前。c用的傳值調用 - 傳名調用
在真正用到時候,再求值。
Thunk函數(shù),在傳名調用實現(xiàn)中,將參數(shù)放入一個臨時函數(shù)之中,再將這個臨時函數(shù),傳入函數(shù)體。這個臨時函數(shù),就是Thunk函數(shù)。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
JavaScript的Thunk函數(shù)
js是傳值調用,它的Thunk函數(shù),替換的是多參數(shù)函數(shù),將其替換成,只接受回調函數(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);
也就是說,我們通過thunk將回到函數(shù),普通參數(shù)分離開來傳入。這為我們交替執(zhí)行權做準備。
下邊是簡單的Thunk函數(shù)轉換器
// 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版本
var Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
使用方法
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
生產環(huán)境的Thunk
建議使用Thunkify模塊
它的源碼,與上邊的很像
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確保回調函數(shù)只運行一次。
重點:利用Thunk的 Generator函數(shù)自動流程管理
由于多個異步操作,需要保證前一步執(zhí)行完,再執(zhí)行后一步。可以這樣
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());
};
兩個異步操作,在執(zhí)行他們時候,把執(zhí)行權交給了另一個函數(shù),然后,在另一個函數(shù)(readFileThunk
)中,當異步操作完成,我們再把執(zhí)行權返還給gen
,這樣就保證了前一步完成,再執(zhí)行下一步。
現(xiàn)在我們有Thunk函數(shù),有了Generator函數(shù),關鍵就是,自動執(zhí)行的函數(shù)怎么寫。
我們的目的是,在Thunk的回調函數(shù)與Generator之間進行切換
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
// 注意這一步,result.value是gen函數(shù)中的readFileThunk,
// 它是一個Thunk函數(shù),也就是說,這一步等價于readFileThunk(file1)(next)
}
next();
}
function* g() {
// ...
}
run(g);
上邊的函數(shù)中:
第一步——next就是Thunk函數(shù)的回調函數(shù),首先將執(zhí)行權還給gen函數(shù)
第二步——gen 函數(shù)執(zhí)行到y(tǒng)ield語句,將執(zhí)行權交給next函數(shù),并把需要的異步操作一并傳給next函數(shù)。
第三步——run函數(shù)收到返回的對象,判斷流程是否結束,如果沒有結束,直接調用傳回的異步操作,即result.value(next),等價于readFileThunk(file1)(next)
第四步——異步操作完成,將會調用回到函數(shù)next,并將請求的數(shù)據(jù)傳入next函數(shù)。此時,next函數(shù)重復第一步,并將數(shù)據(jù)傳回gen函數(shù)。
如此往復循環(huán),知道結束
也就是說,異步操作的回調函數(shù),用來控制流程,并且將數(shù)據(jù)原封不動的傳回給gen函數(shù)。真正對請求數(shù)據(jù)的操作不是在回調函數(shù)里做的,而是在gen函數(shù)里。
co模塊——基于Promise對象的Generator函數(shù)自動流程管理
TJ Holowaychuk發(fā)布的小工具,就是上邊的run函數(shù),只不過用的是Promise
一個例子:
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
首先將異步操作封裝成Promise(上一部分是封裝成Thunk)。
如果手動執(zhí)行:
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
其實就是一堆then,,,
ok,接下來寫自動執(zhí)行器
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
跟上一部分沒什么差別。用Promise編程而已
來看看co的源碼
最外層,co接受一個Generator,返回一個Promise
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
});
}
在返回的Promise對象中,首先檢查gen
是否為Generator,如果是,就執(zhí)行,如果不是,就返回,并將Promise的狀態(tài)resolved
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
});
}
co將next方法抽象為兩部分,1-執(zhí)行權交給gen,2-調用自身。在第一部分加入了出錯處理,使得方法更健壯。
function co(gen) {
var ctx = this;
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);// 關鍵部分
}
});
}
最關鍵的是新的next函數(shù)
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)
+ '"'
)
);
}
如果gen結束了,那執(zhí)行器也結束
第二行,保證gen傳過來的每一步,都是Promise
第三行,如果確實是Promise,就執(zhí)行異步操作,并設置onFulfilled為成功的回調函數(shù)。
第四行,如果參數(shù)不符合要求,終止執(zhí)行,并將返回的Promise狀態(tài)改為rejected。
co支持并發(fā)的異步操作
并發(fā),即,某些操作同時進行,等到他們全部完成,才執(zhí)行下一步
將并發(fā)的操作放在數(shù)組/對象里邊,跟在yield語句后邊即可
// 數(shù)組的寫法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
// 對象的寫法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
另一個例子
co(function* () {
var values = [n1, n2, n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x) {
// do something async
return y
}
處理Stream
Stream,就是將整個數(shù)據(jù),一塊塊挨著處理。
Stream模式使用EventEmitter API,會釋放三個事件
-
data
事件:下一塊數(shù)據(jù)準備好了 -
end
事件:整個數(shù)據(jù)流處理完了 -
error
事件:發(fā)生錯誤
利用Promise.race()
函數(shù),可以判斷只有當data
事件發(fā)生,才進入下一個數(shù)據(jù)塊的處理。
const co = require('co');
const fs = require('fs');
const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;
co(function*() {
while(true) {
const res = yield Promise.race([
new Promise(resolve => stream.once('data', resolve)),
new Promise(resolve => stream.once('end', resolve)),
new Promise((resolve, reject) => stream.once('error', reject))
]);
if (!res) {
break;
}
stream.removeAllListeners('data');
stream.removeAllListeners('end');
stream.removeAllListeners('error');
valjeanCount += (res.toString().match(/valjean/ig) || []).length;
}
console.log('count:', valjeanCount); // count: 1120
});