Co source code analysis

Thank you for taking me to read the co source code, the harvest is great, want to join the partners look over: announcement · Yuque

The preparatory work

Iterators and generators

About iterators and generators, I read “Deep Understanding OF ES6”, which is very good, if you don’t understand, you can make up for it first, the English version is open source, here.

A quick summary of iterators and generators:

  • The iterator:Iterators are just objects with a specific interface designed for iteration. All iterator objects have a next() method that returns a result object.
  • A generator is A function that returns an iterator.
  • Iterables: Closely related to iterators, an可迭代 is an object with a Symbol.iterator property.

The relevant code

For the sake of source analysis, I pulled the co source and its type definition @types/co down and managed the dependencies using YARN Workspace. I also wrote some of my test code in test/co.spec.ts and test/co.type.spec.d.ts. All source code is available at github.com/upupming/ko… Get the original link at: github.com/upupming/ko… .

Co source code analysis

The latest version of CO is V4.6.0, which was released on July 09, 2015. It has been more than 6 years since then, so it can be seen that it is relatively stable. In other words, with async/await programming, THERE is no need for CO at all. His readme. md also states that for [email protected] “It is a stepping stone towards the async/await proposal.”, the return type replaces “thunk” with a Promise. The type definition @types/co was last updated at 2019-06-05 and is quite old.

I start from the use of methods, and then in-depth source code analysis.

Start with type TS

Before we get familiar with the code, let’s familiarize ourselves with the TS type, and make sure we know what type co is passed in and returned.

After looking at the @types/co source code, it seems that his extraction definition of generator return value types may be slightly inaccurate. I mentioned a PR optimized ExtractType of CO: github.com/DefinitelyT…

However, it is true that JS is a more dynamic language, co logic is more complex, can deal with more cases, but TS to accurately express the input and output type of each case is more troublesome.

ExtractTypeExtract the iterable’s return value

/** * Passes an iterable of type 'I', returns the iterable's final return type * if 'I' is not an iterable: * If it is a function, the return value of the function is returned. * If it is a different type, the return value of I is returned. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-6.html */

type ExtractType<I> = I extends{[Symbol.iterator]: () = > Iterator<any, infer TReturn, any>}? TReturn : Iextends(... args:any[]) = >any ? ReturnType<I> : I

// Note that Generator inherits from Iterator and is essentially an iterable. GeneratorFunction is the type definition of the Generator function
// yield number, return string, next boolean
type A = ExtractType<Generator<number.string.boolean>>
// yield number, no return, next boolean
type B = ExtractType<Generator<number.undefined.boolean>>
// no yield number, return string, next boolean
type C = ExtractType<Generator<undefined.string.boolean>>

export type extractTypeCases = [
  // ExtractType is an extractturn type
  Expect<Equal<A, string>>,
  Expect<Equal<B, undefined>>,
  Expect<Equal<C, string> >,)Copy the code

CoThe incoming generator function returns a Promise

/** * next comes co's own type declaration, which is a function with the generic type F passed to fn. F is a function that returns Iterator, So it's natural to be Generator Function * The passed argument 'args' is of type' F '* returns a Promise, The return value from the Promise is the Generator's 'ExtractType' result returned by 'F' */
// type Co
      
        Iterator
       
        > = (fn: F, ... args: Parameters
        
         ) => Promise
         
          >>
         
        
       
      

type Co<F extends(... args:any[]) => Iterator<any.any.any> > =(fn: F, ... args: Parameters
       ) = > Promise<ExtractType<ReturnType<F>>>

function * d (x: number, y: string, z: boolean) :Generator<boolean.string.number> {
  const ret = yield false
  console.log('a', ret)
  return '1'
}

type D = typeof d
// When passing D to Co, check the return type
type E = ReturnType<Co<D>>

