preface

Original address source address

Co is a library of functions written by TJ that enables generator automatic execution. Koa also uses it to manage asynchronous flow control and synchronize asynchronous task writing. And get rid of the callback hell problem of all time.

How to use

First of all, we will make some changes according to the official document of CO to see how to use CO, and then carry out the source code analysis step by step (the version of CO analyzed in this article is 4.6.0).

A common type that can be followed by yield

  1. promises
  2. array (parallel execution)
  3. objects (parallel execution)
  4. generator functions (delegation)

promises

let co = require('co')
let genTimeoutFun = (delay) = > {
  return (a)= > {
    return new Promise((resolve, reject) = > {
      setTimeout((a)= > {
        resolve(`delayTime: ${delay}`)
      }, delay)
    })
  }
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)

co(function * () {
  let a = yield timeout1()
  console.log(a) // delayTime: 1000
  let b = yield timeout2()
  console.log(b) // delayTime: 200

  return 'end'
}).then((res) = > {
  console.log(res)
})Copy the code

array

let co = require('co')
let genTimeoutFun = (delay) = > {
  return (a)= > {
    return new Promise((resolve, reject) = > {
      setTimeout((a)= > {
        resolve(`delayTime: ${delay}`)
      }, delay)
    })
  }
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)

co(function * () {
  let a = yield [timeout1(), timeout2()]
  console.log(a) // [ 'delayTime: 1000', 'delayTime: 200' ]
  return 'end'
}).then((res) = > {
  console.log(res) // end
})Copy the code

objects

let co = require('co')
let genTimeoutFun = (delay) = > {
  return (a)= > {
    return new Promise((resolve, reject) = > {
      setTimeout((a)= > {
        resolve(`delayTime: ${delay}`)
      }, delay)
    })
  }
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)

co(function * () {
  let a = yield {
    timeout1: timeout1(),
    timeout2: timeout2()
  }
  console.log(a) // { timeout1: 'delayTime: 1000',timeout2: 'delayTime: 200' }
  return 'end'
}).then((res) = > {
  console.log(res) // end
})Copy the code

generator functions

let co = require('co')
let genTimeoutFun = (delay) = > {
  return (a)= > {
    return new Promise((resolve, reject) = > {
      setTimeout((a)= > {
        resolve(`delayTime: ${delay}`)
      }, delay)
    })
  }
}
let timeout1 = genTimeoutFun(1000)
let timeout2 = genTimeoutFun(200)

function * gen () {
  let a = yield timeout1()
  console.log(a) // delayTime: 1000
  let b = yield timeout2()
  console.log(b) // delayTime: 200
}

co(function * () {
  yield gen()

  return 'end'
}).then((res) = > {
  console.log(res) // end
})Copy the code

Finally, there is the issue of executing the incoming generator function to receive parameters

let co = require('co')

co(function * (name) {
  console.log(name) // qianlongo
}, 'qianlongo')Copy the code

Starting with the second argument to the co function is the argument that can be received by the passed generator function

Start analyzing the source code

You can copy the above code to the local test to see the effect, next we will start to analyze the source of CO step by step

As you can see from the above example, the CO function itself receives a generator function and returns a Promise when the CO executes

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments.1)

  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if(! gen ||typeofgen.next ! = ='function') return resolve(gen);

    // xxx
  });
}Copy the code

Inside the Promise, the gen passed in from the outside is executed, the result of which is returned if it does not have the next attribute (and if it is a function), and the resolve(gen) callback is executed successfully, otherwise a pointer object is returned.

Moving on…


onFulfilled();

/** * @param {Mixed} res * @return {Promise} * @api private */

function onFulfilled(res) {
  var ret;
  try {
    ret = gen.next(res); // Use the generator after gen above to point the pointer to the next location
  } catch (e) {
    return reject(e);
  }
  next(ret); {value: XXX, done: true or false}}
}

/** * @param {Error} err * @return {Promise} * @api private */

function onRejected(err) {
  var ret;
  try {
    ret = gen.throw(err);
  } catch (e) {
    return reject(e);
  }
  next(ret);
}Copy the code

I think onFulfilled and onRejected can be regarded as resolve and reject of the return Promise.

Ondepressing is also a wrapping of the native Generator’s Next method, probably to catch errors (see the internal try catch).

