一、什麼是 Generator
Generator 用來控制循環流程,主要解決異步編程嵌套層級較深的問題。
二、ES6 如何讓遍歷“停”下來
ES5 中循環一旦執行則無法停下
function loop() {
for (let i = 0; i < 5; i++) {
console.log(i)
}
}
loop()
// 0
// 1
// 2
// 3
// 5
而使用 ES6 Generator 可以將執行的循環停下,步驟如下:
1、在 loop
前面加一個星號
2、在輸出前面加 yield
3、定義一個變量將 loop
賦值給 l
function* loop() {
for (let i = 0; i < 5; i++) {
yield console.log(i)
}
}
const l = loop()
// 這個時候並沒有輸出,若要輸出調用 next 方法
l.next() // 0
l.next() // 1
l.next() // 2
l.next() // 3
l.next() // 4
l.next() // 之後不會輸出任何東西
可用於年會抽獎、自定義遍歷器等場景
三、Basic Syntax —— 基礎語法
1、遍歷器就是一個函數,但與普通的函數不同,形式上多了一個 *
2、函數內部使用 yield
停下來
3、調用時不會立即執行而是返回一個生成器對象
4、返回的生成器對象調用 next
來控制循環
5、Generator
函數的定義不能使用箭頭函數,否則會觸發報錯 SyntaxError
function* gen() {
let val
val = yield 1
console.log(val)
}
const l = gen()
console.log(l) // gen {<suspended>}
l.next() // 沒有任何輸出
l.next() // undefined yield 表達式沒有返回值,所以返回 undefined
6、next()
的返回值
① 第一個參數:返回的值
② 第二個參數:done
屬性,表示是否遍歷完成,false
是沒有遍歷完,true
是遍歷完成
7、再執行一次 next()
方法會繼續執行
function* gen() {
let val
val = yield [1, 2, 3]
console.log(val) // undefined
}
const l = gen()
console.log(l.next()) // {value: Array(3), done: false}
console.log(l.next()) // {value: undefined, done: true}
function* gen() {
let val
// yield 後面加了一個星號,後面是一個遍歷的對象,所以可以嵌套一個 Generator對象
val = yield* [1, 2, 3]
console.log(val) // undefined
}
const l = gen()
console.log(l.next()) // {value: 1, done: false}
console.log(l.next()) // {value: 2, done: false}
擴展
1、yield 有沒有返回值?
沒有,但是遍歷器對象的 next
方法可以修改這個默認值
2、和 ES5 相比,是如何控制程序的停止和啓動的?
使用 yield
去控制停止,使用 next
去控制啓動
四、Senior Syntax —— 高級語法
1、next 添加參數
next
函數寫參數,作爲 yield
的返回值
function* gen() {
let val
val = yield [1, 2, 3]
console.log(val) // 20
}
const l = gen()
console.log(l.next(10))// {value: Array(3), done: false}
// 此時 yield 沒有賦值,所以 10 並沒有用
console.log(l.next(20))// {value: undefined, done: true}
// 此時 yield 對 val 進行賦值操作,yield 表達式的值是 20
再舉個例子:
function* gen() {
var val = 100
while (true) {
console.log(`before${val}`)
val = yield val
console.log(`return ${val}`)
}
}
let g = gen()
console.log(g.next(20).value)
// before 100
// 100
console.log(g.next(30).value)
// return 30
// before 30
// 30
console.log(g.next(40).value)
// return 40
// before 40
// 40
① g.next(20)
這句代碼會執行 gen
內部的代碼,遇到第一個 yield
暫停,所以 console.log("before "+val)
執行輸出了 before 100
,此時的 val
是 100
,所以執行到 yield val
返回了 100
,注意 yield val
並沒有賦值給 val
。
② g.next(30)
這句代碼會繼續執行 gen
內部的代碼,也就是 val = yield val
這句,因爲 next
傳入了 30
,所以 yield val
這個返回值就是 30
,因此 val
被賦值 30
,執行到 console.log("return "+val)
輸出了 30
,此時沒有遇到 yield
代碼繼續執行,也就是 while
的判斷,繼續執行 console.log("before "+val)
輸出了 before 30
,再執行遇到了 yield val
程序暫停。
③ g.next(40)
重複步驟 2
。
2、return 控制結束
function* gen() {
let val
val = yield [1, 2, 3]
console.log(val) // 沒有執行
}
const l = gen()
console.log(l.next(10))// {value: Array(3), done: false}
console.log(l.return())// {value: undefined, done: true}
// 返回操作,函數終止
console.log(l.next(20))// {value: undefined, done: true}
添加返回值的參數
function* gen() {
let val
val = yield [1, 2, 3]
console.log(val) // 沒有執行
}
const l = gen()
console.log(l.next(10))// {value: Array(3), done: false}
console.log(l.return(100))// {value: 100, done: true}
// 返回操作,函數終止
console.log(l.next(20))// {value: undefined, done: true}
3、throw 拋出異常控制
function* gen() {
while (true) {
try {
yield 1
} catch (e) {
console.log(e.message) // ss
}
}
}
const l = gen()
console.log(l.next())//{value: 1, done: false}
console.log(l.next())//{value: 1, done: false}
console.log(l.next())//{value: 1, done: false}
l.throw(new Error('ss'))
// 拋出錯誤,執行 catch
console.log(l.next()) //{value: 1, done: false}
四、Generator 異步方案
Generator 可用於解決異步編程嵌套層級較深的問題。
雖然使用 Promise
沒有了大量的嵌套代碼,但是依然有大量的回調函數,可讀性依然不好。
// Promise chain
ajax('/api/url1')
.then(value => {
return ajax('ajax/url2')
})
.then(value => {
return ajax('ajax/url3')
})
.then(value => {
return ajax('ajax/url4')
})
.catch(error => {
console.error(error)
})
要解決上面的問題,就要用到 Generator 生成器函數:
// 定義一個 generator 函數,ajax 返回一個 promise 對象
function* main() {
try {
const users = yield ajax('/api/users.json')
console.log(users)
const posts = yield ajax('/api/posts.json')
console.log(posts)
const urls = yield ajax('/api/urls.json')
console.log(urls)
} catch (e) {
// 捕獲異常
console.log(e)
}
}
const g = main()
// 定義一個遞歸函數
function handlerResult(result) {
if (result.done) return // 如果爲 true,退出遞歸調用
// result.value 返回是一個 promise 對象,使用 then 可以執行其結果
result.value.then(data => {
// g.next(data) 可以作爲 yield 返回值,再進入下一次遞歸
handlerResult(g.next(data))
// 異常邏輯
}, error => {
g.throw(error)
})
}
handleResult(g.next())
五、Generator 應用實例
1、實現抽獎
① ES5 做法
function draw(first = 1, second = 3, third = 5) {
// 三個獎的候選人,一個結果,一個隨機數
let firstPrize = ['1A', '1B', '1C', '1D', '1E']
let secondPrize = ['2A', '2B', '2C', '2D', '2E', '2F', '2G', '2H', '2I', '2J', '2K', '2L']
let thirdPrize = ['3A', '3B', '3C', '3D', '3E', '3F', '3G', '3H', '3I', '3J', '3K', '3L', '3M', '3N', '3O', '3P', '3Q', '3R', '3S', '3T', '3U', '3V', '3W', '3X', '3Y', '3Z']
let result = []
let random
// 抽一等獎
for (let i = 0; i < first; i++) {
random = Math.floor(Math.random() * firstPrize.length)
result = result.concat(firstPrize.splice(random, 1))
}
// 抽二等獎
for (let i = 0; i < second; i++) {
random = Math.floor(Math.random() * secondPrize.length)
result = result.concat(secondPrize.splice(random, 1))
}
// 抽三等獎
for (let i = 0; i < third; i++) {
random = Math.floor(Math.random() * thirdPrize.length)
result = result.concat(thirdPrize.splice(random, 1))
}
return result
}
console.log(draw())
// ["1A", "2D", "2K", "2A", "3A", "3G", "3Y", "3W", "3P"]
② ES6 做法
function* draw(first = 1, second = 3, third = 5) {
// 三個獎的候選人,一個結果,一個隨機數
let firstPrize = ['1A', '1B', '1C', '1D', '1E']
let secondPrize = ['2A', '2B', '2C', '2D', '2E', '2F', '2G', '2H', '2I', '2J', '2K', '2L']
let thirdPrize = ['3A', '3B', '3C', '3D', '3E', '3F', '3G', '3H', '3I', '3J', '3K', '3L', '3M', '3N', '3O', '3P', '3Q', '3R', '3S', '3T', '3U', '3V', '3W', '3X', '3Y', '3Z']
let count = 0
let random
while (1) {
if (count < first) {
random = Math.floor(Math.random() * firstPrize.length)
yield firstPrize[random]
count++
firstPrize.splice(random, 1)
} else if (count < first + second) {
random = Math.floor(Math.random() * secondPrize.length)
yield secondPrize[random]
count++
secondPrize.splice(random, 1)
} else if (count < first + second + third) {
random = Math.floor(Math.random() * thirdPrize.length)
yield thirdPrize[random]
count++
thirdPrize.splice(random, 1)
} else {
return false
}
}
}
let d = draw()
console.log(d.next().value) // 1C
console.log(d.next().value) // 2E
console.log(d.next().value) // 2H
console.log(d.next().value) // 2C
console.log(d.next().value) // 3H
console.log(d.next().value) // 3V
console.log(d.next().value) // 3A
console.log(d.next().value) // 3J
console.log(d.next().value) // 3N
console.log(d.next().value) // false
console.log(d.next().value) // undefined
console.log(d.next().value) // undefined
2、實現數 3 的倍數小遊戲
如果是 ES5,會無限死循環,導致程序崩潰
ES6 做法:
function* count(x = 1) {
while (1) {
if (x % 3 === 0) {
yield x
}
x++
}
}
let num = count()
console.log(num.next().value) // 3
console.log(num.next().value) // 6
console.log(num.next().value) // 9
console.log(num.next().value) // 12
console.log(num.next().value) // 15
console.log(num.next().value) // 18
...
3、實現 Iterator 方法
const todos = {
life: ['喫飯', '睡覺', '打豆豆'],
learn: ['語文', '數學', '外語'],
work: ['喝茶'],
each: function (callback) {
const all = [].concat(this.life, this.learn, this.work)
for (const item of all) {
callback(item)
}
},
[Symbol.iterator]: function* () {
const all = [...this.life, ...this.learn, ...this.work]
for (const item of all) {
yield item
}
}
}
for (const item of todos) {
console.log(item)
}
// 喫飯
// 睡覺
// 打豆豆
// 語文
// 數學
// 外語
// 喝茶