最近在看 koa 的中间件实现时,总是看到 yield* next
这种用法,觉得很困惑。下面是学习成果。
代理 yield
我们先来看看原生语法中,代理 yield(delegating yield)的含义和用法。
首先是下面的代码(普通的 yield):
1 | function* outer() { |
然后是下面的代码(代理 yield):
1 | function* outer() { |
根据文档的描述,yield*
后面接受一个 iterable object 作为参数,然后去迭代(iterate)这个迭代器(iterable object),同时 yield*
本身这个表达式的值就是迭代器迭代完成时(done: true)的返回值。调用 generator function 会返回一个 generator object,这个对象本身也是一种 iterable object,所以,我们可以使用 yield* generator_function()
这种写法。
啊,好绕。在我实际的使用过程中,yield*
一般用来在一个 generator 函数里“执行”另一个 generator 函数,并可以取得其返回值。
yield 和 co
我们再来看看 co 中 yield 的用法,代码如下:
1 | co(function* () { |
等等!为什么 co 可以直接 yield 一个 generator 而不用 yield*
,更奇怪的是居然可以直接 yield 一个 generator function?
实际上,co 里面做了封装,在 co 的 toPromise
代码里有这样的判断:
1 | if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); |
所以,其实我们的代码:
1 | // yield generator function |
实际上会变成类似这样:
1 | var b = yield co(fn); |
而 co 方法最后会返回一个 Promise,同时还会对传入的参数进行处理,如果是函数则直接调用这个 generator function 取得 generator,大致如下:
1 | function co(gen) { |
所以,再 yield
这个 Promise 后,保证了我们取到了正常结果。
其实,我们也可以直接用“原生”的语法写成:
1 | var b = yield* fn(); |
因为 yield*
接受一个 iterable object 作为参数(比如:generator function 返回的 generator object),所以,我们需要调用 generator function 来返回一个 generator object。
所以,co 背后是封装了很多黑魔法的,这多创建了闭包,会有一点点点点的内存消耗。不是我打错了,根据这里的讨论:https://github.com/koajs/compose/issues/2,确实没有非常要命的影响。而我们的 TJ 大神非常不喜欢在 yield 和代理 yield 间切换,他觉得本来就应该是语言自己去帮我做代理的,反正我就是不想写 yield*
。
不过嘛,这个 issue 最后的结果还是将 next 实现成了一个 generator object,所以我们才会在各种 koa 的中间件里面看到 yield* next
这种写法。
yield* 的作用
好了,在 co 的世界里,yield* fn
“几乎”等价于 yield co(fn)
。既然这样,那 yield*
到底有啥用啊?还是有用处的。具体有下面这几点:
- 用原生语法,抛弃 co 的黑魔法,换取一点点点点性能提升
- 明确表明自己的意图,避免混淆
- 调用时保证正确的 this 指向
第一点上面有提到,就不展开说了。
第二点我们来解释下。看看下面的代码,你能知道 yield 后面的到底是个什么鬼吗?
1 | var v = yield later(); |
如果你不看 later 的实现,实际上你并不知道。later 方法完全可以实现成返回一个 Promise,返回一个 thunk,返回一个数组、对象,或者就是返回 generator object。
但是下面这样的代码:
1 | var v = yield* later(); |
我们就很自信的知道 later 肯定是返回一个 generator object 了。
第三点也是 co 黑魔法的一个大坑。
比如我们有下面这样的代码:
1 | function later(n, t) { |
你会发现, result 是 run->undefined
,这说明 this 的指向不对了。这是因为 co 实际上是类似这样执行上面的代码的:
1 | var runner = new Runner(); |
破解大法也很简单,就是使用原生语法:
1 | var result = yield* runner.run(); |
那么,我就说完了。如果各位没有了解过 co 的黑魔法,看的可能会有点累,推荐大家看看参考资料的第一条。
以上。
参考资料:
- http://purplebamboo.github.io/2014/05/24/koa-source-analytics-2/
- http://www.jongleberry.com/delegating-yield.html
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield*
本文采用 知识共享署名 3.0 中国大陆许可协议,可自由转载、引用,但需署名作者且注明文章出处 。