type F = () = > Generator<boolean.undefined.number>
type G = ReturnType<Co<F>>
type H = () = > Generator<undefined.string.number>
type I = ReturnType<Co<H>>
/ / can see final Co function return type is Promise < yield | return >
export type coCases = [
  Expect<Equal<E, Promise<string>>>,
  Expect<Equal<G, Promise<undefined>>>,
  Expect<Equal<I, Promise<string> > >,)/** * Co['wrap']; /** * Co['wrap']
wrap: <F extends (... args: any[]) => Iterator<any, any, any>>(fn: F) => (... args: Parameters<F>) => Promise<ExtractType<ReturnType<F>>>;Copy the code

Start with usage examplescoThe implementation of the

Eg. 1: co(*gen)

Eg.1.1: Common use
Method of use
it('should work as documented'.async() = > {function * gen (a: number, b: string, c: boolean) :Generator<Promise<boolean>, boolean.boolean> {
    expect(a).toEqual(1)
    expect(b).toEqual('2')
    expect(c).toEqual(true)
    const r1 = yield Promise.resolve(false)
    expect(r1).toEqual(false)
    const r2 = yield Promise.resolve(true)
    expect(r2).toEqual(true)
    return r2
  }
  await co(gen, 1.'2'.true)
    .then(function (value) {
      expect(value).toEqual(true)},function (err) {
      console.error(err.stack)
    })
})
Copy the code

The above code is equivalent to:

it('should same as async/await'.async() = > {const fun = async (a: number, b: string, c: boolean): Promise<boolean> => {
    expect(a).toEqual(1)
    expect(b).toEqual('2')
    expect(c).toEqual(true)
    const r1 = await Promise.resolve(false)
    expect(r1).toEqual(false)
    const r2 = await Promise.resolve(true)
    expect(r2).toEqual(true)
    return r2
  }
  await fun(1.'2'.true)
    .then(function (value) {
      expect(value).toEqual(true)},function (err) {
      console.error(err.stack)
    })
})
Copy the code

In short, the syntax for continuously using yield inside a GENERATOR wrapped in CO is the same as for continuously using await inside an async function. Async /await was officially released in 2017 (ES2017 (ES8)). It was still a long time after CO (co was the beginning and Promise hadn’t even come out yet, using the documentation for the “thunk” function, which is a callback function (callback) {… }, see the code pattern for callbacks in the “Asynchronous task executor” section of understanding ES6). Generator function + co + promise = async/await

Co creates an iterator using the passed generator function, and iterates over the iterator, yielding ret each time, Next (ret), which calls the iterator, assigns ret to R1 / R2 in the above sample code (see the sections “Passing parameters to the iterator” and “Passing data to the task executor” in Understanding ES6, the logic is identical to the example in the book). At the end of the iterator (done=true), the resulting value (that is, the return statement in the generator function) is used as the final resolve value of the Promise.

coThe implementation code

The co function is only about 60 lines long and very simple, I added some comments:

function co(gen) {
  // Save this pointer
  var ctx = this;
  // Save all parameters after gen
  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 gen is a function (note typeof Generator function === 'function'), this function is executed
    // It is obvious that for scenarios where gen is a generator function, gen becomes an iterable after assignment and can then interact with yield
    // If gen is a normal function, then gen becomes the return value of the function
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    / /! Gen or Gen.next is not a function (that is, gen is not an iterable), resolve gen directly
    Typeof Gen [symbol.iterator] = typeof gen[Symbol. Iterator] = typeof Gen [Symbol. Iterator] = typeof Gen [Symbol. == 'function', because any iterator returned by the generator (both an iterator (with a 'next' method) and an iterable (with a 'symbol. iterator' property) must have the 'symbol. iterator' property, Refer to https://stackoverflow.com/a/32538867/8242705
    // The TS type definition 'Generator' inherits' Iterator 'and adds a' Symbol. Iterator 'attribute to it
    if(! gen ||typeofgen.next ! = ='function') return resolve(gen);

    // The ondepressing will be forgotten the first time because gen. Next (res) 's first parameter passing is meaningless because no yield has been performed yet. The first next parameter passing will always be ignored by the generator. See the section on passing parameters to iterators in Understanding ES6 for a detailed explanation.
    onFulfilled();

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

    function onFulfilled(res) {
      var ret;
      try {
        // Get the ret from yield
        ret = gen.next(res);
      } catch (e) {
        // If an unexpected error occurs, the outermost Promise is reject
        return reject(e);
      }
      // Yield the value to next. In our example code, ret is a {value: Promise<1>, done: false} for the first time.
      next(ret);
      return null;
    }

    / * * *@param {Error} err
     * @return {Promise}
     * @api private* /
    // is a helper function that calls Gen.throw to throw an error to the generator
    // See the section "Throwing errors in iterators" in understanding ES6
    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        // If an unexpected error occurs, the outermost Promise is reject
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private* /

    function next(ret) {
      If the generator reaches the return statement, the outermost Promise resolve is always the generator's final return value
      if (ret.done) return resolve(ret.value);
      // Ret.val could be a native data type, Promise, array, object, or nesting of these things, with a layer of toPromise conversions
      var value = toPromise.call(ctx, ret.value);
      // This is a pity, onFulfilled after converting to promise
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      // Throw an error to the generator if the conversion does not come out as a Promise indicating that a type not supported by CO was passed
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"')); }}); }// The then function is a promise
function isPromise(obj) {
  return 'function'= =typeof obj.then;
}
Copy the code
Eg. 1.2: Yield Promise array
Method of use

In addition, CO also supports passing in arrays that behave like Promise.all.

co(function * () {
  // resolve multiple promises in parallel
  const a = Promise.resolve(1)
  const b = Promise.resolve(2)
  const c = Promise.resolve(3)
  const res = yield [a, b, c]
  return res
}).then(value= > {
  expect(value).toEqual([1.2.3])})Copy the code

You’ll find that the above code is equivalent to:

const a = Promise.resolve(1)
const b = Promise.resolve(2)
const c = Promise.resolve(3)
Promise.all([a, b, c]).then(value= > {
  expect(value).toEqual([1.2.3])})Copy the code
coThe implementation code

Yield value: toPromise; yield value: toPromise;

function toPromise(obj) {
  if(! obj)return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function'= =typeof obj) return thunkToPromise.call(this, obj);
  // arrayToPromise is called
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}
For each element in the array, call toPromise recursively. This recursion is too beautiful, so it can support arrays, objects nesting, can be used for reference later
function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}
Copy the code
Eg. 1.3: Error handling
Method of use

The Promise error is also thrown directly at the Generator function itself.

co(function * () {
  try {
    yield Promise.reject(new Error('boom'))}catch (err) {
    expect(err.message).toEqual('boom')}})Copy the code

This is basically the same as the try-catch syntax for async/await:

try {
  await Promise.reject(new Error('boom'))}catch (err) {
  expect(err.message).toEqual('boom')}Copy the code
coThe implementation code

This is actually implemented in places where gen.throw is called:

function onRejected(err) {
  var ret;
  try {
    // onRejected encapsulates gen.throw
    ret = gen.throw(err);
  } catch (e) {
    return reject(e);
  }
  next(ret);
}
function next(ret) {
    // ...
    // 如果 yield 出来的 promise reject 了的话,调用 onRejected
    if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
    // Call onRejected when the yield type is incorrect
    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
Eg. 1.4: Object handling
Method of use

The case where all values on an object are promises can be executed asynchronously, eventually returning a value that is the value after Promise resolve.

co(function * () {
  const res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2)}return res
}).then(res= > {
  expect(res).toEqual({ 1: 1.2: 2})})Copy the code

This is also easy to implement with promise. all and object. fromEntries:

const obj = {
  1: Promise.resolve(1),
  2: Promise.resolve(2)}/ / here takes advantage of the Object. The keys and Object. The values in order to maintain consistent features: https://stackoverflow.com/a/52706191/8242705
const tmp = await Promise.all(Object.values(obj))
const res = Object.fromEntries(Object.keys(obj).map((key, idx) = > [key, tmp[idx]]))
expect(res).toEqual({ 1: 1.2: 2 })
Copy the code
coThe implementation code

Similar to passing arrays, the code is mostly about how toPromise handles data of object types. Instead of using the method I described above, he uses a combination of Object.keys and promise. all and closures.

function toPromise(obj) {
  if(! obj)return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function'= =typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  // Here objectToPromise is called
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}
function objectToPromise(obj){
  var results = new obj.constructor();
  var keys = Object.keys(obj);
  // Save a promises array by keys, then pass it to promise. all async
  var promises = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    // obj[key] returns toPromise to obj[key]
    var promise = toPromise.call(this, obj[key]);
    // If it is a Promise, place the result in the results object after resolve
    if (promise && isPromise(promise)) defer(promise, key);
    // Otherwise, place the results object directly
    else results[key] = obj[key];
  }
  return Promise.all(promises).then(function () {
    return results;
  });

  // Wait for the promise resolve to place its result in results[key]
  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

Eg.2: co.wrap(*gen)

The co. Wrap is a simple wrapper around the co itself. Instead of passing parameters to a generator function, the co is given a function that can be executed multiple times, passing parameters at each time.

const fn = co.wrap(function * (val) {
  return yield Promise.resolve(val)
})

fn(true).then(function (val) {
  expect(val).toEqual(true)})Copy the code

An async function can itself be called multiple times with different arguments, so wrap should not need the equivalent form under Async.

const fn = async (val): Promise<any> = > {return await Promise.resolve(val)
}
fn(true).then(function (val) {
  expect(val).toEqual(true)})Copy the code

Returns a function. Each call to this function corresponds to a call to co(fn, arguments) :

co.wrap = function (fn) {
  return createPromise() {
    return co.call(this, fn.apply(this.arguments)); }};Copy the code

conclusion

The traditional asynchronous model based on callback functions is poorly written and prone to callback hell, where CO is a syntactic sugar and, as the author says, “It is a stepping stone towards the async/await proposal.” Co uses yield and next in generator functions to make asynchronous code very natural, and while Promise and async/await have been added to the ES standard for many years now, CO has become less and less needed. However, we can see that async/await can be polyfilled with generator functions.

The main tool functions in CO are mainly recursive toPromise implementation, which can carry out recursive transformation of nested objects, which can be used for reference in the future if necessary.

All test cases and code are located at github.com/upupming/ko… You can clone co. Spec. ts and Co. Type.spec.d.ts at github.com/upupming/ko…

Thank you again for taking me to read the co source code, the harvest is great, want to join the partners look over: announcement · Yuque