Yield magic
The introduction of Generators in ES6 has greatly changed the way JavaScript programmers view iterators and provided a new way to solve callback hell.
Generators
The iterator pattern is a commonly used design pattern, but when the iteration rules are complex, maintaining the state within the iterator can be cumbersome. This is where generators come in. What are generators?
Generators: a better way to build Iterators.
With the help of the yield keyword, we can implement the Fibonacci sequence more elegantly.
function* fibonacci() {
let a = 0, b = 1;
while(true) {
yield a;
[a, b] = [b, a + b];
}
}Yield and Asynchronous Operations
yield can pause the execution flow, which provides the possibility of changing the execution flow. This is similar to Python's coroutine.
The reason why generators can be used to control code flow is that they use yield to switch the execution paths of two or more generators. This switching is at the statement level, not the function call level. Its essence is CPS transformation.
After yield, the current call actually ends, and the control has actually been transferred to the function that called the next method of the generator externally, accompanied by a change in state. So if the external function does not continue to call the next method, the function where yield is located is equivalent to stopping at yield. So to complete the asynchronous operation and continue the function execution, just call the next method of the generator at the appropriate place, just like the function is executing after pausing.
V8 Implementation
Parse Phase
The processing of generator functions and the yield keyword is in parser.cc. Let's take a look at the AST parsing function: Parser::ParseEagerFunctionBody()
L3955 determines whether it is a generator function. ParseStatementList parses the function body. Note that a generator function is also a function, and in V8, it is also represented by JSFunction.
In the two if function bodies, Yield::kInitial and Yield::kFinal two Yield AST nodes are created.
Yield states are:
Codegen phase
The machine code generation (x64 platform) mainly focuses on runtime-generator.cc and full-codegen-x64.cc.
runtime-generator.cc provides stub code segments such as Create, Suspend, Resume, Close, etc.,
which are used by full-codegen for inline use to generate assembly code.
Let's take a look at RUNTIME_FUNCTION(Runtime_CreateJSGeneratorObject),
The function creates a JSGeneratorObject object to store JSFunction, Context, and pc pointer based on the current Frame, and sets the operand stack to empty.
After yield, the current execution environment is actually saved. L74 saves the current operand stack and saves it to the JSGeneratorObject object.
L108 sets the PC offset of the current frame. L118 restores the operand stack, and L126-L130 returns the value based on the restored mode.
这边我们关注下 args 参数, args[0]是JSGeneratorObject 对象generator_object, args[1]是Object 对象 value, 也就是 next 的返回值,args[2]是表示 resume 模式的值。
对应的我们看到 FullCodeGenerator::EmitGeneratorResume() 中的这几行代码:
L2297从 result 寄存器中取出 value, L2299调用 RUNTIME_FUNCTION(Runtime_ResumeJSGeneratorObject)。
这样,从 yield value 到 g.next() 取出 value, 相信大家有了一个大概的认知了。
延伸
我们看到node.js依托 v8层面实现了协程,有兴趣的同学可以关心下 fibjs, 它是用 C库实现了协程,遇到异步调用就 "yield" 放弃 CPU, 交由协程调度,也解决了 callback hell 的问题。 本质思想上两种方案没本质区别:
Generator是利用yield特殊关键字来暂停执行,而fibers是利用Fiber.yield()暂停
Generator是利用函数返回的Generator句柄来控制函数的继续执行,而fibers是在异步回调中利用Fiber.current.run()继续执行。
参考
http://en.wikipedia.org/wiki/Continuation-passing_style
https://zh.wikipedia.org/zh-cn/协程
fibjs https://github.com/xicilion/fibjs
Last updated