众所周知,传统的JavaScript异步的实现是通过回调函数来实现的,但是这种方式有两个明显的缺陷:
1.缺乏可信任性。例如我们发起ajax请求的时候是把回调函数交给第三方进行处理,期待它能执行我们的回调函数,实现正确的功能 2.缺乏顺序性。众多回调函数嵌套使用,执行的顺序不符合我们大脑常规的思维逻辑,回调逻辑嵌套比较深的话调试代码时可能会难以定位。
Promise恢复了异步回调的可信任性,而Generator正是以一种看似顺序、同步的方式实现了异步控制流程,增强了代码可读性。
generator是ES6提供的一种异步编程解决方案,在语法上,可以把它理解为一个状态机,内部封装了多种状态。执行generator,会生成返回一个遍历器对象。返回的遍历器对象,可以依次遍历generator函数的每一个状态。同时ES6规定这个遍历器是Generator函数的实例,也继承了Genarator函数的prototype对象上的方法。
Generator 函数是一个普通函数,但是有两个特征。
一是,function关键字与函数名之间有一个星号; 二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
Generator(生成器)是一类特殊的函数,跟普通函数声明时的区别是加了一个*号。
Iterator(迭代器):当我们实例化一个生成器函数之后,这个实例就是一个迭代器。可以通过next()方法去启动生成器以及控制生成器的是否往下执行。
yield/next:这是控制代码执行顺序的一对好基友。通过yield语句可以在生成器函数内部暂停代码的执行使其挂起,此时生成器函数仍然是运行并且是活跃的,其内部资源都会保留下来,只不过是处在暂停状态。在迭代器上调用next()方法可以使代码从暂停的位置开始继续往下执行。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
上面的程序执行了四次next:
第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。
第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。
第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。
第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句或者执行完return之后再运行next的时候,则返回的对象的value属性值为undefined,done为true。
注意: 需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。 yield表达式如果用在另一个表达式之中,必须放在圆括号里面 yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。 yield放在表达式中的时候,let s =(yield 1+2),s其值将会是undefined,而1+2这个等于3的值将会作为next返回对象的value的值
Generator函数返回的Iterator运行的过程中,如果碰到了yield, 就会把yield后面的值返回, 此时函数相当于停止了, 下次再执行next()方法的时候, 函数又会从上次退出去的地方重新开始执行;
如果把yield和return一起使用的话, 那么return的值也会作为最后的返回值, 如果return语句后面还有yield, 那么这些yield不生效:
function* gen() {
yield 0;
yield 1;
return 2;
yield 3;
};
let g = gen();
console.log(g.next(),g.next(),g.next(),g.next());
//输出:{ value: 0, done: false } { value: 1, done: false } { value: 2, done: true } { value: undefined, done: true }
yield这种语句让我们可以在Generator函数里面再套一个Generator, 当然你要在一个Generator里面调用另外的Generator需要使用: yield 函数() 这种语法, 都是套路啊:
function* foo() {
yield 0;
yield 1;
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
};
//依次输出x,0,1,y
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
这个参数就是解决了上面说的注意事项的最后一个,yield的返回值总是undefined,
由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
Generator函数返回的Iterator执行next()方法以后, 返回值的结构为:
{
value : "value", //value为返回的值
done : false //done的值为一个布尔值, 如果Interator未遍历完毕, 他会返回false, 否则返回true;
}
所以我们可以模拟一个Generator生成器, 利用闭包保存变量, 每一次执行next()方法, 都模拟生成一个{value:value,done:false}的键值对:
function gen(array){
var nextIndex = 0;
return {
next: function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
};
var it = gen(["arr0", "arr1", "arr2", "arr3"]);
console.log( it.next() );
console.log( it.next() );
console.log( it.next() );
console.log( it.next() );
console.log( it.next() );
如果给next方法传参数, 那么这个参数将会作为上一次yield语句的返回值 ,这个特性在异步处理中是非常重要的, 因为在执行异步代码以后, 有时候需要上一个异步的结果, 作为下次异步的参数, 如此循环::
<script>
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
</script>
如果执行Generator生成器的throw()方法, 如果在Iterator执行到的yield语句写在try{}语句块中, 那么这个错误会被内部的try{}catch(){}捕获 :
<script>
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获0', e);
}
};
var i = g();
i.next(); //让代码执行到yield处;
try {
i.throw('a');
} catch (e) {
console.log('外部捕获', e);
}
</script>
如果执行Iterator的return()方法, 那么这个迭代器的返回会被强制设置为迭代完毕, 执行return()方法的参数就是这个Iterator的返回值,此时done的状态也为true:
<script>
function* gen() {
yield 0;
yield 1;
yield 2;
yield 3;
};
let g = gen();
console.log(g.return("heheda")); //输出:{ value: 'heheda', done: true }
</script>
Generator中的this就是谁调用它,那么this就是谁, 我们利用Reflect.apply可以改变Generator的上下文:
function* gen() {
console.log(this);
yield 0;
};
console.log(gen().next());
console.log(Reflect.apply(gen,"heheda").next());
1.比如抽奖环节,当前用户还可以抽奖5次。点击后次数减1。
若采用ES5的方式,不使用Generator,则需要将count存入全局变量中,但是这样非常不安全,如果别人知道变量是什么,就可以修改变量;另外存入全局变量也会影响性能。
{
let draw=function(count){
//具体抽奖逻辑,跟次数的校验是分开的
//输出剩余次数
console.log(`剩余${count}次`)
}
//利用Generator控制次数
let residue=function*(count){
while(count>0){
count--
yield draw(count)
}
}
//将Generator实例化,通过按钮绑定,点击执行next,进行抽奖
let star=residue(5)
let btn=document.createElement('button')
btn.id='start'
btn.textContent='抽奖'
document.body.appendChild(btn)
document.getElementById('start').addEventListener('click',function(){
star.next()
},false)
}
2.长轮询
场景:服务端的某一个数据状态定期变化,前端需要定时的去服务端取这个状态
对于这种场景,有两种解决方案
1)长轮询(定时器,定时访问接口)
2)websocket(浏览器兼容性不好)
{
let ajax=function* (){
yield new Promise(function(resolve,reject){
setTimeout(function(){
resolve({code:0})
},200)
})
}
let pull=function(){
let generator=ajax()
let step=generator.next()
step.value.then(function(d){
if(d.code!=0){
setTimeout(function(){
console.log('wait')
pull()
},1000)
}else{
console.log(d)
}
})
}
pull()
}
输出结果为
{code: 0}
将resolve({code:0})中code改成1,会一直轮询,输出结果为
wait
wait
wait
...
ES6 诞生以前,异步编程的方法,大概有下面四种。
1.回调函数。 2.事件监听。 3.发布/订阅。 4.Promise 对象。
Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。