前兩年大量的在寫+
Generator
co
,用它來寫一些類似同步的代碼
但實際上,
并不是被造出來幹這個使的,不然也就不會有後來的
Generator
、
async
了
await
Generator
是一個可以被暫停的函數,并且何時恢複,由調用方決定
希望本文可以幫助你了解
究竟是什麼,以及怎麼用
Generator
放一張圖來表示我對
Generator
的了解:
一個咖啡機,雖說我并不喝咖啡,可惜找不到造王老吉的機器-.-
我所了解的Generator咖啡機大概就是這麼的一個樣子的:
- 首先,我們往機器裡邊放一些咖啡豆
- 等我們想喝咖啡的時候,就可以按開關(gen.next()),機器開始磨咖啡豆、煮咖啡、接下來就得到咖啡了
- 等接滿了一杯咖啡後,閥門就會自動關閉(yield)
- 如果你一開始往機器裡邊放的咖啡豆很多的話,此時,機器裡邊還是會有一些剩餘的,下次再想喝還可以繼續按開關,執行(磨豆、煮咖啡、接咖啡)這一套操作
拿
Generator
将上述咖啡機實作一下:
function * coffeeMachineGenerator (beans) {
do {
yield cookCoffee()
} while (--beans)
// 煮咖啡
function cookCoffee () {
console.log('cooking')
return 'Here you are'
}
}
// 往咖啡機放咖啡豆
let coffeeMachine = coffeeMachineGenerator(10)
// 我想喝咖啡了
coffeeMachine.next()
// 我在3秒後還會喝咖啡
setTimeout(() => {
coffeeMachine.next()
}, 3 * 1e3)
代碼運作後,我們首先會得到一條
cooking
的
log
,
然後在
3s
後會再次得到一條
log
。
這就解釋了
Generator
是什麼:
一個可以暫停的疊代器
調用
next
來擷取資料(我們自己來決定是否何時煮咖啡)
在遇到
yield
以後函數的執行就會停止(接滿了一杯,閥門關閉)
我們來決定何時運作剩餘的代碼
next
(什麼時候想喝了再去煮)
這是
Generator
中最重要的特性,我們隻有在真正需要的時候才擷取下一個值,而不是一次性擷取所有的值
Generator的文法
聲明
Generator
函數有很多種途徑,最重要的一點就是,在
function
關鍵字後添加一個
*
function * generator () {}
function* generator () {}
function *generator () {}
let generator = function * () {}
let generator = function* () {}
let generator = function *() {}
// 錯誤的示例
let generator = *() => {}
let generator = ()* => {}
let generator = (*) => {}
或者,因為是一個函數,也可以作為一個對象的屬性來存在:
class MyClass {
* generator() {}
*generator2() {}
}
const obj = {
*generator() {}
* generator() {}
}
generator的初始化與複用
一個
Generator
函數通過調用兩次方法,将會生成兩個完全獨立的
狀态機
是以,儲存目前的
Generator
對象很重要:
function * generator (name = 'unknown') {
yield `Your name: ${name}`
}
const gen1 = generator()
const gen2 = generator('Niko Bellic')
gen1.next() // { value: Your name: unknown , done: false}
gen2.next() // { value: Your name: Niko Bellic, done: false}
Method: next()
最常用的
next()
方法,無論何時調用它,都會得到下一次輸出的傳回對象(在代碼執行完後的調用将會始終傳回
{value: undefined, done: true}
)。
next
總會傳回一個對象,包含兩個屬性值:
value
:
yield
關鍵字後邊表達式的值
done
:如果已經沒有
yield
關鍵字了,則會傳回
true
.
function * generator () {
yield 5
return 6
}
const gen = generator()
console.log(gen.next()) // {value: 5, done: false}
console.log(gen.next()) // {value: 6, done: true}
console.log(gen.next()) // {value: undefined, done: true}
console.log(gen.next()) // {value: undefined, done: true} -- 後續再調用也都會是這個結果
作為疊代器使用
Generator
函數是一個可疊代的,是以,我們可以直接通過
for of
來使用它。
function * generator () {
yield 1
yield 2
return 3
}
for (let item of generator()) {
item
}
// 1
// 2
return
不參與疊代
疊代會執行所有的
yield
,也就是說,在疊代後的
Generator
對象将不會再傳回任何有效的值
Method: return()
我們可以在疊代器對象上直接調用
return()
,來終止後續的代碼執行。
在
return
後的所有
next()
調用都将傳回
{value: undefined, done: true}
function * generator () {
yield 1
yield 2
yield 3
}
const gen = generator()
gen.return() // {value: undefined, done: true}
gen.return('hi') // {value: "hi", done: true}
gen.next() // {value: undefined, done: true}
Method: throw()
在調用
throw()
後同樣會終止所有的
yield
執行,同時會抛出一個異常,需要通過
try-catch
來接收:
function * generator () {
yield 1
yield 2
yield 3
}
const gen = generator()
gen.throw('error text') // Error: error text
gen.next() // {value: undefined, done: true}
Yield的文法
yield
的文法有點像
return
,但是,
return
是在函數調用結束後傳回結果的
并且在調用
return
之後不會執行其他任何的操作
function method (a) {
let b = 5
return a + b
// 下邊的兩句代碼永遠不會執行
b = 6
return a * b
}
method(6) // 11
method(6) // 11
而yield的表現則不一樣
function * yieldMethod(a) {
let b = 5
yield a + b
// 在執行第二次`next`時,下邊兩行則會執行
b = 6
return a * b
}
const gen = yieldMethod(6)
gen.next().value // 11
gen.next().value // 36
yield*
yield*
用來将一個
Generator
放到另一個
Generator
函數中執行。
有點像
[...]
的功能:
function * gen1 () {
yield 2
yield 3
}
function * gen2 () {
yield 1
yield * gen1()
yield 4
}
let gen = gen2()
gen.next().value // 1
gen.next().value // 2
gen.next().value // 3
gen.next().value // 4
yield的傳回值
yield
是可以接收傳回值的,傳回值可以在後續的代碼被使用
一個詭異的寫法
function * generator (num) {
return yield yield num
}
let gen = generator(1)
console.log(gen.next()) // {value: 1, done: false}
console.log(gen.next(2)) // {value: 2, done: false}
console.log(gen.next(3)) // {value: 3, done: true }
我們在調用第一次
next
時候,代碼執行到了
yield num
,此時傳回
num
然後我們再調用
next(2)
,代碼執行的是
yield (yield num)
,而其中傳回的值就是我們在
next
中傳入的參數了,作為
yield num
的傳回值存在。
以及最後的
next(3)
,執行的是這部分代碼
return (yield (yield num))
,第二次
yield
表達式的傳回值。
一些實際的使用場景
上邊的所有示例都是建立在已知次數的
Generator
函數上的,但如果你需要一個未知次數的
Generator
,僅需要建立一個無限循環就夠了。
一個簡單的随機數生成
比如我們将實作一個随機數的擷取:
function * randomGenerator (...randoms) {
let len = randoms.length
while (true) {
yield randoms[Math.floor(Math.random() * len)]
}
}
const randomeGen = randomGenerator(1, 2, 3, 4)
randomeGen.next().value // 傳回一個随機數
代替一些遞歸的操作
那個最著名的斐波那契數,基本上都會選擇使用遞歸來實作
但是再結合着
Generator
以後,就可以使用一個無限循環來實作了:
function * fibonacci(seed1, seed2) {
while (true) {
yield (() => {
seed2 = seed2 + seed1;
seed1 = seed2 - seed1;
return seed2;
})();
}
}
const fib = fibonacci(0, 1);
fib.next(); // {value: 1, done: false}
fib.next(); // {value: 2, done: false}
fib.next(); // {value: 3, done: false}
fib.next(); // {value: 5, done: false}
fib.next(); // {value: 8, done: false}
與async/await的結合
再次重申,我個人不認為async/await是Generator的文法糖。。
如果是寫前端的童鞋,基本上都會遇到處理分頁加載資料的時候
如果結合着
Generator
async
await
,我們可以這樣實作:
async function * loadDataGenerator (url) {
let page = 1
while (true) {
page = (yield await ajax(url, {
data: page
})) || ++page
}
}
// 使用setTimeout模拟異步請求
function ajax (url, { data: page }) {
return new Promise((resolve) => {
setTimeout(_ => {
console.log(`get page: ${page}`);
resolve()
}, 1000)
})
}
let loadData = loadDataGenerator('get-data-url')
await loadData.next()
await loadData.next()
// force load page 1
await loadData.next(1)
await loadData.next()
// get page: 1
// get page: 2
// get page: 1
// get page: 2
這樣我們可以在簡單的幾行代碼中實作一個分頁控制函數了。
如果想要從加載特定的頁碼,直接将
page
傳入
next
即可。
小記
Generator
還有更多的使用方式,(實作異步流程控制、按需進行資料讀取)
個人認為,
Generator
的優勢在于代碼的惰性執行,
Generator
所實作的事情,我們不使用它也可以做到,隻是使用
Generator
後,能夠讓代碼的可讀性變得更好、流程變得更清晰、更專注于邏輯的實作。
如果有什麼不懂的地方 or 文章中一些的錯誤,歡迎指出
參考資料
- Javascript (ES6) Generators — Part I: Understanding Generators
- What are JavaScript Generators and how to use them