迭代函数-Generator函数的理解
一、基本用法
1.什么是迭代器?
迭代器是被设计专用于迭代的对象,带有特定接口。所有的迭代器对象都拥有 next() 方 法,会返回一个结果对象。该结果对象有两个属性:对应下一个值的 value ,以及一个布尔 类型的 done ,其值为 true 时表示没有更多值可供使用。迭代器持有一个指向集合位置的 内部指针,每当调用了 next() 方法,迭代器就会返回相应的下一个值。 若你在最后一个值返回后再调用 next() ,所返回的 done 属性值会是 true ,并且 value 属性值会是迭代器自身的返回值( return value ,即使用 return 语句明确返回的 值)。该“返回值”不是原数据集的一部分,却会成为相关数据的最后一个片段,或在迭代器未 提供返回值的时候使用 undefined 。迭代器自身的返回值类似于函数的返回值,是向调用者 返回信息的最后手段。
2.什么是生成器?
生成器( generator )是能返回一个迭代器的函数。生成器函数由放在 function 关键字之后的一个星号( * )来表示,并能使用新的 yield 关键字。
generator函数
跟普通函数在写法上的区别就是,多了一个星号*
,并且只有在generator函数
中才能使用yield
什么是yield
呢,他相当于generator函数
执行的中途暂停点
,比如下方有3个暂停点。而怎么才能暂停后继续走呢?那就得使用到next方法
,next方法
执行后会返回一个对象,对象中有value 和 done
两个属性
- value:暂停点后面接的值,也就是yield后面接的值
- done:是否generator函数已走完,没走完为false,走完为true
function* gen() { yield 1 yield 2 yield 3 } const g = gen() console.log(g.next()) // { value: 1, done: false } console.log(g.next()) // { value: 2, done: false } console.log(g.next()) // { value: 3, done: false } console.log(g.next()) // { value: undefined, done: true }
可以看到最后一个是undefined,这取决于你generator函数有无返回值
function* gen() { yield 1 yield 2 yield 3 return 4 } const g = gen() console.log(g.next()) // { value: 1, done: false } console.log(g.next()) // { value: 2, done: false } console.log(g.next()) // { value: 3, done: false } console.log(g.next()) // { value: 4, done: true }
二、yield后面接函数
yield后面接函数的话,到了对应暂停点yield,会马上执行此函数,并且该函数的执行返回值,会被当做此暂停点对象的value
function fn(num) { console.log(num) return num } function* gen() { yield fn(1) yield fn(2) return 3 } const g = gen() console.log(g.next()) // 1 // { value: 1, done: false } console.log(g.next()) // 2 // { value: 2, done: false } console.log(g.next()) // { value: 3, done: true }
三、yield后面接peomise
前面说了,函数执行返回值会当做暂停点对象的value值,那么下面例子就可以理解了,前两个的value都是pending状态的Promise对象
function fn(num) { return new Promise(resolve => { setTimeout(() => { resolve(num) }, 1000) }) } function* gen() { yield fn(1) yield fn(2) return 3 } const g = gen() console.log(g.next()) // { value: Promise { <pending> }, done: false } console.log(g.next()) // { value: Promise { <pending> }, done: false } console.log(g.next()) // { value: 3, done: true }
但是其实我们想要的结果是两个Promise的结果1 和 2
,那怎么做呢?直接使用Promise的then方法就行了
const g = gen() const next1 = g.next() next1.value.then(res1 => { console.log(next1) // 1秒后输出 { value: Promise { 1 }, done: false } console.log(res1) // 1秒后输出 1 const next2 = g.next() next2.value.then(res2 => { console.log(next2) // 2秒后输出 { value: Promise { 2 }, done: false } console.log(res2) // 2秒后输出 2 console.log(g.next()) // 2秒后输出 { value: 3, done: true } }) })
四、yield后边接 *表达式
如果在 Generator 函数中需要调用另一个 Generator 函数或者其他可迭代对象,就需要用到yield*
表达式,yield*
表达式可以产生迭代对象返回的的每个值,yield*
本身的返回值是当迭代器结束时返回的值。
function* gen1() { yield "a"; yield "b"; return "C"; } function* gen2() { yield 1; const result = yield* gen1(); console.info("[result]", result); yield* "xy"; yield 2; } for (let v of gen2()) { console.log(v); } // 依次输出: // 1 // a // b // [result] C // x // y // 2
如上,gen2
中包含了对gen1()
和字符串xy
,并对这两个数据使用了yield*
命令,实际上相当于将他们依次迭代,并将迭代结果再使用yield
,故对gen2()
对象遍历,也得到了gen1()
和字符串的迭代结果。另外,最后一次迭代的结果,会被作为yield*
的返回值,如上的[result] C
,如果将gen1
中的return "C"
替换成yield "C"
,那么resuilt
就是 undefined
。
与迭代器紧密相关的是,可迭代对象( iterable )是包含 Symbol.iterator 属性的对象。这 个 Symbol.iterator 知名符号定义了为指定对象返回迭代器的函数。在 ES6 中,所有的集合对象(数组、Set Map )以及字符串都是可迭代对象,因此它们都被指定了默认的迭代器。可迭代对象被设计用于与 ES 新增的 for-of 循环配合使用。
生成器创建的所有迭代器都是可迭代对象,因为生成器默认就会为 Symbol.iterator 属性赋值
yield一些说明:
- yield并不能直接生产值,而是产生一个等待输出的函数
- 除IE外,其他所有浏览器均可兼容(包括win10 的Edge)
- 某个函数包含了yield,意味着这个函数已经是一个Generator
- 如果yield在其他表达式中,需要用()单独括起来、不然会报错
- yield表达式本身没有返回值,或者说总是返回undefined(由next返回)
- next()可无限调用,但既定循环完成之后总是返回undeinded
五、next函数传参
generator函数可以用next方法
来传参,并且可以通过yield
来接收这个参数,注意两点:
第一次传参参数无效,只有从第二次开始next传参才有用
next传值时,要记住顺序是,先右边yield,后左边接收参数
function* gen() { const num1 = yield 1 console.log(num1) const num2 = yield 2 console.log(num2) return 3 } const g = gen() console.log(g.next()) // { value: 1, done: false } console.log(g.next(11111)) // 11111 // { value: 2, done: false } console.log(g.next(22222)) // 22222 // { value: 3, done: true }
第一句next()之前没有执行yield,所以即使带参数也不会赋值,next参数只赋值给已经执行完毕的yield语句左边的变量
看下边这个例子:
function *foo(x) { var y = x * (yield); return y; } var it = foo( 6 ); // 启动foo(..) it.next(); var res = it.next( 7 ); res.value; // 42
为什么回事这个结果?
首先,传入 6 作为参数 x。然后调用 it.next(),这会启动 *foo(..)。在 *foo(..) 内部,开始执行语句 var y = x ..,但随后就遇到了一个 yield 表达式。它就会在这一点上暂停 *foo(..)(在赋值语句中间!),并在本质上要求调用代码为 yield表达式提供一个结果值。接下来,调用 it.next( 7 ),这一句把值 7 传回作为被暂停的yield 表达式的结果。所以,这时赋值语句实际上就是 var y = 6 * 7。现在,return y 返回值 42 作为调用it.next( 7 ) 的结果。
因为第一个 next(..) 总是启动一个生成器,并运行到第一个 yield 处。不过,是第二个next(..) 调用完成第一个被暂停的 yield 表达式,第三个 next(..) 调用完成第二个 yield,以此类推。
function *foo(x) { var y = x * (yield "Hello"); // <-- yield一个值! return y; } var it = foo( 6 ); var res = it.next(); // 第一个next(),并不传入任何东西 res.value; // "Hello" res = it.next( 7 ); // 向等待的yield传入7 res.value; // 42
我们并没有向第一个 next() 调用发送值。只有暂停的 yield才能接受这样一个通过 next(..) 传递的值,而在生成器的起始处我们调用第一个 next() 时,还没有暂停的 yield 来接受这样一个值。规范和所有兼容浏览器都会默默丢弃传递给第一个 next() 的任何东西。传值过去仍然不是一个好思路,因为你创建了沉默的无效代码,这会让人迷惑。因此,启动生成器时一定要用不带参数的 next()。
第一个 next() 调用(没有参数的)基本上就是在提出一个问题:“生成器 *foo(..) 要给我的下一个值是什么”。谁来回答这个问题呢?第一个 yield "hello" 表达式。
与 yield 语句的数量相比,还是多出了一个额外的 next()。所以,最后一个it.next(7) 调用再次提出了这样的问题:生成器将要产生的下一个值是什么。但是,再没有 yield 语句来回答这个问题了,是不是?那么谁来回答呢?
六、promise+next传参
看下这两个组合起来会是什么样?
function fn(nums) { return new Promise(resolve => { setTimeout(() => { resolve(nums * 2) }, 1000) }) } function* gen() { const num1 = yield fn(1) const num2 = yield fn(num1) const num3 = yield fn(num2) return num3 } const g = gen() const next1 = g.next() next1.value.then(res1 => { console.log(next1) // 1秒后同时输出 { value: Promise { 2 }, done: false } console.log(res1) // 1秒后同时输出 2 const next2 = g.next(res1) // 传入上次的res1 next2.value.then(res2 => { console.log(next2) // 2秒后同时输出 { value: Promise { 4 }, done: false } console.log(res2) // 2秒后同时输出 4 const next3 = g.next(res2) // 传入上次的res2 next3.value.then(res3 => { console.log(next3) // 3秒后同时输出 { value: Promise { 8 }, done: false } console.log(res3) // 3秒后同时输出 8 // 传入上次的res3 console.log(g.next(res3)) // 3秒后同时输出 { value: 8, done: true } }) }) })
七、用generator函数实现async/await
其实上方的generator函数
的Promise+next传参
,就很像async/await
了,区别在于
- gen函数执行返回值不是Promise,asyncFn执行返回值是Promise
- gen函数需要执行相应的操作,才能等同于asyncFn的排队效果
- gen函数执行的操作是不完善的,因为并不确定有几个yield,不确定会嵌套几次
之前我们说到,async函数的执行返回值是一个Promise,那我们要怎么实现相同的结果呢
function* gen() { } function generatorToAsync (generatorFn) { return function () { return new Promise((resolve, reject) => { }) } } const asyncFn = generatorToAsync(gen) console.log(asyncFn()) // Promise
把之前的处理代码,加入generatorToAsync函数
中
function fn(nums) { return new Promise(resolve => { setTimeout(() => { resolve(nums * 2) }, 1000) }) } function* gen() { const num1 = yield fn(1) const num2 = yield fn(num1) const num3 = yield fn(num2) return num3 } function generatorToAsync(generatorFn) { return function () { return new Promise((resolve, reject) => { const g = generatorFn() const next1 = g.next() next1.value.then(res1 => { const next2 = g.next(res1) // 传入上次的res1 next2.value.then(res2 => { const next3 = g.next(res2) // 传入上次的res2 next3.value.then(res3 => { // 传入上次的res3 resolve(g.next(res3).value) }) }) }) }) } } const asyncFn = generatorToAsync(gen) asyncFn().then(res => console.log(res)) // 3秒后输出 8
可以发现,其实已经实现了以下的async/await
的结果了
async function asyncFn() { const num1 = await fn(1) const num2 = await fn(num1) const num3 = await fn(num2) return num3 } asyncFn().then(res => console.log(res)) // 3秒后输出 8
完善一下代码上面的代码其实都是死代码,因为一个async函数中可能有2个await,3个await,5个await ,其实await的个数是不确定的。同样类比,generator函数中,也可能有2个yield,3个yield,5个yield,所以把代码写成活的才行
function generatorToAsync(generatorFn) { return function() { const gen = generatorFn.apply(this, arguments) // gen有可能传参 // 返回一个Promise return new Promise((resolve, reject) => { function go(key, arg) { let res try { res = gen[key](arg) // 这里有可能会执行返回reject状态的Promise } catch (error) { return reject(error) // 报错的话会走catch,直接reject } // 解构获得value和done const { value, done } = res if (done) { // 如果done为true,说明走完了,进行resolve(value) return resolve(value) } else { // 如果done为false,说明没走完,还得继续走 // value有可能是:常量,Promise,Promise有可能是成功或者失败 return Promise.resolve(value).then(val => go('next', val), err => go('throw', err)) } } go("next") // 第一次执行 }) } } const asyncFn = generatorToAsync(gen) asyncFn().then(res => console.log(res))
这样的话,无论是多少个yield都会排队执行了
示例:
async/await
版本
async function asyncFn() { const num1 = await fn(1) console.log(num1) // 2 const num2 = await fn(num1) console.log(num2) // 4 const num3 = await fn(num2) console.log(num3) // 8 return num3 } const asyncRes = asyncFn() console.log(asyncRes) // Promise asyncRes.then(res => console.log(res)) // 8
使用generatorToAsync函数
的版本
function* gen() { const num1 = yield fn(1) console.log(num1) // 2 const num2 = yield fn(num1) console.log(num2) // 4 const num3 = yield fn(num2) console.log(num3) // 8 return num3 } const genToAsync = generatorToAsync(gen) const asyncRes = genToAsync() console.log(asyncRes) // Promise asyncRes.then(res => console.log(res)) // 8
因此可以看出 async/await
是一种语法糖