This is the 8th day of my participation in Gwen Challenge
Those of you who are familiar with the Koa framework should know that Koa1 middleware uses Generator functions and Koa2 middleware uses async functions. However, to be compatible with Generator functions, Koa2 uses the CO library to convert a Generator into a Promise.
The basic concept
Before analyzing the CO source code, let’s take a quick look at some basic knowledge.
Generator functions and Generator objects
Generator functions are defined by the function* syntax and are simply used as follows:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
Copy the code
Running this function returns a Generator object,
var hw = helloWorldGenerator();
console.log(hw.__proto__); // Generator {}
Copy the code
You can see that Generator functions have yield expressions, and Generator objects inherit from next, return, and throw methods. Each time a Generator object executes the next method, it continues execution from where the Generator function last left off to the next yield or return. The yield (or return) expression is the return value. The next method returns a {done:… , value:… }, where value is the return value of a yield or return expression, as follows:
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
Copy the code
See the syntax of Generator functions and the asynchronous use of Generator functions for details.
Promise
A Promise object can be thought of as a container that encapsulates some event (usually an asynchronous operation) that will end in the future. Promises have three states:
- Pending (pending)
- This is a big pity.
- Rejected (= rejected)
The Promise constructor takes a function as an argument, resolve and reject. Simple use is as follows:
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* Asynchronous operation succeeded */){
resolve(value);
} else{ reject(error); }});Copy the code
When the resolve function is called, a Promise changes from its pending state to a fulfilled state. When the REJECT function is called, Promsie changes from pending to Rejected.
The then method can be used to specify the callback function to execute when the state of a Promise changes:
promise.then(function(value) {
// The Promise state will be fulfilled
}, function(error) {
// Called when the Promise state changes to Rejected
});
Copy the code
In addition to the then method, promises also have catch and finally methods:
promise
.then(result= > {···}) // The Promise state will be fulfilled
.catch(error= > {···}) // Called when the Promise state changes to Rejected
.finally(() = > {···}); // Can not know the state of the previous phase, will call
Copy the code
In simple terms, the Promise object expresses asynchronous operations as a flow of synchronous operations, avoiding layers of nested callback functions. In addition, Promise objects provide a unified interface that makes it easier to control asynchronous operations. Flip through the Promise object for more in-depth learning.
Thunk function
The Thunk function is a function that takes only one callback function as an argument. Here is an example:
function sum (x, y, callback) {
setTimeout(function () {
callback(x + y)
}, 1000)}// Set the first few parameters,
// Encapsulate foo as a Thunk function, taking only one more callback as an argument
function sumThunk (callback) {
return sum(3.4, callback)
}
// Execute Thunk
sumThunk(function (sum) {
console.log(sum)
})
Copy the code
For more details, see You-dont-know-js Thunks.
Source code analysis
Finally, the source code. Co source code is actually very small, only an index.js file, only dozens of lines of core code, learning does not need to spend a lot of time. This is also the first project where I’ve read the source code line by line.
Type judgment
Before analyzing the core code, we first learn some type judgment method in the source code, see how the big guy is to do type judgment. The types of promise, common objects, Generator functions, and Generator objects are encapsulated in CO as follows:
// Check whether obj is a promise
function isPromise(obj) {
return 'function'= =typeof obj.then;
}
// Determine if val is an ordinary object
function isObject(val) {
return Object == val.constructor;
}
// check whether obj is a Generator function
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction'= = =constructor.name| | 'GeneratorFunction'= = =constructor.displayName) return true;
return isGenerator(constructor.prototype);
}
// Check whether obj is a Generator object
function isGenerator(obj) {
return 'function'= =typeof obj.next && 'function'= =typeof obj.throw;
}
Copy the code
The core logic
Let’s take a look at the core code of CO: co function, finally exported for our use is this function. This function takes a Generator function that returns a Promise object.
Co first determines whether the parameter passed is a function type, if so it executes the function to get the return value, and then determines whether the return value is a Generator object. The state of the Promise object will be fulfilled if the parameters do not meet the criteria.
function co(gen) {
var ctx = this;
var args = slice.call(arguments.1);
return new Promise(function(resolve, reject) {
// If it is a function, execute the function and get the return value
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// Check whether it is a Generator. If not, return directly, and set the Promise state to the depressing state
if(! gen ||typeofgen.next ! = ='function') return resolve(gen);
onFulfilled();
/ /...
});
}
Copy the code
If the parameters meet the conditions, the onFulfilled function is run.
OnFulfilled executes the next method of the Generator object and, if an exception occurs, simply changes the Promise state to Reject.
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
Copy the code
Get the return value of the Next method on the Generator object and call the custom next function.
Next function verifies RET, converts value to Promise, and continues onFulfilled next method of Generator object through promise. then method. In this way, the Generator function can execute itself. For detailed explanations of each step, see the following notes:
function next(ret) {
// The last step to determine whether the current function is Generator, if so return
if (ret.done) return resolve(ret.value);
// Convert the return value to a Promise
var value = toPromise.call(ctx, ret.value);
// Implement the self-execution of the Generator function by calling next again via ondepressing function using the promise's then method
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// If the iterator value cannot be converted to a Promise, the passed Generator does not meet the criteria. Change the state of the Promise object to Rejected and terminate execution.
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
Copy the code
From here we can see that the CO library implements Generator self-executing logic:
- Call ondepressing to execute the iterator’s next method;
- Call the next function to transform the iterator value into a Promise, and then execute the ondepressing function through the promise. then method
- Repeat steps 1 and 2 until the Generator has completed, obtaining the value of the last returned value, and changing the Promise state to resolve.
Exception handling and Promise conversion
Finally, let’s look at the onRejected function and the toPromise function.
The onRejected function throws an error in the Generator object’s throw. This error will be passed to the Generator’s internal try… Catch, and then continue to execute the next function to determine the return value, continue the self-executing logic. If there is no try inside the Generator… Catch code block, the exception will be external try… Catch sets the state of the Promise to Rejected.
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
Copy the code
The toPromise function converts the value returned by executing the Generator into a Promise object with the following logic:
function toPromise(obj) {
// False values or promises are returned without processing the need
if(! obj)return obj;
if (isPromise(obj)) return obj;
// If it is a Generator or a Generator object, the co function is recursively called for conversion
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
// If it is a thunk function, array, object, call the corresponding method to convert
if ('function'= =typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
// If none of the above conditions is met, return directly
return obj;
}
Copy the code
The Thunk function converts to Promise as follows:
function thunkToPromise(fn) {
var ctx = this;
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments.1);
resolve(res);
});
});
}
Copy the code
Co supports concurrent asynchronous operations that allow certain operations to take place at the same time and wait until they are all complete before proceeding to the next step. In this way, concurrent operations are placed in arrays or objects.
When an array is passed in, you need to convert the Generator into a Promise, as follows:
function arrayToPromise(obj) {
// Recall that in toPromise, if the input parameter is Generator, the co function is called to implement the promise conversion
return Promise.all(obj.map(toPromise, this));
}
Copy the code
If an object is passed in, the logic is as follows:
function objectToPromise(obj){
var results = new obj.constructor(); // Store the result
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// Make a Promise transform for each property of the object
var promise = toPromise.call(this, obj[key]);
// If it is a promise, put it into promises array
if (promise && isPromise(promise)) defer(promise, key);
// If not, place it in the result array
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) { results[key] = res; })); }}Copy the code
co.wrap
Finally, a co. Wrap method is added, which is similar to the function of currification. First pass the Generator function to CO, and then perform the conversion of promise after obtaining the required parameters.
// Actually returns a Currified function
var fn = co.wrap(function* (val) {
return yield Promise.resolve(val);
});
// Return promise after passing in the argument
fn(true).then(function (val) {
// do something...
});
Copy the code
The source code and explanation for implementing this method are as follows:
co.wrap = function (fn) {
// Save the fn. In some cases you may need to retrieve the Generator Function
createPromise.__generatorFunction__ = fn;
// Encapsulates the fn passed in
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this.arguments)); }};Copy the code
conclusion
The CO library is used to convert a Generator into a Promise, and the core logic for Generator self-execution is as follows:
- Call ondepressing to execute the iterator’s next method;
- Call the next function to transform the iterator value into a Promise, and then execute the ondepressing function through the promise. then method
- Repeat steps 1 and 2 until the Generator has completed, obtaining the value of the last returned value, and changing the Promise state to resolve.
For the Generator accepted by co, the yield return value must be one of the following:
- promise
- Thunk function
- An array of
- object
- The Generator object
- The Generator function
Exception handling takes precedence over the Generator’s try… A block of catch code is caught. If it does not catch, the external try… Catch code block capture.
Think about it
How is the following code executed in the source code?
co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err.stack);
});
Copy the code
reference
The meaning and usage of co function library, by Ruan Yifeng
The meaning and Usage of Thunk function, by Ruan Yifeng
Co source code by TJ Holowaychuk
The Generator, by MDN
Dont-Know-JS Thunks, by JobbyM