ES6-深入理解Generator yield & Koa中间件执行顺序

时间:2021-07-21 23:32:58

几个月前写过一篇博客,讲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

执行顺序:
ES6-深入理解Generator yield & Koa中间件执行顺序

并不是执行完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

ES6-深入理解Generator yield & Koa中间件执行顺序
当一个Generator函数没有未执行的yield,变为普通函数,GeneratorStatus的值从suspended变为closed。