Asynchronous application of Generator in ES6

Keywords: Programming Javascript github JSON

1, introduction

Asynchronous programming is too important for the Javascript language. The execution environment of Javascript language is "single thread". If there is no asynchronous programming, it cannot be used at all. It must be stuck. Now I'll focus on how the Generator function performs asynchronous operations.

// Asynchronous programming: callback functions
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});

// Asynchronous programming: Promise
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});

// Asynchronous programming: Generator
function* gen(x) {
  var y = yield x + 2;
  return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

// Encapsulation of asynchronous tasks: asynchronous operations are succinct, but process management is not convenient (i.e. when to perform the first phase and when to perform the second phase)
var fetch = require('node-fetch');
function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){ // Because the Fetch module returns a Promise object, the next next method is called with the then method
  return data.json();
}).then(function(data){
  g.next(data);
});

2. Thunk function of JavaScript language

In JavaScript language, the Thunk function replaces the multi parameter function with a single parameter function that only accepts the callback function as a parameter.

// Normal version of readFile (multiparameter version)
fs.readFile(fileName, callback);
// Read file of Thunk Version (single parameter version)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);

// The Generator function executes automatically, but is not suitable for asynchronous operations. If the previous step must be completed before the next step can be executed, the following automatic execution is not feasible.
function* gen() {
  // ...
}
var g = gen();
var res = g.next();
while(!res.done){
  console.log(res.value);
  res = g.next();
}

// Automatic process management of Thunk function
var fs = require('fs');
function run(generator) {
    var it = generator(go);
    function go(err, result) {
        if (err) return it.throw(err);
        it.next(result);
    }
    go();
}
run(function* (done) {
    var firstFile;
    try {
        var dirFiles = yield fs.readdir('NoNoNoNo', done); // No such dir
        firstFile = dirFiles[0];
    } catch (err) {
        firstFile = null;
    }
    console.log(firstFile);
});

3. co module

Co module is a small tool released by TJ Holowaychuk, a famous programmer, in June 2013 for automatic execution of Generator functions. The co module is actually the combination of two kinds of automatic actuators
(Thunk function and Promise object), wrapped as a module. The precondition for using co is that the yield command of the Generator function can only be followed by the Thunk function or Promise pair
Elephant. If the array or the members of the object are all Promise objects, co can also be used. In this way, when the asynchronous operation has the result, it can return the execution right with the then method.

// The co module allows you not to write the executor of the Generator function. As long as the Generator function passes in the co function, it will automatically execute and return a Promise object
var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};
var gen = function* () {
  var f1 = yield readFile('/etc/fstab');
  var f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
var co = require('co');
co(gen);

// Automatic execution based on Promise object
function getFoo() {
    return new Promise(function (resolve, reject) {
        resolve('foo');
    });
}
function run(generator) {
    var it = generator();
    function go(result) {
        if (result.done) return result.value;
        return result.value.then(function (value) {
            return go(it.next(value));
        }, function (error) {
            return go(it.throw(error));
        });
    }
    go(it.next());
}
run(function* () {
    try {
        var foo = yield getFoo();
        console.log(foo);
    } catch (e) {
        console.log(e);
    }
});

// co source code
var slice = Array.prototype.slice;
module.exports = co['default'] = co.co = co;
// Encapsulate the generator function fn into a function that performs a return of Promise
co.wrap = function (fn) {
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};
// Execute generator function or iterator, return Promise
function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1);
  return new Promise(function (resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);  // Call the generator function to get the traverser
    if (!gen || typeof gen.next !== 'function') return resolve(gen);  // If it is not a traverser, set the Promise state to resolved directly
    // Manual once
    onFulfilled();
    // Successful callback function
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);  // Start traversal for the first time, and give the return value to ret variable
      } catch (e) {
        return reject(e); // Set project state to rejected if execution fails
      }
      next(ret);
      return null;
    }
    // Failed callback function
    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err); // Throw wrong
      } catch (e) {
        return reject(e); // Set the project state to rejected
      }
      next(ret);
    }
    // It is mainly to add onFulfilled and onRejected functions to the callback function in ret.value or its converted Promise
    function next(ret) {
      if (ret.done) return resolve(ret.value); // If the traversal is completed, ret.value is returned
      var value = toPromise.call(ctx, ret.value); // Convert ret.value to Promise
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);  // Add onFulfilled and onRejected to the callback function of Promise
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"')); // If the returned data conversion fails, an error is thrown and the project state is set to rejected
    }
  });
}
// Convert parameter obj to Promise
function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);  // If it's a generator function, co returns Promise
  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);
  return obj;
}
// Convert thunk function to Promise
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);
    });
  });
}
// Convert array to Promise
function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}
// Convert obj to Promise
function objectToPromise(obj) {
  var results = new obj.constructor(); // Get a new obj object results
  var keys = Object.keys(obj);  // Get the keys of the object
  var promises = [];
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]); // Convert the values of an object to a promise object
    if (promise && isPromise(promise)) defer(promise, key); // If the conversion is successful, defer
    else results[key] = obj[key]; // Pass value to new object if conversion fails
  }
  return Promise.all(promises).then(function () { // Merge all promise into a new promise and add a callback function
    return results; // Return new object results
  });
  function defer(promise, key) {
    // predefine the key in the result
    results[key] = undefined;   // Set the corresponding value of the new object results to undefined
    promises.push(promise.then(function (res) { // Add a callback function for the new promise and pass the value into the promises array
      results[key] = res; // Set the corresponding value of the new object results to the original value in the callback function
    }));
  }
}
// Judge whether the parameter obj is a promise object
function isPromise(obj) {
  return 'function' == typeof obj.then;
}
// Determine whether the parameter obj is an iterator
function isGenerator(obj) {
  return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
// Determine whether the parameter obj is a generator function or an iterator generated by the generator
function isGeneratorFunction(obj) {
  var constructor = obj.constructor;  // ƒ GeneratorFunction() { [native code] }
  if (!constructor) return false;
  // The displayName property returns the display name of the function; the name(ES6) property returns the name of a function declaration
  if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
  return isGenerator(constructor.prototype); // Judge whether it is an iterator generated by the generator. constructor.prototype returns the generator object
}
// Judge whether it is a simple object
function isObject(val) {
  return Object == val.constructor;
}

4. Generator application

Generator can pause function execution and return the value of any expression. This feature enables the generator to have a variety of application scenarios.

// Synchronous expression of asynchronous operation
function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}
function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}
var it = main();
it.next();

// Control flow management (only suitable for synchronous operation, asynchronous operation uses Thunk or co)
scheduler(longRunningTask(initialValue));
function scheduler(task) {
  var taskObj = task.next(task.value);
  // If the Generator function does not end, continue calling
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

// Deploy Iterator interface
function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}

// As a data structure (Generator enables data or operations to have an array like interface)
function* doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}
for (task of doStuff()) {
  // task is a function that can be used like a callback function
}
function doStuff() {  // You can use arrays to simulate this use of generators
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

Posted by praeses on Tue, 31 Mar 2020 22:29:28 -0700