Generator
Generator 函數是一個狀態機,封裝了多個內部狀態。執行 Generator 函數會返回一個遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
};
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
Generator函數調用后不會立刻執行,而是返回一個指向內部狀態的指針對象。
調用該對象的next()方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield語句(或return語句)為止。
yield語句是暫停執行的標記,而next方法可以恢復執行。
遍歷器對象的next方法的運行邏輯如下。
(1)遇到yield語句,就暫停執行后面的操作,並將緊跟在yield后面的那個表達式的值,作為返回的對象的value屬性值。
(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield語句。
(3)如果沒有再遇到新的yield語句(最后一個yield返回的對象的done為false),就一直運行到函數結束,直到return語句為止,並將return語句后面的表達式的值,作為返回的對象的value屬性值(對象的done為true)。
(4)如果該函數沒有return語句,則返回的對象的value屬性值為undefined。
yield句本身沒有返回值,或者說總是返回undefined。next方法可以帶一個參數,該參數就會被當作上一個yield語句的返回值。
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 }
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 }
for...of循環可以自動遍歷Generator函數時生成的Iterator對象,且此時不再需要調用next方法。
一旦next方法的返回對象的done屬性為true,for...of循環就會中止,且不包含該返回對象,所以下面代碼的return語句返回的6,不包括在for...of循環之中。
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
Generator函數返回的遍歷器對象,還有一個return方法,可以返回給定的值,並且終結遍歷Generator函數。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果return方法調用時,不提供參數,則返回值的value屬性為undefined。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
如果Generator函數內部有try...finally代碼塊,那么return方法會推遲到finally代碼塊執行完再執行。
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 }
在一個 Generator 函數里面執行另一個 Generator 函數要用到yield*語句。
function* foo() {
yield 'a';
yield 'b';
}
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"
Generator異步
異步任務的封裝
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
Generator自動執行器
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
var g = function* (){
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
};
run(g);
基於 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());
};
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);
async 函數
async 函數,一句話,它就是 Generator 函數的語法糖。
上面的例子,寫成async函數,就是下面這樣:
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 asyncReadFile = function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
asyncReadFile();
async函數對 Generator 函數的改進,體現在以下四點。
(1)內置執行器。
(2)更好的語義。
(3)更廣的適用性。
(4)返回值是 Promise。
async函數完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。
async函數內部return語句返回的值,會成為then方法回調函數的參數。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
async函數內部報錯錯誤會導致返回的Promise對象變為reject狀態,拋出的錯誤會被catch方法接受到
async function f() {
throw new Error('this is an error');
}
f().then(
v => console.log(v),
e => console.log(e)
)
async函數返回的 Promise 對象,必須等到內部所有await命令后面的 Promise 對象執行完,才會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操作執行完,才會執行then方法指定的回調函數。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
上面代碼中,函數getTitle內部有三個操作:抓取網頁、取出文本、匹配頁面標題。只有這三個操作全部完成,才會執行then方法里面的console.log。
await命令
正常情況下,await命令后面是一個 Promise 對象。如果不是,會被轉成一個立即resolve的 Promise 對象。
async function f() {
return await 123;
}
f().then(v => console.log(v))
// 123
上面代碼中,await命令的參數是數值123,它被轉成 Promise 對象,並立即resolve。
await命令后面的 Promise 對象如果變為reject狀態,則reject的參數會被catch方法的回調函數接收到。
async function f() {
await Promise.reject('出錯了');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了
只要一個await語句后面的 Promise 變為reject,那么整個async函數都會中斷執行。
async function f() {
await Promise.reject('出錯了');
await Promise.resolve('hello world'); // 不會執行
}
有時,我們希望即使前一個異步操作失敗,也不要中斷后面的異步操作。這時可以將第一個await放在try...catch結構里面,這樣不管這個異步操作是否成功,第二個await都會執行。
async function f() {
try {
await Promise.reject('出錯了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
另一種方法是await后面的 Promise 對象再跟一個catch方法,處理前面可能出現的錯誤。
async function f() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出錯了
// hello world
如果有多個await命令,可以統一放在try...catch結構中。
async function main() {
try {
var val1 = await firstStep();
var val2 = await secondStep(val1);
var val3 = await thirdStep(val1, val2);
console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
}
多個await命令后面的異步操作,如果不存在繼發關系,最好讓它們同時觸發。
//getFoo和getBar是兩個獨立的異步操作(即互不依賴),被寫成繼發關系。這樣比較耗時,因為只有getFoo完成以后,才會執行getBar,完全可以讓它們同時觸發。
let foo = await getFoo();
let bar = await getBar();
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;