几个月前写过一篇博客,讲Generator,比较基础。最近总在写ES6,想深入讲讲yield的执行顺序。你可能想问,Generator执行顺序很简单啊,就是调用next()就执行下一个yield后面的代码。很多问题,如果你认为很简单,很可能是你理解不深刻,就像我当初也认为Generator很简单。如果你关心koa中间件的执行顺序也可以接着看看。
yield与yield*
关于这个话题,你只需要知道四点:
- yield* fn():相当于用fn的内容来替换该位置,不会消耗一次next()调用,fn内的代码会被执行
- yield* fn:报错。因为fn是一个generator function,而yield*后面应该是一个generator
- yield fn():yield的结果是一个generator,消耗一次gen的next()调用,且fn内的代码不会被执行
- yield fn: yield的结果是一个generator function,消耗一次gen的next()调用,且fn内的代码不会被执行
例子:
class Test{
constructor(){
this.gen = this.f1();
this.gen.next();
this.gen.next();
}
*noop() {}
*f1() {
console.log('1: pre');
yield this.f2();
console.log('1: after');
}
*f2() {
console.log('2: pre');
yield this.f3();
console.log('2: after');
}
*f3() {
console.log('3: pre');
console.log('3: after');
}
}
let test = new Test();
打印出:
1: pre
1: after
没有执行f2,f3代码,因为yield不会继续执行后面的函数,但是yield* 会继续执行后面的函数,知道得出一个结果。把上面代码的yield
换成yield*
,执行结果:
1: pre
2: pre
3: pre
3: after
2: after
1: after
如果你知道Koa,你会发现,这就是Koa中间件的执行顺序,但是Koa不是通过yield*实现Generator的自动执行,而是通过co。
Koa中间件执行顺序的原理
下面是一段koa代码,如果你不知道koa也没关系,只要知道koa的所有中间件都是Generator函数:
var koa = require('koa');
var app = koa();
app.use(function* f1(next) {
console.log('f1: pre next');
yield next;
console.log('f1: post next');
});
app.use(function* f2(next) {
console.log(' f2: pre next');
yield next;
console.log(' f2: post next');
});
app.use(function* f3(next) {
console.log(' f3: pre next');
this.body = 'hello world';
console.log(' f3: post next');
});
app.listen(4000);
你感觉执行顺序是什么?
答案:
f1: pre next
f2: pre next
f3: pre next
f3: post next
f2: post next
f1: post next
执行顺序:
并不是执行完f1再执行f2,最后执行f3。
似乎和上面yield*执行顺序有点相似呢。
compose
上述带yield*的函数为了实现这个“回形”执行顺序时候,在定义f1的时候需要yield* f2()
,显然Koa不可能在注入每个中间件时候再改变其内部yield的内容,那么我们就需要compose函数了。
function compose(middleware) {
return function*(next) {
var i = middleware.length;
var prev = next || noop();
var curr;
while (i--) {
curr = middleware[i];
prev = curr.call(this, prev);
}
yield* prev;
}
}
function* noop() {}
调用方式
let gen = compose([f1, f2, f3]); //f1,f2,f3是带yield的Generator Function
//gen是一个类似 f1(f2(f3({})))的Generator对象,Generator对象是调用一次Generator Function返回的结果
co
如果你执行gen.next()
会返回11: prev
,不能调用f2和f3,当然,如果你和上面一样,把yield都改成yield* 是会链式调用f2,f3的。可是koa没有规定必须使用yield*。为了执行gen,koa引入了co——一个Gnerator自动执行函数。
const co = require ('co');
co(gen);
f1, f2,f3就可以实现上述“回形”执行了。如果你想深入co原理,请戳co。
Generator的GeneratorStatus
更深入一点,如果有多个yield会怎么样。
class Test{
constructor(){
this.g = this.compose([this.f1, this.f2, this.f3])();
co(this.g);
}
*noop() {}
*f1(next) {
console.log('11: prev');
yield next;
console.log('11: after');
console.log('12: prev');
yield next;
console.log('12: after');
}
*f2(next) {
console.log('21: prev');
yield next;
console.log('21: after');
console.log('22: prev');
yield next;
console.log('22: after');
}
*f3(next) {
console.log('31: prev');
yield next;
console.log('31: after');
console.log('32: prev');
yield next;
console.log('32: after');
}
compose(middleware) {
let self = this;
return function*(next) {
var i = middleware.length;
var prev = next || self.noop();
var curr;
while (i--) {
curr = middleware[i];
prev = curr.call(this, prev);
}
yield* prev;
}
}
}
let test = new Test();
会不会 f1 -> f2 -> f3 -> f2 遇到f2的第二个yield又去执行f3?
答案是不会的。遇到f2第二个yield会跳过,继续执行下面语句,不会再执行一遍f3。原因和简单,Generator对象是有状态的,即GeneratorStatus属性,其值从suspended变为closed后,不会再改变。就是说,Generator对象在一个环境中,只能执行一遍。上述代码执行结果:
11: prev
21: prev
31: prev
31: after
32: prev
32: after
21: after
22: prev
22: after
11: after
12: prev
12: after
当一个Generator函数没有未执行的yield,变为普通函数,GeneratorStatus的值从suspended变为closed。