生成器Generator
1. 生成器函数和生成器对象
- 生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数的暂停和继续执行等
- 平时写的函数终止条件通常是return或者异常
- 生成器函数也是一个函数,只不过它和普通函数是有一些区别的
- 首先,生成器函数需要在function后面加一个*
- 其次,生成器函数可以通过yield关键字来控制函数的执行流程
- 最后,生成器函数的返回值是一个生成器对象
- 生成器是一种特殊的迭代器
2. 生成器的返回值和参数
-
调用生成器函数时,只会返回一个生成器对象,而函数内部的代码并不会执行
-
如果想要执行函数内部的代码,就需要调用生成器对象的next方法
-
我们已经知道了,迭代器的next方法是会有返回值的,而生成器对象的next方法,如果也想要返回值不为undefined的话,就需要使用yield来返回结果
function* foo() { console.log("aaa") console.log("bbb") yield "第一个yield" console.log("ccc") console.log("ddd") yield "第二个yield" console.log("ccc") console.log("ddd") yield "第三个yield" } // 调用foo函数时,内部的代码并不会执行,只会返回一个生成器 const generator = foo() // 如果想要执行foo内部的代码,就可以调用next方法 // next方法是有返回值的 // 如果想要next方法返回值的value不为undefined,就需要yield返回一个值 console.log(generator.next()) // {value: '第一个yield', done: false} console.log(generator.next()) // {value: '第二个yield', done: false} console.log(generator.next()) // {value: '第三个yield', done: false} console.log(generator.next()) // {value: undefined, done: true}
-
生成器函数是可以分段执行的,所以如果想要给每段都传递参数的话,就可以在调用next方法时,给它传入参数,这个参数会传到yield身上。
function* foo(result1) { console.log("aaa", result1) console.log("bbb", result1) const result2 = yield "第一个yield" console.log("ccc", result2) console.log("ddd", result2) const result3 = yield "第二个yield" console.log("ccc", result3) console.log("ddd", result3) const result4 = yield "第三个yield" } // 如果想要给result1传值,调用foo函数时,传入一个值即可 const generator = foo("first") // 如果想要每段代码都有不同的参数,在调用next方法时,传入参数即可 // 第一次调用next时,没有yield进行接收,所以这个值无法通过next方法传进去 generator.next("second") // 第二次调用时,传入的值就是第一个yield的,所以只有第一个yield之后的代码能够使用这个值 generator.next("third") generator.next("faith") generator.next() // 输出结果:没有second aaa first bbb first ccc third ddd third ccc faith ddd faith
3. 生成器提前结束
-
要让生成器中提前返回{ done: true }, 就可以调用return方法
-
return传入的值,会作为return方法的返回值的value出现
-
return之后的next不会继续生成值
function* foo(result1) { console.log("aaa") console.log("bbb") yield "第一个yield" console.log("ccc") console.log("ddd") yield "第二个yield" console.log("ccc") console.log("ddd") yield "第三个yield" } const generator = foo("first") console.log(generator.next()) console.log(generator.return("结束啦")) console.log(generator.next()) // 运行结果 aaa bbb {value: '第一个yield', done: false} {value: '结束啦', done: true} {value: undefined, done: true}
-
-
调用throw方法,抛出一个异常,也可以使后面的next获取不到值
function* foo(result1) { console.log("aaa") console.log("bbb") yield "第一个yield" console.log("ccc") console.log("ddd") yield "第二个yield" console.log("ccc") console.log("ddd") yield "第三个yield" } const generator = foo("first") console.log(generator.next()) console.log(generator.throw(new Error("coderWhy来咯"))) console.log(generator.next())
4. generator代替iterator
-
生成器既然是一种特殊的迭代器,那么就意味着在某些情况下,生成器可以替代迭代器
-
生成器函数跟[Symbol.iterator]方法一样,会返回一个迭代器
-
它们的迭代器的内部都有一个next方法,这个方法都可以返回{ done: false/true, value: value/undefined }
-
而生成器函数的yield返回的值,就是next方法返回值中的value的值,所以就可以通过for循环,利用yield返回数组中的每个元素
const arrs = ["abc", "cba", "nba"] const friends = ["kobe", "james", "linda", "Judy"] function* createIterator(arr) { for (let i = 0; i < arr.length; i++) { yield arr[i] } } const generator1 = createIterator(arrs) console.log(generator1.next()) console.log(generator1.next()) console.log(generator1.next()) console.log(generator1.next()) const generator2 = createIterator(friends) console.log(generator2.next()) console.log(generator2.next()) console.log(generator2.next()) console.log(generator2.next()) console.log(generator2.next())
-
-
注意:yield还有一种语法糖,就是
yield*
,它可以依次迭代自己后面的可迭代对象,每次迭代一个值-
所以更简便的写法如下
const arrs = ["abc", "cba", "nba"] const friends = ["kobe", "james", "linda", "Judy"] function* createIterator(arr) { // 语法糖 // !!!!注意,yield后面必须只能是可迭代对象 yield* arr } const generator1 = createIterator(arrs) console.log(generator1.next()) console.log(generator1.next()) console.log(generator1.next()) console.log(generator1.next())
-
因此迭代器中的自定义类迭代案例就可以写成下面这样
-
由于class定义的类中,实例方法并不能使用function声明
-
所以要想使[Symbol.iterator]成为一个生成器,可以在它的前面加一个*(
*[Symbol.iterator]
)class Classroom { constructor(name, address, ...students) { this.name = name this.address = address this.students = students } newStudent(student) { this.students.push(student) } // 在类中写好实例方法,那么这个类所new出来的实例对象,就都是可迭代的了 // 前面加一个*,就可以使其称为迭代器 *[Symbol.iterator]() { yield* this.students } } const Classroom1 = new Classroom("2201", "2号楼", "kobe", "james") Classroom1.newStudent("coderWhy") Classroom1.newStudent("linda") for(let student of Classroom1) { console.log(student) }
-
-
5. 异步代码的处理方案
-
在学习Promise时,知道了Promise是用来处理异步代码的
-
但是如果要实现下面这个案例,单单使用Promise解决的话,代码的可读性就会非常差,这个时候就需要结合生成器了
-
案例需求:
- 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求
- 第二次请求的url依赖于第一次请求的结果
- 第三次请求的url依赖于第二次请求的结果
- 依此类推…
针对这个案例,由可读性差到可读性好,有四种方式
function requestData(url) { // 模拟网络请求 return new Promise((resolve, reject) => { setTimeout(() => { // 在请求到数据之后,将数据返回回来 resolve(url) }, 2000) }) } // 方式一: 层层嵌套(回调地狱) function getData() { // 第一次调用 requestData("Judy").then(res1 => { console.log("res1:", res1) // 第二次调用 requestData(res1 + "coderWhy").then(res2 => { console.log("res2:", res2) // 第三次调用 requestData(res2 + "kobe").then(res3 => { console.log("res3", res3) }) }) }) } getData() // 方式二:利用Promise的特性链式调用 function getData() { requestData("Judy").then(res1 => { console.log("res1:", res1) // 利用resolve不同值的特性做: // 1. return返回的值是then方法返回的Promise中的resolve的参数 // 2. 而resolve的参数如果是一个新的Promise的话,那么then方法返回的这个Promise的状态就由新Promise决定 // 3. 所以就相当于还是在用then方法监听requestData返回的Promise return requestData(res1 + "coderWhy") }).then(res2 => { console.log("res2:", res2) return requestData(res2 + "kobe") }).then(res3 => { console.log("res3:", res3) }) } getData() // 方式三:利用生成器调用 function* getData() { const result1 = yield requestData("Judy") console.log(result1) const result2 = yield requestData(result1 + "coderWhy") console.log(result2) const result3 = yield requestData(result2 + "kobe") console.log(result3) } const generator = getData() // result -> { done: false/true, value: promise对象/undefined } // 注意: // 1.需要知道,yield后面的内容的执行结果,将会被添加到next方法返回的对象中的value属性中 // 2. next方法中传入的值,会被添加到yield中,所以将yield赋值给result1,就相当于把next方法中传入的值,赋值给了result1 // 代码执行顺序: // 1. 第一次调用next方法,只执行第一个yield后面的参数为Judy的requestData函数 // 2. 第一个yield的返回值是一个对象,而其中的value就是promise对象,所以可以对它进行监听 // 3. 当拿到监听到的res1时,再将res1传入第二个next方法中 // 4. 此时,相当于调用了第二次next方法,将从第一个yield赋值给result1执行到第二个yield后面的requestData()为止 // 5. 第二个yield的返回值是一个对象,而其中的value就是promise对象..... generator.next().value.then(res1 => { generator.next(res1).value.then(res2 => { generator.next(res2).value.then(res3 => { generator.next(res3) }) }) }) // 方式四:使用async和await async function getData() { // result -> { done: false/true, value: promise对象/undefined } const result1 = await requestData("Judy") console.log(result1) const result2 = await requestData(result1 + "coderWhy") console.log(result2) const result3 = await requestData(result2 + "kobe") console.log(result3) } getData()
对上面的方式三(generator)进行优化
-
由于不知道到底需要调用几次promise,所以那样写终究是不行的
-
并且如果还有其他需要这样执行的函数,怎么办呢?
- 首先想到封装一个函数,由别人把需要使用生成器对象层层调用的生成器函数作为参数传进来
- 其次这种看起来就有规律的代码一般都是可以使用循环或者递归做的
function requestData(url) { // 模拟网络请求 return new Promise((resolve, reject) => { setTimeout(() => { // 在请求到数据之后,将数据返回回来 resolve(url) }, 2000) }) } function* getData() { const res1 = yield requestData("Judy") console.log(res1) const res2 = yield requestData(res1 + "coderWhy") console.log(res2) const res3 = yield requestData(res2 + "kobe") console.log(res3) const res4 = yield requestData(res3 + "james") console.log(res4) } // 如果看不懂,就照着方式三的生成器对象调用next的步骤,自己想 function execGenerator(generatorFn) { // 得到生成器对象 const generator = generatorFn() function exec(res) { // 拿到yield返回的对象{ done: false/true, value: promise对象/undefined } let result = generator.next(res) // 判断如果done为true,就证明生成器函数中的代码已经执行完了,所以直接返回即可 if (result.done) return // 否则就是生成器函数中还有代码需要继续发送网络请求,继续递归调用即可 result.value.then(res => { exec(res) }) } // 定义完exec函数之后,对其调用 exec() } execGenerator(getData)