什么是 Generator 函數(shù)
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案(可以按序執(zhí)行異步方法),但其語法行為與傳統(tǒng)函數(shù)完全不同。
先看看 Generator 函數(shù)在形式上的定義:
let log = console.log
function* gen() {
yield 1
yield 2
yield 3
return 4
}
let g = gen()
log(g.next()) // { value: 1, done: false }
log(g.next()) // { value: 2, done: false }
log(g.next()) // { value: 3, done: false }
log(g.next()) // { value: 4, done: true }
log(g.next()) // { value: undefined, done: true }
- generator 函數(shù)和普通函數(shù)不同的是,generator 由 function*定義(function后帶有一個星號 *)
- yield 表達式:你可以理解為 Generator 函數(shù)是一個狀態(tài)機,封裝了多個內(nèi)部狀態(tài),而函數(shù)體內(nèi)部使用 yield 表達式來定義不同的內(nèi)部狀態(tài)
- 執(zhí)行 Generator 函數(shù)后會返回一個遍歷器對象(不會直接得到 return 的結(jié)果)
- 依次調(diào)用遍歷器對象的 next 方法,可以遍歷 Generator 函數(shù)內(nèi)部的每一個狀態(tài)
繼續(xù)分析上面的代碼,如果一個 yield 表達式算一個 generator 的一個狀態(tài),上述的代碼一共有4個狀態(tài),即 3 個 yield 表達式和 1 個 return 語句(return 也算一個狀態(tài))。
每次調(diào)用遍歷器的 next 方法,都會返回一個對象 {value: xxx, done: xxx},表示當前遍歷器狀態(tài)的信息,該對象包含兩個屬性,一個是 value 屬性,表示當前 yield 表達式的值, 一個是 done 屬性,表示當前遍歷是否結(jié)束。當所有狀態(tài)遍歷結(jié)束后,done 的值變?yōu)?true。
當遍歷結(jié)束后如果繼續(xù)調(diào)用遍歷器的 next 方法,done 的值不再改變,而 value 的值變?yōu)?undefined。
yield 表達式
- yield 語句就是暫停標志,遇到 yield 語句就暫停執(zhí)行后面的操作,并將緊跟在 yield 后的表達式的值作為返回的對象的 value 屬性值。
- 下一次調(diào)用next 方法時再繼續(xù)往下執(zhí)行,知道遇到下一條 yield。
- 如果沒有再遇到新的 yield 語句,就一直運行到函數(shù)結(jié)束,直到 return 語句為止,并將 return 語句后面的表達式的值作為返回對象的 value 屬性值。
- 如果該函數(shù)沒有 return 語句,則返回對象的 value 屬性值為 undefined。
不能在其他普通函數(shù)體中使用 yield,會報語法錯誤
(function (){
yield 1;
})() // SyntaxError: Unexpected number
yield 表達式如果用在另一個表達式中,必須放在圓括號里面
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
next 方法
注意,yield 語句本身沒有返回值,或者說總是返回 undefined。next 方法可以帶有一個參數(shù),該參數(shù)會被當作上一條 yield 語句的返回值。
因為上面這句話,也就有了下面這個經(jīng)典的例子:
function* foo(x) {
let y = 2 * (yield(x + 1))
let z = yield(y / 3)
return (x + y + z)
}
let g = foo(5)
log(g.next())
log(g.next())
log(g.next())
猜猜打印的結(jié)果什么?答案如下:
{ value: 6, done: false }
{ value: NaN, done: false }
{ value: NaN, done: true }
第一個 next() 語句中的參數(shù)總是無效的,無論傳入什么值,都不會對接下來的表達式產(chǎn)生影響,因為在執(zhí)行第一個 next() 方法的時候,還沒遇到 yield 語句,參數(shù)也無法作為返回值,所以第一個next的參數(shù)是無意義的。
可能有點繞,按步驟說明下上述代碼的執(zhí)行過程:
g = foo(), foo 生成了遍歷器,返回給了 g 變量
執(zhí)行第一個,g.next(),g 執(zhí)行 next 方法,此時在代碼中體現(xiàn)為執(zhí)行了 x + 1 = 6
然后遇到了 yield 語句,暫停,返回結(jié)果 {value: 6, done: false}
執(zhí)行第二個,g.next(), next 參數(shù)為空,即默認上一個yield語句的返回值為 undefined,執(zhí)行 let y = 2 * undefind, y = NaN, 繼續(xù)計算 y/3 , 即 NaN/3 = NaN
然后遇到了 yield 語句,暫停,返回結(jié)果 {value: NaN, done: false}
執(zhí)行第三個,g.next(), next 參數(shù)為空,即默認上一個yield語句的返回值為 undefined, 執(zhí)行 let z = undefined, return (x+y+z) 即 return (5+NaN+undefined) 最后的返回值 undefined。
為 next 方法傳入一些參數(shù):
function* foo(x) {
let y = 2 * (yield(x + 1))
let z = yield(y / 3)
return (x + y + z)
}
let g = foo(5)
log(g.next()) // { value:6, done:false }
log(g.next(12)) // { value:8, done:false }
log(g.next(13)) // { value:42, done:true }
可以按上述的過程走一遍,然后把參數(shù)代入就得到了注釋中的結(jié)果。
for...of 循環(huán)
for...of循環(huán)可以自動遍歷 Generator 函數(shù)運行時生成的Iterator對象,且此時不再需要調(diào)用next方法。
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
上面代碼使用for...of循環(huán),依次顯示 5 個yield表達式的值。這里需要注意,一旦next方法的返回對象的done屬性為true,for...of循環(huán)就會中止,且不包含該返回對象,所以上面代碼的return語句返回的6,不包括在for...of循環(huán)之中。
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
上面代碼中,遍歷器對象i連續(xù)拋出兩個錯誤。第一個錯誤被 Generator 函數(shù)體內(nèi)的catch語句捕獲。i第二次拋出錯誤,由于 Generator 函數(shù)內(nèi)部的catch語句已經(jīng)執(zhí)行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被拋出了 Generator 函數(shù)體,被函數(shù)體外的catch語句捕獲。
throw方法可以接受一個參數(shù),該參數(shù)會被catch語句接收,建議拋出Error對象的實例。
var g = function* () {
try {
yield;
} catch (e) {
console.log(e);
}
};
var i = g();
i.next();
i.throw(new Error('出錯了!'));
// Error: 出錯了!(…)
注意,不要混淆遍歷器對象的throw方法和全局的throw命令。上面代碼的錯誤,是用遍歷器對象的throw方法拋出的,而不是用throw命令拋出的。后者只能被函數(shù)體外的catch語句捕獲。
利用 Genertor 函數(shù)返回的迭代器的 throw 方法可以很好地處理代碼運行中的錯誤,這點在實現(xiàn)一個具有錯誤處理的 async 函數(shù)中也有一定體現(xiàn)。
參考文章:
[1] https://www.cnblogs.com/rogerwu/p/10764046.html
[2] https://es6.ruanyifeng.com/#docs/generator
async 和 await 的簡單實現(xiàn)
async/await 被稱為是 generator 的語法糖,實際上 async/await 就是 generator 函數(shù)加上自動執(zhí)行器來實現(xiàn)的。
從上一小節(jié)知道,generator 函數(shù)是不會自動執(zhí)行的,每一次調(diào)用它的 next 方法,會停留在下一個 yield 的位置。
利用這個特性,我們只要編寫一個自動執(zhí)行的函數(shù),就可以讓這個 generator 函數(shù)完全實現(xiàn) async 函數(shù)的功能。
先看一個 async 函數(shù)的示例
let p = function (val) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(val)
}, 1000);
})
}
async function testAsync() {
const data1 = await p(1)
console.log(data1)
const data2 = await p(2)
console.log(data2)
const data3 = await p(3)
console.log(data3)
}
testAsync()
這個 async 函數(shù)的效果就是,每隔1秒,按序打印出 1,2,3
嘗試利用 generator 函數(shù)替代 async 函數(shù)來實現(xiàn)這段代碼。
function* testG() {
const data1 = yield p(1)
console.log(data1)
const data2 = yield p(2)
console.log(data2)
const data3 = yield p(3)
console.log(data3)
}
let gen = testG()
let dataPromise = gen.next().value // 返回對象中的 value 值才是一個 promise
dataPromise.then((val) => {
let data2Promise = gen.next(val).value
data2Promise.then((val2) => {
let data3Promise = gen.next(val2).value
data3Promise.then((val3) => {
gen.next(val3)
})
})
})
// 按序每隔一秒打印 1、2、3
為了保證每個狀態(tài)獲取到正確的值,并且按序執(zhí)行,則 gen.next() 必須在 promise 對象的 then 方法的回調(diào)中執(zhí)行,以保證在 generator 的 next 方法中傳入正確的參數(shù),當有多個異步方法要執(zhí)行的時候,最終實現(xiàn)的效果就像一個回調(diào)地獄的調(diào)用。
不過好歹是用 generator 實現(xiàn)了 async 函數(shù)的效果,盡管代碼不是很優(yōu)美。
實現(xiàn)一個高階函數(shù)來代替回調(diào)地獄
先不考慮包含錯誤處理的情況:
function asyncToGenerator(generatorFunc) {
return function () {
// 相當于 gen = generatorFunc() ,順便引入上下文環(huán)境,返回一個遍歷器
const gen = generatorFunc.apply(this, arguments)
return new Promise((resolve) => {
// 定義一個步進函數(shù),遞歸調(diào)用,直到遍歷器的返回結(jié)果 done = true
function step(arg) {
let generatorResult = gen.next(arg)
const {
value,
done
} = generatorResult
if (done) {
return resolve(value)
} else {
// 不一定每一個返回值都是 promise 函數(shù),這里需要用 Promise.resolve 方法來包裝一下。
return Promise.resolve(value).then(val => step(val))
}
}
step() // 默認參數(shù)是 undefined,所以第一次執(zhí)行這里不傳參
})
}
}
asyncToGenerator(testG)()
考慮錯誤處理的情況,加逐行解釋代碼
參考:https://juejin.im/post/6844904102053281806
function asyncToGenerator(generatorFunc) {
// 返回的是一個新的函數(shù)
return function() {
// 先調(diào)用generator函數(shù) 生成迭代器
// 對應 var gen = testG()
const gen = generatorFunc.apply(this, arguments)
// 返回一個promise 因為外部是用.then的方式 或者await的方式去使用這個函數(shù)的返回值的
// var test = asyncToGenerator(testG)
// test().then(res => console.log(res))
return new Promise((resolve, reject) => {
// 內(nèi)部定義一個step函數(shù) 用來一步一步的跨過yield的阻礙
// key有next和throw兩種取值,分別對應了gen的next和throw方法
// arg參數(shù)則是用來把promise resolve出來的值交給下一個yield
function step(key, arg) {
let generatorResult
// 這個方法需要包裹在try catch中
// 如果報錯了 就把promise給reject掉 外部通過.catch可以獲取到錯誤
try {
generatorResult = gen[key](arg)
} catch (error) {
return reject(error)
}
// gen.next() 得到的結(jié)果是一個 { value, done } 的結(jié)構(gòu)
const { value, done } = generatorResult
if (done) {
// 如果已經(jīng)完成了 就直接resolve這個promise
// 這個done是在最后一次調(diào)用next后才會為true
// 以本文的例子來說 此時的結(jié)果是 { done: true, value: 'success' }
// 這個value也就是generator函數(shù)最后的返回值
return resolve(value)
} else {
// 除了最后結(jié)束的時候外,每次調(diào)用gen.next()
// 其實是返回 { value: Promise, done: false } 的結(jié)構(gòu),
// 這里要注意的是Promise.resolve可以接受一個promise為參數(shù)
// 并且這個promise參數(shù)被resolve的時候,這個then才會被調(diào)用
return Promise.resolve(
// 這個value對應的是yield后面的promise
value
).then(
// value這個promise被resove的時候,就會執(zhí)行next
// 并且只要done不是true的時候 就會遞歸的往下解開promise
// 對應gen.next().value.then(value => {
// gen.next(value).value.then(value2 => {
// gen.next()
//
// // 此時done為true了 整個promise被resolve了
// // 最外部的test().then(res => console.log(res))的then就開始執(zhí)行了
// })
// })
function onResolve(val) {
step("next", val)
},
// 如果promise被reject了 就再次進入step函數(shù)
// 不同的是,這次的try catch中調(diào)用的是gen.throw(err)
// 那么自然就被catch到 然后把promise給reject掉啦
function onReject(err) {
step("throw", err)
},
)
}
}
step("next")
})
}
}