学习Generators之前我们先理解一下es6的迭代器:迭代器详解
生成器也是ES6新增加的一种特性。它的写法和函数非常相似,只是在声明时多了一个” * ”号。
function* say(){}
const say = function*(){}
注意:这个 ※ 只能写在function关键字的后面。
但是生成器和普通函数并不是只有写法上的区别,本质上,普通函数在调用后,必定执行该函数直到函数结束或者return为止,中途是不能停止的,而生成器则是可以暂停的的函数。
function* say(){
yield "开始";
yield "执行中";
yield "结束";
}
let it = say(); // 调用say方法,得到一个迭代器
console.log(it) // [object Generator]
console.log(it.next()); // { value: '开始', done: false }
console.log(it.next()); // { value: '执行中', done: false }
console.log(it.next()); // { value: '结束', done: false }
console.log(it.next()); // { value: undefined, done: true }
调用生成器的say方法,此时的函数并没有执行而得到的是一个迭代器,通过前言中迭代器的认识,我们可以知道调用迭代器next()方法,say函数开始执行,当遇到yield时,函数被挂起并返回一个对象,其中包含value属性,它的值是yield后面跟着的数据,并且done的值为false。再次执行next,函数又被激活,并继续往下执行,直到遇到下一个yield。当所有的yield都执行完了,再次调用next时得到的value就是undefined,done的值为true。
当然Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000); //两秒后 -> "执行了!"
如果你能理解前言中的迭代器,那么此时的生成器也就很好理解了。它可以通过yield关键字将函数的执行挂起,或者理解成暂停。它的外部在通过调用next方法,让函数继续执行,直到遇到下一个yield,或函数执行完毕,它的yield,其实就是next方法执行后挂起的地方,并得到你返回的数据。
所以生成器由此可以理解为 可以暂停(暂缓执行)的函数 。
下面再看一个例子
function * say(x) {
let y = 2 * (yield x)
let z = yield (y / 3)
return x+y+z
}
let it = say(5)
console.log(it.next());//{value: 5, done: false}
console.log(it.next());//{value: NaN, done: false}
console.log(it.next());//{value: NaN, done: false}
yield表达式本身没有返回值,或者说总是返回undefined。undefined+数字就返回NaN,next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
下面我们给next传入参数:
function* say(x) {
let y = 2 * (yield x)
let z = yield(y / 3)
console.log(x,y,z);//9,18,5
return x + y + z
}
let it = say(6)
console.log(it.next());//{value: 6, done: false}
console.log(it.next(9));//{value: 6, done: false}
console.log(it.next(5));//{value: 29, done: true}
这里需要认真理解下,
第一个it.next()时返回的是 (yield x)
的值,say(6)
传参为6,所以第一个next() 返回结果为x:6
第二个it.next(9)时传值为9,这里的参数会替换上一个next的返回值来进行计算,所以结果为x:9,y= 2 * x =18
,则返回 z = y/3 = 6
,最终的返回结果为 z:6。
第三个it.next(5)时传值为5,更改上一个返回结果z:6,则z=5, 综上的x:9,y:18,z:5,所以xyz之和为29。
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。
for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
用生成器来完成斐波那契数列:
function* fibonacci() {
let [prev, curr] = [0, 1];
for (curr=1;curr;[prev, curr] = [curr, prev + curr]) {
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
yiled*
表达式ES6 提供了yield*表达式,,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
如果yield
表达式后面跟的是一个遍历器对象,需要在yield
表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*
表达式。
Generator 在函数体外抛出一个错误,然后在函数体内捕获。例如
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
返回给定值,并终结生成器。例如
function *gen2(){
yield 1;
yield 2;
yield 3;
}
let g2 = gen1();
g2.next();//{value:1,done:false}
g2.return();//{value:undefined,done:true}
g2.next();//{value:undefined.done:true}
遍历器对象g2 调用return方法后,Generator 函数的遍历就终止了,返回值的done属性为true,以后再调用next方法,done属性总是返回true。
如果return方法调用时,不提供参数,则返回值的value属性为undefined。
Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
有时候我们希望在异步操作中加入同步的行为。比如,我想打印4句话,但是每句话都在前一句话的基础上延迟2秒输出。代码如下:
使用定时器:
setTimeout(function(){
console.log("first");
setTimeout(function(){
console.log("second");
setTimeout(function(){
console.log("third");
setTimeout(function(){
console.log("fourth");
},2000);
},2000);
},2000);
},2000);
使用回调函数实现异步,你想在前面的异步操作完成后再进行接下来的动作,那只能在它的回调函数中进行,这样就会越套越多,代码越来越来复杂,俗称“回调地狱”。
那么用promise优化一下该功能:
let timeout = function(time){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve();
},time);
});
}
console.log("go on");
timeout(2000).then(function(){
console.log("first");
return timeout(2000);
}).then(function(){
console.log("second");
return timeout(2000);
}).then(function(){
console.log("third");
return timeout(2000);
}).then(function(){
console.log("fourth");
return timeout(2000);
});
Promise作为ES6提供的一种新的异步编程解决方案,但是它也有问题。比如,代码并没有因为新方法的出现而减少,反而变得更加复杂,同时理解难度也加大。
那么使用Generators怎么实现该功能:
//定时器包含Promise方法,方便之后调用
function sleep(time, data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(console.log(data))
}, time);
})
}
//生成器函数
function* timeout(time) {
let sleep1 = yield sleep(2000, 'first')
let sleep2 = yield sleep(2000, 'second')
let sleep3 = yield sleep(2000, 'third')
let sleep4 = yield sleep(2000, 'fourth')
return console.log('go on');
}
// 手动执行
let it = timeout();
it.next().value.then(() => {
it.next().value.then(() => {
it.next().value.then(() => {
it.next().value.then(() => {
it.next()
})
})
})
});
// 自动执行
function run(gen) {
var g = gen()
function next(data) {
var result = g.next(data) //{value: Promise, done: false}
if (result.data) return result.value //如果done:true,则返回最后一项
result.value.then(data => next(data))//如果不是,就继续调用next()进行下一步
}
next()
}
run(timeout)
上述代码就使用了生成器+promise来完成了,定时2秒打印的功能,手动执行其实就是用then方法,层层添加回调函数。而自动执行只要 Generator 函数还没执行到最后一步,next函数就调用自身,一次实现自动执行。
这样,在生成器中,就可以像写同步代码一样来实现异步操作。
但是生成器这种方式需要编写外部的执行器,而执行器的代码写起来一点也不简单。除了可以使用外部模块来自动执行之外,我们在es7中还会学习到一个基于生成器实现的语法糖 :async和await
那么我们看一下async和await怎么实现该功能?
function sleep(time, data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(console.log(data))
}, time);
})
}
async function timeout() {
let sleep1 = await sleep(2000, 'first')
let sleep2 = await sleep(2000, 'second')
let sleep3 = await sleep(2000, 'third')
let sleep4 = await sleep(2000, 'fourth')
return console.log('go on');
}
timeout()
就这样,像同步一样写异步代码,很简单的实现了,大喊一声牛逼!但是因为是es7的语法,所以还是要考虑兼容性的。
在ES7中,加入了async函数来处理异步。它实际上只是生成器的一种语法糖而已,简化了外部执行器的代码,同时利用await替代yield,async替代生成器的(*)号。
第一句用了await。它替代了之前的yield。后面同样需要跟上一个Promise对象。接下来的打印语句会在上面的异步操作完成后执行。外部调用时就和正常的函数调用一样,但它的实现原理和生成器是类似的。因为有了async关键字,所以它的外部一定会有相应的执行器来执行它,并在异步操作完成后执行回调函数。只不过这一切都被隐藏起来了,由JS引擎帮助我们完成。我们需要做的就是加上关键字,在函数中使用await来执行异步操作。这样,可以大大的简化异步操作。
附:自动执行的插件–co模块
Generators可以像同步一样写异步代码,但是执行的过程却有点不尽人意,所以可以借助插件来完成 :co模块。
从上面的例子可以看出,generator 函数体可以停在 yield 语句处,直到下一次执行 next()。co 模块的思路就是利用
generator 的这个特性,将异步操作跟在 yield 后面,当异步操作完成并返回结果后,再触发下一次 next() 。
以上,算是对es6的Generators(生成器 )进行了完整的了解,还有更多处理异步的方法和各自优缺点对比请看另一篇 :JavaScript异步编程是什么? 异步编程都有哪些解决方案?
借鉴:
https://www.cnblogs.com/jaxu/p/6372809.html
http://es6.ruanyifeng.com/#docs/generator
如果本文对你有帮助的话,请不要忘记给我点赞打call哦~o( ̄▽ ̄)do
有其他问题留言 over~