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:
enumKind { kInitial, // The initial yield that returns the unboxed generator object.
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.
40RUNTIME_FUNCTION(Runtime_SuspendJSGeneratorObject) {41 HandleScope handle_scope(isolate);42DCHECK(args.length() ==1);43CONVERT_ARG_HANDLE_CHECKED(JSGeneratorObject, generator_object,0);4445 JavaScriptFrameIterator stack_iterator(isolate);46 JavaScriptFrame* frame =stack_iterator.frame();47RUNTIME_ASSERT(frame->function()->shared()->is_generator());48DCHECK_EQ(frame->function(),generator_object->function());4950 // The caller should have saved the context and continuation already.51DCHECK_EQ(generator_object->context(), Context::cast(frame->context()));52DCHECK_LT(0,generator_object->continuation());5354 // We expect there to be at least two values on the operand stack: the return55 // value of the yield expression, and the argument to this runtime call.56 // Neither of those should be saved.57int operands_count =frame->ComputeOperandsCount();58DCHECK_GE(operands_count,2);59 operands_count -=2;6061if (operands_count ==0) {62 // Although it's semantically harmless to call this function with an63 // operands_count of zero, it is also unnecessary.64DCHECK_EQ(generator_object->operand_stack(),65isolate->heap()->empty_fixed_array());66DCHECK_EQ(generator_object->stack_handler_index(),-1);67 // If there are no operands on the stack, there shouldn't be a handler68 // active either.69DCHECK(!frame->HasHandler());70 } else {71int stack_handler_index =-1;72 Handle<FixedArray> operand_stack =73isolate->factory()->NewFixedArray(operands_count);74frame->SaveOperandStack(*operand_stack,&stack_handler_index);75generator_object->set_operand_stack(*operand_stack);76generator_object->set_stack_handler_index(stack_handler_index);77 }7879returnisolate->heap()->undefined_value();80 }
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.