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:

  1. Call ondepressing to execute the iterator’s next method;
  2. Call the next function to transform the iterator value into a Promise, and then execute the ondepressing function through the promise. then method
  3. 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:

  1. Call ondepressing to execute the iterator’s next method;
  2. Call the next function to transform the iterator value into a Promise, and then execute the ondepressing function through the promise. then method
  3. 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