Ok, we see that after moving the pointer to the first position inside CO, we then execute the internal next method, and then focus on that function


function next(ret) {
  // If the internal state of the entire generator function indicates completion, set the Promise state to success and execute resolve
  if (ret.done) return resolve(ret.value);
  // This step converts the value of ret to a Promise form
  var value = toPromise.call(ctx, ret.value);
  // Here is the key, is the CO to realize the call itself, the key to achieve process automation
  // Notice the use of value.then, which means to add successful and failed callbacks to the return value. Ondepressing will be performed within the successful callback, and then the internal next function will be called
  // Doesn't that guarantee that the process will follow exactly the order you wrote it?
  if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
  // Throw an error. Yield can only be followed by the following types specified
  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

Are you smart enough to see how CO automatically manages asynchronous processes

But I have questions about the toPromise function in the next function. What does it actually do? Yield supports arrays, objects, and generator functions in CO.

Step by step


function toPromise(obj) {
  // obj does not exist
  if(! obj)return obj;
  // If obj is already a Promise, it will return directly
  if (isPromise(obj)) return obj;
  // If it is a generator or generator, pass it to co manually as if you were calling co yourself
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  // If obj is neither a Promise, isGeneratorFunction, nor isGenerator, if it is a normal function (subject to the Thunk function specification), wrap the function as a Promise
  if ('function'= =typeof obj) return thunkToPromise.call(this, obj);
  // If it is an array, go to arrayToPromise and wrap it
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}Copy the code

First of all, if obj doesn’t exist, it just returns, you know, co is dependent on the value that the last pointer returned was a Promise or whatever, so if you return at this point

{
  value: false.done: false
}Copy the code

So there’s no need to give a false value and convert it into a Promise.

And then, if OBj itself is a Promise and returns directly, using the internal isPromise function, let’s see how it does that.


function isPromise(obj) {
  return 'function'= =typeof obj.then;
}Copy the code

It’s basically checking whether the then property of obj is a function

Next, if it’s a generator or generator, pass it to CO manually as if you were calling co yourself.

isGeneratorFunction


function isGeneratorFunction(obj) {
  var constructor = obj.constructor;
  if (!constructor) return false;
  if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
  return isGenerator(constructor.prototype);
}Copy the code

Use the constructor attribute to determine whether OBj is a GeneratorFunction. If not, use isGenerator to determine whether OBj’s prototype is a generator

function isGenerator(obj) {
  return 'function'= =typeof obj.next && 'function'= =typeof obj.throw;
}Copy the code

Next is a function, and throw is a function

Let’s move on

If obj is neither a Promise nor isGeneratorFunction nor isGenerator, if obj is an ordinary function, then wrap the function as a Promise. Here we mainly need to look at thunkToPromise

function thunkToPromise(fn) {
  var ctx = this;
  // Wrap thunk as a Promise
  return new Promise(function (resolve, reject) {
      // Execute the thunk function
    fn.call(ctx, function (err, res) { 
      // The thunk callback is passed in err, reject, reject
      if (err) return reject(err); 
      // If there are more than two parameters, consolidate the parameters into an array
      if (arguments.length > 2) res = slice.call(arguments.1);
      // Finally a successful callback is executed
      resolve(res);
    });
  });
}Copy the code

Now, here’s the big deal. What if you handle yield followed by an array in CO? It’s basically the arrayToPromise function

function arrayToPromise(obj) {
  // Use promise. all to repackage multiple Promise instances in obj (you can also fill the array with thunk, generator, etc.) into one. Finally, a new Promise is returned
  return Promise.all(obj.map(toPromise, this));
}Copy the code

One last judgment, what if OBj is an object?

function objectToPromise(obj){
  // Create an object that has the same constructor as the passed object, as does results
  var results = new obj.constructor();
  // get obj's keys
  var keys = Object.keys(obj);
  // Store the Promise property in obj
  var promises = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]);
    // If the result is Promise, use the defer function to modify the results
    if (promise && isPromise(promise)) defer(promise, key);
    // Return as-is if it is not a Promise
    else results[key] = obj[key];
  }
  // Finally use promise. all, add multiple Promise instances in obj
  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) {
      // Assign the result to results after the run succeedsresults[key] = res; })); }}Copy the code

At the end

Here, CO source code analysis is over. Always feel some did not say in place, welcome to clap brick, good night.