主题
generator学习及使用
1. js解决异步的发展历程
js解决异步,最早靠的是回调,然后随着promise的出现,升级为promise,但是,promise还是不支持同步的写法,写起来还是得不停的.then
,写长了也很麻烦,代码看起来也很痛苦,后面出现了generator
,但是generator写起来还是很不友好,直到最终出现了async + await才让一步很方便
generator用的不是很多,但是它的原理需要掌握,以便更好的理解async + await
2. generator是什么?
生成器对象Generator是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。
generator
(生成器)是ES6
标准引入的新的数据类型。一个generator
看上去像一个函数,但可以返回多次。
generator
和函数不同的是,generator
由function*
定义(注意多出的*号),并且,除了return
语句,还可以用yield
返回多次。
js
function * fn(){
}
let it = fn()
注意: generator
函数的这个 *
可以挨着function
,或者挨着函数名,也可以放在中间谁都不挨;
生成器函数调用后,会返回一个iterator
,叫做迭代器;
调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的 迭代器 ( iterator )对象。当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield的位置为止,yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。
迭代器中有next
方法,可以通过iterator
调用next
,是固定写法it.next()
,调用后会有返回结果,例如:
js
function * fn(){
}
let it = fn()
let obj = it.next()
console.log(obj) // { value: undefined, done: true }
{ value: undefined, done: true }
中,value
为undefined
表示没有产出结果,done
为true
表示已经结束了;
next()方法返回一个对象,这个对象包含两个属性:value 和 done,value 属性表示本次 yield 表达式的返回值,done 属性为布尔类型,表示生成器后续是否还有 yield 语句,即生成器函数是否已经执行完毕并返回。
*
可以配合yeild
来使用,yield
含义是产出,且有中断功能更,相当于,函数执行过程中,碰到yield
就停止执行,例如:
js
function * fn(){
let a = yield 1
}
let it = fn()
let obj = it.next()
console.log(obj) // { value: 1, done: false } 这里的value:1并不是a的值,而是yield产出的结果
// 此时函数执行,碰到yield,产出1,且函数停止执行,所以obj中value为1,done为false
当迭代器每调用一次next
方法,会让执行一个yield
后面的代码执行,并产出结果,但是此时代码是卡在yield
的位置,要想yield
左边得到返回值,需要接着调用yield
,例如:
js
function * fn(){
let a = yield 1
console.log(a); // undefined
}
let it = fn()
let obj = it.next()
console.log(obj) // { value: 1, done: false } 这里的value:1并不是a的值,而是yield产出的结果
let obj1 = it.next()
console.log(obj1); // { value: undefined, done: true } 表示函数执行完毕,但是没有返回值
若是用一个return
返回一个值,则看效果
js
function * fn(){
let a = yield 1
console.log(a); // undefined
return 100
}
let it = fn()
let obj = it.next()
console.log(obj) // { value: 1, done: false } 这里的value:1并不是a的值,而是yield产出的结果
let obj1 = it.next()
console.log(obj1); // { value: 100, done: true } 表示函数执行完毕,返回值是100
上面的例子可以看出,generator
函数执行完后,返回的是一个带有value
和down
的对象; yield
非常像return
,但是从上面例子可以看出,a
的值依然是undefined
,所以yield
并不会给它前面的变量赋值,要想将yield产出的值赋给它前面的变量a,可以这样写:
js
function * fn(){
let a = yield 1
console.log(a); // 1
}
let it = fn()
// 第一个next函数中传递参数是没意义的
let obj = it.next()
console.log(obj) // { value: 1, done: false } 这里的value:1并不是a的值,而是yield产出的结果
// next会将传入从参数,会传递给上一次yield前面的变量(或者说会传递给上一次yield的返回值)
let obj1 = it.next(obj.value)
console.log(obj1); // { value: undefined, done: true } 表示函数执行完毕,但是没有返回值
注意
- next会将传入从参数,会传递给上一次yield前面的变量(或者说会传递给上一次yield的返回值)
- 第一个next函数中传递参数是没意义的,因为没有上一个yield
调用 next()方法时,如果传入了参数,那么这个参数会传给上一条执行的 yield语句左边的变量
当在生成器函数中显式 return 时,会导致生成器立即变为完成状态,即调用 next() 方法返回的对象的 done 为 true。如果 return 后面跟了一个值,那么这个值会作为当前调用 next() 方法返回的 value 值。
总结:
在generator 函数中,
- yield 相当于设置了一个断点,执行到这就暂停当前函数的运行,并返回值或执行语句的结果value。它和return的区别在于,return的返回意味着函数生命周期的结束,而yield的返回只是挂起了函数的运行。
- next 相当于从之前的yield断点或函数起始位置按F5执行,并在下一个yield断点,或return正常结束时停下脚步,后获得返回的value。next同样可以传参数,其参数会占领上一轮yield的结束位置,如果未指定则替代值为undefined。next 和 yield是一一对应的开始与暂停符,而最后一个next对应return。generator 函数如果正常结束(以return结束),则视为完成任务(done == true).
- yield 如果想在其后嵌套调用generator 函数,则必须在其后加个
*
号,yield* generatorFunc() 会继续自动调用 generatorFunc().next(),而不会暂停当前函数。yield* 必须是一个generator 函数对象,否则会将其当成字符串以yield的方式逐个字符输出。
3. generator函数原理
要想知道generator语法的原理,可以看一下,编译过后,generator函数语法具体是怎么实现的,这里借助babel,将generator进行编译:
从上图可以看出,编译前后代码对比,我们可以知道,核心靠的就是 switch case
来实现的;
4. 迭代器基本实现
js
const interable = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
interable[Symbol.iterator] = function() {
let index = 0;
return { // 遍历器对象
next: () => {
return { value: this[index], done: index++ == this.length }
}
}
}
console.log([...interable]); // [ 'a', 'b', 'c' ]
解释: 一个伪数组该具备的条件:
- 有索引
- 有length属性
- 能被遍历(迭代)
一个迭代器都会有一个Symbol.iterator
属性,且,迭代器会返回一个next方法,调用这个next会返回一个包含value和done属性的对象。
如果我们自己去迭代一个对象需要实现一个迭代器接口,自己返回一个具有next方法的对象。内部会调用这个next方法返回结果包含value和done,当done为true时迭代完成
1. Array.from于...展开运算符号的区别
看一下例子
js
let obj = {
0: 1,
1: 2,
2: 3,
length:3
}
// 用from
let arr = Array.from(obj)
console.log(arr); // [ 1, 2, 3 ]
// 展开运算符
let arr1 = [...obj] // 报错:object is not iterable (cannot read property Symbol(Symbol.iterator))
5. 通过生成器实现遍历器
上面说过,调用生成器函数会得到一个可迭代对象,那么可以将上一步中实现的迭代器的interable[Symbol.iterator]
方法直接其换成生成器函数,如下:
js
const iterable = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
iterable[Symbol.iterator] = function*() {
let index = 0;
while (index !== this.length) {
yield this[index++]
}
}
console.log([...iterable]);
生成器函数会自带next方法
6. 生成器的使用
这里主是掌握异步迭代的思想。(产生一个迭代函数,当做回调函数使用)
js
// it表示 iterator,也就是生成器函数调用之后的结果
function co(it){
return new Promise((resolve,reject)=>{
function next(data){
let {value,done} = it.next(data);
if(!done){
Promise.resolve(value).then(data=>{
next(data);
},reject)
}else{
resolve(value);
}
}
next();
});
}
这里是模拟了一个库,co
,co库就是对生成器函数的使用进行了封装,我们子使用生成器函数时候就不用很麻烦的写一堆next方法了。 CO库npm地址:https://www.npmjs.com/package/co
[Symbol.iterator]
Symbol.iterator
是ES6中提供的,可以用来会去对象的@@iterator
内部属性。
数组,字符串等对象之所以可以用for...of
进行比遍历,原因是这些对象内部都内置的有@@iterator
对象,
for...in
循环除外,for...in
循环用来遍历对象,并且for...in
遍历对象是无法直接获取属性值的,因为它遍历的实际上是对象中所有可枚举属性,取药手动取属性值。
for...of
不一样,for...of
首先会向背访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()
方法来遍历所有返回值
例如:数组内置了@@iterator
,for...of
可以直接应用在数组上,我们使用内置的@@iterator
进行手动遍历数组,可以看他是怎么工作的:
js
let arr = [1,2,3];
let it == arr[Symbol.iterator]();
it.next(); // {value:1, done:false}
it.next(); // {value:2, done:false}
it.next(); // {value:3, done:false}
it.next(); // {done:true}
与数组不同,普通对象内并没有@@iterator
,所以无法自动完成for...of
遍历。
但是,我们若是想用for...of
遍历一个普通对象,只需要手动为这个对象添加@@iterator
即可,例如:
js
let obj = {
a: 1,
b: 2,
};
// --------------为obj添加@@iterator对象------------
// 这里用Object.defineProperty定义的原因是为了让Symbol.iterator属性不可枚举
Object.defineProperty(obj, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: () => {
let keys = Object.keys(obj);
let idx = 0;
return {
next: () => ({
value: obj[keys[idx++]],
done: idx > keys.length,
}),
};
},
});
for (const v of obj) {
console.log(v);
}
// 1
// 2
这样普通对象就变成了可迭代对象。