Original address: Regenerator
I searched around for the Generator and talked mostly about its usage, but very little about how it works, namely “coroutines”. But because it’s something we use every day, directly or indirectly, we’re going to write an article about it
History of JS callback
A,Callback
- In ES5 and earlier, writing callbacks was mostly callback, so leave callback hell alone
Second, the Promise
-
Promise optimizes the way callbacks are written by chain-calling, which is essentially a callback. Deferred encapsulated by it can also be seen in various open source libraries, such as Qiankun
-
Promises themselves are nothing new, but callbacks registered by THEN perform this during the microtask phase of the current event loop, meaning promises can only be provided at the native level. User-level polyfill can only be done with macro tasks, such as promise-polyfill
Third, the Generator
-
Generator is the star of this article. ES6’s blockbuster feature, known as a state machine, contains various states and uses yield to trigger the next step
-
Generator introduces the concept of “coroutines” that traditional callbacks cannot match, which means that asynchronous code can be written synchronously with automatic execution, such as tJ’s CO library
-
The generator object also implements:
-
Iterable protocol (symbol. iterator) : available through for… Of iterates, such as the built-in objects Array and String, which implement this protocol
-
Iterator protocol (next()) : Its next method can be called to get {value: any, done: Boolean} to determine the status
Async, await
-
Generator, yield syntax sugar, select some features. The flip side of that is that you cut off some functionality (more on that later)
-
If Babel is used to compile code with async, await, and yield, then asyncGeneratorStep and _asyncToGenerator are automatically executed
-
The principle is simple:
-
Get the Generator object and use Promise’s microtask capabilities to execute next
-
The value returned by ret.value is the value of await, encapsulated as a Promise for the next entry
-
Judge each recursion until done is returned true
async function a() {}
function* b() {}
// Babel is compiled
function asyncGeneratorStep(gen, resolve, reject, _next, ...) {
// Call gen's next or throw method
var info = gen[key](arg);
var value = info.value;
if (info.done) {
resolve(value);
} else {
// Execute recursively
Promise.resolve(value).then(_next, _throw); }}function _asyncToGenerator(fn) {
return function () {
return new Promise(function (resolve, reject) {
// Get the generator object
var gen = fn.apply(self, arguments);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
// Initialize next
_next(undefined);
});
};
}
Copy the code
Generator Object, Generator, GeneratorFunction
A, the generator Object
- It is executed by Generator and returned with prototype methods such as next, return, and throw
function* gen() {}
const gObj = gen();
gObj.next();
gObj.return();
Copy the code
Second, the Generator
- through
function*
Syntax, which is an instance of GeneratorFunction
Object.getPrototypeOf(gen).constructor // GeneratorFunction {prototype: Generator, ... }
Copy the code
- The Generator function itself has little meaning at the user code level and is rarely used
Third, GeneratorFunction
- It is a built-in function, but it is not attached directly to the window, but we can get it from an instance of it
const GeneratorFunction = Object.getPrototypeOf(gen).constructor;
Copy the code
- GeneratorFunction and
Function
Is a level that can be passed to create functions such as
const gen = new GeneratorFunction('a'.'yield a * 2');
const gObj = gen(10);
gObj.next().value / / 20
Copy the code
Working principles of the Generator
Examples of code for the positive start:
let num = 0;
async function gen() {
num = num + (await wait(10));
await 123;
await foo();
return num;
}
function wait(num: number) {
return new Promise((resolve) = > setTimeout(() = > resolve(num), 600));
}
async function foo() {
await "literal";
}
await gen();
console.log("regenerator: res", num);
Copy the code
First, the core point
-
How is the state of the Generator implemented, or how does the Generator stop at yield
-
How do multiple generators collaborate, i.e. give authority to one Generator and then give authority back
-
How one Generator listens for the execution of another Generator, yield* genFn()
Ii. The relationship between Generator, GeneratorFunction and its prototype
If you’ve forgotten about prototype chains and inheritance, I suggest you take a look at prototype&extends
class GeneratorFunction {}
// GeneratorFunction prototype
class GeneratorFunctionPrototype {
static [Symbol.toStringTag] = "GeneratorFunction";
// Implement iterator Protocol
next(args) {}
return(args) {}
throw(args) {}
// Implement iterable Protocol
[Symbol.iterator]() {
return this; }}// Reference each other
GeneratorFunctionPrototype.constructor = GeneratorFunction;
GeneratorFunction.prototype = GeneratorFunctionPrototype;
// Set prototype
class Generator {}
Generator.prototype = GeneratorFunctionPrototype.prototype;
Copy the code
Ii. State of the Generator
-
It is not difficult to implement the state machine. The state is recorded through a flag, and the next state is recorded after each state operation, and the execution is carried out after a certain time
-
The state machine is generated by user-level code, which uses switch case + Context to record parameters
function _callee$(_context) {
while (1) {
switch (_context.next) {
case 0:
// await wait(10)
_context.next = 3;
return wait(10);
case 3:
// await 123
_context.next = 7;
return 123;
case 7:
_context.next = 9;
// await foo()
return foo();
case "end":
return_context.stop(); }}}Copy the code
- Each yield corresponds to a switch case, which returns each time.
Three, multiple Generator collaboration
-
Case return Indicates that the Generator is granted permission to actively execute another Generator and exit its own state
-
If foo Generator is a switch case, how does it revert to the parent state machine and trigger the parent state machine to continue
-
Let’s see how Babel compiles async functions. Leaving mark and warp aside, _asyncToGenerator is automatic execution as we said earlier, which is the same as co(markFn). On the other hand, you can infer that the regeneratorRuntime.mark function returns a Generator for polyfill
function _foo() {
_foo = _asyncToGenerator(
regeneratorRuntime.mark(function _callee2() {
return regeneratorRuntime.wrap(function _callee2$(_context2) {
switch (_context2.next) {
case 0:
_context2.next = 2;
return "literal";
case "end":
return_context2.stop(); } }, _callee2); }));return _foo.apply(this.arguments);
}
Copy the code
-
{value: “literal”, done: True} is returned by the mark function and given to _asyncToGenerator. How is this used? Promise.then (next)
-
What about collaboration? Don’t limit yourself to foo, the parent gen function does the same thing! The gen function waits for Foo resolve, of course, then gen returns {value: fooRetValue, done: false}, and continues next
-
Clean up:
-
The parent gen function executes a case, takes the return value of the child foo as the result of the case, and then blocks itself (i.e. waiting for the child Promise resolve).
-
Foo returns done true after executing, terminates his state career, and then resolves his CO Promise resolve
-
(3) Gen stuck Promise receives foo’s result, returns done false this time, starts next, and re-enters the corresponding case via context.next
- So as you can see, Generator is not going to be a big deal without Promise, either native or polyfill, mainly because, again, we can’t interfere with v8’s event loops at the JS level
Mark, wrap, Context
- You should know the MARK function: Take a function and transform it into a Generator. How do you do that? Inherit
function mark(genFn: () => void) {
return _inheritsLoose(genFn, GeneratorFunctionPrototype);
}
function _inheritsLoose(subClass, superClass) {
Object.setPrototypeOf(subClass, superClass);
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
return subClass;
}
Copy the code
-
Each wrap creates a context to manage state and context parameters, and a snapshot is taken each time the case is executed to prevent the parameters from changing after yield
-
The mark function’s next, return, and throw calls are the wrap’s ability to coordinate the user code (switch case) with the context to decide what to do next. So to perfect the GeneratorFunctionPrototype, connects and wrap it, only in charge transfer type and args
type GeneratorMethod = "next" | "return" | "throw";
class GeneratorFunctionPrototype {
// set by wrap fn
private _invoke: (method: GeneratorMethod, args) = > { value: any.done: boolean };
// Note that this is a prototype method
next(args) {
return this._invoke("next", args);
}
return(args) {
return this._invoke("return", args);
}
throw(args) {
return this._invoke("throw", args); }}Copy the code
- Wrap implementation
function wrap(serviceFn) {
/ / still borrow GeneratorFunctionPrototype ability
const generator = new Generator();
const context = new Context();
let state = GenStateSuspendedStart;
/ / implementation _invoke
generator._invoke = function invoke(method: GeneratorMethod, args) {
context.method = method;
context.args = args;
if (method === "next") {
// Record context parameters
context.sent = args;
} else if (method === "throw") {
throw args
} else {
context.abrupt("return", args);
}
// Execute business code
const value = serviceFn(context);
state = context.done ? GenStateCompleted : GenStateSuspendedYield;
return {
value,
done: context.done
};
};
return generator;
}
Copy the code
- Context records the current running status and Context parameters, and provides termination, error reporting, and proxy methods
class Context {
next: number | string = 0;
sent: any = undefined;
method: GeneratorMethod = "next";
args: any = undefined;
done: boolean = false;
value: any = undefined;
stop() {
this.done = true;
return this.value;
}
abrupt(type: "return" | "throw", args) {
if (type === "return") {
this.value = args;
this.method = "return";
this.next = "end";
} else if (type === "throw") {
throwargs; }}}Copy the code
Five, the yield * genFn ()
Finally, although you may not use the Generator enough, it is incomplete without it
- The functionality left out of “await” and “async” is that one Generator listens to the execution of another Generator. In fact, with await we do not know how many await the child function has experienced
async function a() {
const res = await b();
}
async function b() {
await 1;
await 'str';
return { data: 'lawler'.msg: 'ok' };
}
Copy the code
- So how does this function work at the yield level? In effect, yield* replaces Foo with the delegateYield method, looping through the context so that the yield completes in a case
function gen$(_context) {
switch (_context.next) {
case 0:
_context.next = 7;
return wait(10);
case 7:
// Pass foo Generator Object to gen's context
return _context.delegateYield(foo(), "t2".8);
case "end":
return_context.stop(); }}Copy the code
- Inside the wrap, loop execution
generator._invoke = function invoke(method, args) {
context.method = method;
// yield* genFn, returns the result of the genFn iteration until return
while (true) {
const delegate = context.delegate;
if (delegate) {
const delegateResult = maybeInvokeDelegate(delegate, context);
if (delegateResult) {
if (delegateResult === empty) continue;
{value, done}
returndelegateResult; }}}if (method === "next") {}}Copy the code
The last
-
This article simply implements Generator, but regenerator does a lot more than throw error, yield* gen() handling, and other handy apis. Dive in if you like
-
Through the explanation of the working principle of Generator in this article, let us have a deeper understanding of the concept of “coroutine”, which has the effect of “cutting the knife without error” for the things we need to use every day and the debugging code
-
Source code: regenerator
-
Code word is not easy, like small partners, remember to leave your small ❤️ oh ~
The resources
-
MDN Generator
-
MDN Iteration protocols
-
regenerator
-
co