二、生成器 Generator
(一)什么是生成器
在 JavaScript 中,生成器是 ES6 引入的一种新的数据结构,可以定义一个特殊的函数,这个函数可以在执行过程中被暂停和恢复。生成器函数使用 function*
语法来定义,包含 yield
表达式,用于在函数的执行过程中产生(yield)值,而不是返回单个值。调用生成器函数时,它不会立即执行,而是返回一个生成器对象(迭代器对象)。这个生成器对象有一个 next()
方法,调用该方法可以使得生成器函数开始执行,直到遇到 yield
表达式为止。当 yield
表达式被执行时,生成器函数会暂停执行,并返回 yield
后面的值作为结果。之后可以再次调用生成器对象上的 next()
方法来恢复生成器函数的执行。
生成器有以下特点:
- 1、可暂停和恢复:生成器函数在执行过程中可以被
yield
表达式暂停,被next()
方法恢复执行,允许自由控制函数的执行流程。 - 2、产生多个值:通过
yield
表达式,生成器函数可以产生多个值,每次调用next()
方法都会从上次暂停的位置开始执行,直到遇到下一个yield
。 - 3、迭代:调用生成器函数返回的是一个生成器对象,也是一个迭代器对象,每次迭代都会获取生成器函数产生的下一个值。
(二)生成器声明和生成器对象
1、生成器声明
- 函数式声明
// 通过function* 语法来声明了一个生成器函数
function* generatorFun() {
}
- 函数表单式
const generatorFun = function* () {
}
- 生成器函数作为对象字面量方法
let obj1 = {
generatorFun: function* () {
}
}
// 可简化为
let obj2 = {
* generatorFun() {
}
}
- 箭头函数不能定义生成器函数
箭头函数不能用来定义生成器函数,将
function*
和=>
结合使用,是不被允许的。
// 箭头函数中不能使用 yield 表达式
const generatorFun1 = () => {
yield 'hello' // 会报错
yield 'word'
}
// 箭头函数不能结合 * 来使用, function* 才是正确的语法
const generatorFun2 = () =>* { // Unexpected token '*'
yield 'hello'
yield 'word'
}
2、生成器对象
在 JavaScript 中,生成器对象是通过调用生成器函数获得的。当调用一个生成器函数时,它并不会立即执行,而是返回一个特殊的对象,即是生成器对象。这个对象与迭代器相似,实现了 Iterator 接口,因此具有 next()
方法,通过生成器对象上的属性和方法允许控制生成器函数的执行流程,并获取它产生的值。
-
1、生成器对象主要方法:
next()
:这个方法用于恢复生成器函数的执行,并返回一个对象。返回的对象具有value
和done
两个属性。value
包含生成器函数当前产生的值,done
属性是一个布尔值,表示生成器函数是否已经执行完毕。
function* generatorFun() { yield 1 yield 2 yield 3 } const gen = generatorFun() console.log(gen.next()) // {value: 1, done: false} console.log(gen.next()) // {value: 2, done: false} console.log(gen.next()) // {value: 3, done: false} console.log(gen.next()) // {value: undefined, done: false}
return()
:这个方法用于提前终止生成器函数的执行,并返回一个给定的值。在调用return()
方法后,会返回一个对象,其value
属性为return()
方法中指定的值,其done
属性为true
。 任何后续的next()
调用都会返回一个表示生成器已完成的对象,其value
属性为undefined
,其done
属性为true
。
function* generatorFun() { yield 1 yield 2 yield 3 } const gen = generatorFun() console.log(gen.next()) // {value: 1, done: false} console.log(gen.return('finish')) // {value: 'finish', done: true} console.log(gen.next()) // {value: undefined, done: true} console.log(gen.next()) // {value: undefined, done: true}
throw()
:这个方法用于向生成器函数内部抛出一个错误。如果生成器函数内部有try...catch
语句,它可以捕获这个错误并处理。否则,错误会被抛出到生成器对象的外部。
function* generatorFun() { try { yield 1 yield 2 throw new Error('new error') } catch (e) { console.log('Caught error inside generator:', e.message) yield 'Error handled' yield 'Error handled2' } } const gen = generatorFun() console.log(gen.next()) // {value: 1, done: false} console.log(gen.throw(new Error('External error thrown to generator'))) // Caught error inside generator: External error thrown to generator // 调用 throw() 方法,try...catch 捕获到错误,走到 catch 流程,打印出 'Caught error inside generator:', e.message // {value: 'Error handled', done: false} // 同时返回 yield 后的值 console.log(gen.next()) // {value: 'Error handled2', done: false} // 会继续走到 catch 代码块继续执行,不会再走到 try 代码块里面了 console.log(gen.next()) // {value: undefined, done: true}
-
2、生成器对象实现了 Iterator 接口,它们默认的迭代器是自引用的
- 生成器对象,可在
for...of
循环等其他任何期望一个可迭代对象的地方使用
function* GeneratorNumber() { yield 1 yield 2 yield 3 } // 创建一个生成器对象 const genNumber = GeneratorNumber() // 生成器对象实现了 Iterator 接口,因此可直接在 for...of 循环中使用 for(const num of genNumber) { console.log(num) } // 输出 // 1 // 2 // 3
- 生成器对象默认返回它们自己迭代器,意味着当调用生成器对象的
Symbol.iterator()
方法时,会返回生成器对象本身。
function* GeneratorNumber() { yield 1 yield 2 yield 3 } // 创建一个生成器对象 const genNumber = GeneratorNumber() // 获取生成器对象的迭代器 const iteratorNum = genNumber[Symbol.iterator]() // 验证是否相等 console.log(genNumber === iteratorNum) // true // 这是因为生成器函数在每次调用 next() 方法时都会记住上一次离开的位置(即 yield 表达式的位置),并在下一次调用时从那里恢复执行。这种特性使得生成器对象本身就能作为自己的迭代器
- 生成器对象,可在
-
3、生成器对象的状态是单向前进的
一旦生成器对象遍历完成,即生成器对象调用next()
方法返回的done
属性为true
,再次调用next()
方法将不会产生新值。如果再次遍历已经完成的生成器对象,将不会得到任何结果。
function* GeneratorNumber() {
yield 1
yield 2
yield 3
}
// 创建一个生成器对象
const genNumber = GeneratorNumber()
// 生成器对象实现了 Iterator 接口,因此可直接在 for...of 循环中使用
for(const num of genNumber) {
console.log(num)
}
// 输出
// 1
// 2
// 3
for(const num of genNumber) {
console.log(num)
}
// 不会有输出了
(三)生成器函数中的 yield 表达式
yield
表达式只能用于生成器函数内部,用于其他地方会出现语法错误。当生成器函数执行到一个 yield
表示式时,它会停止执行,并返回 yield
表达式的值给调用者。同时,生成器函数的执行上下文(包括局部变量、函数执行状态等)会被保存起来,以便下次调用的时候能够恢复。
- 1、作为生成器函数的一个中间返回语句
function* simpleGenerator() {
console.log('Start')
yield 'First yield'
console.log('Between yields')
yield 'Second yield'
console.log('End')
}
const gen = simpleGenerator()
console.log(gen.next().value)
console.log(1)
console.log(gen.next().value)
console.log(2)
console.log(gen.next().value)
// 输出:
// Start
// First yield
// 1
// Between yields
// Second yield
// 2
// End
// undefined //生成器结束
- 2、
yield
可接收调用生成器对象next(value)
方法向生成器函数发送的值
通过next('aaa')
方法的参数向生成器函数发送值,在生成器函数暂停的 yield 表示式地方接收next('aaa')
方法中传递的参数 aaa,该值可以通过let variavle = yield 1
传递给变量let variavle = 'aaa'
以上代码执行流程:function* generatorFun() { console.log('Start') const input1 = yield 'First yield' console.log(`You entered: ${input1}`) const input2 = yield 'Sencond yield' console.log(`You entered other: ${input2}`) } const gen = generatorFun() console.log(gen.next('1')) // Start // {value: 'First yield', done: false} console.log(1) // 1 console.log(gen.next('2')) // You entered: 2 // {value: 'Sencond yield', done: false} console.log(2) // 2 console.log(gen.next('3')) // You entered other: 3 // {value: undefined, done: true} console.log(3) // 3
- 1、第一步,执行
console.log(gen.next('1'))
, 生成器对象 gen 第一次调用next('1')
方法,生成器函数内部开始执行,遇到console.log('Start')
则打印输出Start
,然后在赋值表达式const input1 = yield 'First yield'
中检测到yield
关键字,则生成器函数在yield
处暂停执行,此时返回yield
表达式右侧的值,即是'First yield'
,将该值作为第一次调用next('1')
方法返回的对象的value
属性的值。console.log(gen.next('1'))
打印出生成器对象 gen 第一次调用next('1')
返回的对象{value: 'First yield', done: false}
; - 2、第二步,执行
console.log(1)
,打印出1
; - 3、第三步,执行
console.log(gen.next('2'))
,生成器对象 gen 第二次调用next('2')
方法,生成器函数内部从上次暂停的 第一个yield
开始执行,即在表达式const input1 = yield 'First yield'
中的yield
处,第二次调用的next('2')
传递了参数2
,作为第一个 yield 处的传入值,相当于把const input1 = yield 'First yield'
替换为const input1 = 2
了,继续执行到console.log(`You entered: ${input1}`)
,则打印出You entered: 2
,然后在赋值表达式中const input2 = yield 'Sencond yield'
检测到 第二个yield
关键字,则生成器函数在第二个yield
处暂停执行,此时返回第二个yield
表达式右侧的值,即是'Sencond yield'
,将该值作为第二次调用next('2')
方法返回的对象value
属性的值。console.log(gen.next('2'))
打印出生成器对象 gen 第二次调用next('2')
返回的对象{value: 'Sencond yield', done: false}
- 第四步,执行
console.log(2)
,打印输出2
- 第五步,执行
console.log(gen.next('3'))
,生成器对象 gen 第三次调用next('3')
方法,生成器函数内部从上次暂停的 第二个yield
开始执行,即在表达式const input2 = yield 'Sencond yield'
中的yield
处,第三次调用的next('3')
传递了参数3
,作为第二个 yield 处的传入值,相当于把const input2 = yield 'Sencond yield'
替换为const input2 = 3
了,继续执行到console.log(`You entered other: ${input2}`)
,则打印输出You entered other: 3
,没有可执行的代码后,返回undefined
,作为第三次调用next('3')
方法返回的对象value
属性的值。console.log(gen.next('3'))
打印出生成器对象 gen 第三次调用next('3')
返回的对象{value: undefined, done: true}
- 第六步,执行
console.log(3)
,打印出3
- 1、第一步,执行
(四)生成器使用场景
1、异步编程
通过生成器结合 Promise 可以实现更简洁、更易于理解的异步编程模式。使用生成器,可以编写同步风格的代码来处理异步操作,即是“异步生成器”。
生成器函数可以暂停和恢复执行。当生成器函数中遇到 yield
关键字时,会暂停执行并返回 yield
后面的值(通常是一个 Promise)。然后可以等待这个 Promise 解决(resolve) 或者拒绝(reject),并通过再次调用生成器的 next()
方法来恢复执行。
function fetchData(url) {
return new Promise((resolve, reject) => {
// 模拟异步数据获取,比如接口请求
setTimeout(() => {
resolve(`Data from ${url}`)
}, 1000)
})
}
// 创建一个生成器函数
function* asyncGenerator() {
const urls = ['http://example.com/1', 'http://example.com/2']
for(const url of urls) {
try {
const data = yield fetchData(url)
console.log(data)
} catch(error) {
console.error('Error fetching data:', error)
}
}
}
// 使用生成器进行异步编程
(function() {
// 生成一个生成器对象
const generator = asyncGenerator()
// 一个递归函数来处理异步操作的结果,一直调用 next() 方法,直到生成器完成
function handleResult(result) {
if (result.done) {
return
}
const promise = result.value
promise.then(data => {
const nextResult = generator.next(data)
handleResult(nextResult)
}, error => {
generator.throw(error)
})
}
// 递归调用生成器对象
handleResult(generator.next())
})()
// 输出(间隔1s依次输出)
// Data from http://example.com/1
// Data from http://example.com/2
// 使用 aysnc/await 和 for...of 循环处理异步生成器
(async function() {
const generator = asyncGenerator()
try {
let result
let runData
do {
result = await generator.next(runData)
runData = await result.value
} while(!result.done)
}catch (error) {
generator.throw(error)
}
})()
// 输出(间隔1s依次输出)
// Data from http://example.com/1
// Data from http://example.com/2
2、无限数据流
在构建一个无限滚动的列表时,可以使用生成器协程来按需加载更多数据
// 定义生成器函数,一般yield后面放接口请求数据
function* infiniteListItems() {
let index = 0
while( true ) {
yield {id: index, content: 'Item ' + index}
index++
}
}
const itemsGenerator = infiniteListItems()
let itemsLoad = 0
function loadMoreItems() {
const nextItem = itemsGenerator.next()
if (nextItem.done) {
return
}
const itemData = nextItem.value
itemsLoad++
}
// 初始加载和滚动触底的时候加载
loadMoreItems()
3、状态管理(状态机)
生成器函数在 JavaScript 中可以作为一种简单的状态机的实现方式。通过 yield 关键字,生成器函数可以在多个状态之间切换,并在每次调用 next() 方法的时候根据上一次的状态和传入参数来决定下一个状态和执行的操作
function* userInteractionStateMachine () {
let state = 'idle'
while(true) {
if (state === 'idle') {
console.log('等待用户输入...')
// 等待 next() 方法的参数作为输入
const input = yield
if (input === 'start') {
state = 'active'
console.log('用户开始交互')
} else {
console.log('无效的输入,请重新输入')
}
} else if (state === 'active') {
console.log('用户正在交互...')
const action = yield
if (action === 'finish') {
state = 'idle'
console.log('用户结束交互')
} else if (action === 'continue') {
console.log('用户继续交互...')
} else {
console.log('未知的操作指令,请重新输入')
}
}
}
}
// 创建状态机的实例
const interaction = userInteractionStateMachine()
// 启动状态机(第一次传入状态机不需要传递参数)
interaction.next()
// 与状态机交互
interaction.next('start')
interaction.next('continue')
interaction.next('finish')
interaction.next('invalid')
// 输出
// 等待用户输入...
// 用户开始交互
// 用户正在交互...
// 用户继续交互...
// 用户正在交互...
// 用户结束交互
// 等待用户输入...
// 无效的输入,请重新输入
// 等待用户输入...
4、协程
协程(Coroutines)是一种用户态的轻量级线程,其执行可以在任何点被挂起(suspend)和恢复(resume)。与操作系统层面的线程不同,协程的调度完全由用户代码控制,而不是由操作系统内核来管理。这一特性使得协程在切换时开销非常小,几乎认为是无开销的。
在 JavaScript 中,由于它是单线程的,并且基于事件循环(Event Loop)来处理异步操作,协程的概念并不直接使用。可通过一些模式(如生成器函数配合 Promise 或 async/await)和第三方实现的协程库,可以在 JavaScript 中模拟协程的行为。
以上个人理解总结,如有错误,敬请指正
终于完结了~