Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Asynchronous application of the ES6 Generator function


May 08, 2021 ES6


Table of contents


Traditional methods

Before ES6 was born, there were probably four ways to program asynchronously.

  • Callback function
  • Event monitoring
  • Publish/subscribe
  • The Promise object

The Generator function takes JavaScript asynchronous programming to a whole new stage.

Basic concepts

Asynchronous

The "异步" simply said that a task is not completed continuously, can be understood as the task is man-made into two paragraphs, first to perform the first paragraph, and then to carry out other tasks, and so on ready, and then go back to the second paragraph.

For example, there is a task that reads files for processing, and the first segment of a task is to make a request to the operating system to read the files. T he program then performs other tasks, waiting for the operating system to return the file, followed by the second paragraph of the task (working with the file). This unseth consecutive execution is called asynchronous.

Accordingly, continuous execution is called synchronization. Because it is continuously executed and no other tasks can be inserted, the program can only wait while the operating system reads files from the hard disk.

Callback function

The JavaScript language's implementation of asynchronous programming is 回调函数 T he so-called callback function is to write the second paragraph of the task in a single function, and when the task is re-executed, the function is called directly. Callback, the English name of the callback function, translates as "重新调用"

Read the file for processing, as written.

  1. fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  2. if (err) throw err;
  3. console.log(data);
  4. });

In the code above, the third argument of the readFile function is the callback function, which is the second paragraph of the task. The callback function does not execute until the operating system returns the /etc/passwd file.

An interesting question is, why does the Node convention, the first argument of the callback function, have to be the wrong object err (if there is no error, the argument is null)?

The reason is that the execution is divided into two segments, and after the first paragraph is completed, the context in which the task is located is over. After this throw error, the original context has been unable to catch, can only be used as an argument, passed into the second paragraph.

Promise

The callback function itself is not a problem, its problem occurs in the nesting of multiple callback functions. Suppose you read the A file and then read the B file, as follows.

  1. fs.readFile(fileA, 'utf-8', function (err, data) {
  2. fs.readFile(fileB, 'utf-8', function (err, data) {
  3. // ...
  4. });
  5. });

It's not hard to imagine multiple nesting if you read more than two files in turn. C ode does not develop vertically, but horizontally, and can quickly become a mess that cannot be managed. B ecause multiple asynchronous operations form a strong coupling, as long as there is an operation that needs to be modified, its upper callback function and lower callback function may have to be modified. This is called callback hell.

The Promise object was proposed to solve this problem. I t is not a new syntax feature, but a new way of writing that allows callback functions to be nested into chain calls. With Promise, multiple files are read in a row and written as follows.

  1. var readFile = require('fs-readfile-promise');
  2. readFile(fileA)
  3. .then(function (data) {
  4. console.log(data.toString());
  5. })
  6. .then(function () {
  7. return readFile(fileB);
  8. })
  9. .then(function (data) {
  10. console.log(data.toString());
  11. })
  12. .catch(function (err) {
  13. console.log(err);
  14. });

In the code above, I used the fs-readfile-promise module, which is designed to return a promise version of the readFile function. Promise provides the then method load callback function, which catches errors thrown during execution.

As you can see, Promise's writing is only an improvement on the callback function, and with the then method, the two-segment execution of the asynchronous task is more clear, and nothing new.

Promise's biggest problem is code redundancy, the original task is wrapped by Promise, no matter what the operation, at a glance is a bunch of then, the original semantics become very unclear.

So, is there a better way to write it?

3. Generator function

Co-program

Traditional programming languages have long had asynchronous programming solutions (in fact, multitaste solutions). One of these is "协程" which means that multiple threads work together to accomplish asynchronous tasks.

Co-programs are a 函数 functions, and they're a 线程 It runs roughly as follows.

  • The first step, Co-equation A, begins.
  • In the second step, Co-Program A executes to halfway through, goes into a pause, and the executive power is transferred to Co-Program B.
  • The third step, (after a period of time) co-program B returns the executive power.
  • Step 4, Co-Equation A resumes execution.

Co-equation A of the above process is an asynchronous task because it is performed in two (or more) segments.

For example, the co-writing of a read file is as follows.

  1. function* asyncJob() {
  2. // ...其他代码
  3. var f = yield readFile(fileA);
  4. // ...其他代码
  5. }

The function of the above code, asyncJob, is a co-program in which the mystery lies in the yield command. I t indicates that execution is here and that the executive power will be handed over to other co-programs. That is, the yield command is the asynchronous two-stage boundary.

The co-program is paused when it encounters the yield command, waits until the execution returns, and then continues from where it was suspended. Its greatest advantage is that the code is written very much like a synchronization operation, and if you remove the yield command, it's exactly the same.

The generator function implementation of the co-program

The Generator function is the implementation of the co-program in ES6, the biggest feature of which is that the execution of the function can be surrendered (i.e., suspended execution).

The Generator function is an encapsulated asynchronous task, or a container for an asynchronous task. W here asynchronous operations need to be paused, they are yield the yield statement. The Generator function is executed as follows.

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

In the above code, calling the Generator function returns an internal pointer (i.e. the traverser) g. T his is another place where the Generator function differs from the normal function in that executing it does not return results, returning pointer objects. Calling the next method of pointer g moves the internal pointer (that is, the first paragraph of performing an asynchronous task) to the first yield statement encountered, in the example of execution until x plus 2.

In other words, the next method is designed to perform the Generator function in stages. E ach time the next method is called, an object is returned that represents the information for the current stage (value property and done property). The value property is the value of the expression after the yield statement, which represents the value of the current stage, and the done property is a Boolean value that indicates whether the Generator function is executed or not, i.e. whether there is another stage.

Data exchange and error handling of generator functions

The Generator function can pause and resume execution, which is the root cause of the asynchronous task it encapsulates. In addition, it has two features that make it a complete solution for asynchronous programming: data exchange and error handling mechanisms inside and outside the function.

Next returns the value property of the value, which is the Generator function outputs data outwards, and the next method can also accept parameters and enter data into the Generator function body.

  1. function* gen(x){
  2. var y = yield x + 2;
  3. return y;
  4. }
  5. var g = gen(1);
  6. g.next() // { value: 3, done: false }
  7. g.next(2) // { value: 2, done: true }

In the above code, the value property of the first next method returns the value 3 of the expression x plus 2. T he second next method has argument 2, which can be passed into the Generator function and received by the variable y inside the function as a return result of the asynchronous task of the previous stage. Therefore, the value property of this step returns 2 (the value of variable y).

The Generator function can also deploy error handling code within to catch errors thrown outside the function.

  1. function* gen(x){
  2. try {
  3. var y = yield x + 2;
  4. } catch (e){
  5. console.log(e);
  6. }
  7. return y;
  8. }
  9. var g = gen(1);
  10. g.next();
  11. g.throw('出错了');
  12. // 出错了

On the last line of the above code, outside the Generator function, an error thrown using the throw method of the pointer object can be called try inside the function... C atch block capture. This means that the error code and the code that handles the error achieve a time and space separation, which is undoubtedly important for asynchronous programming.

Encapsulation of asynchronous tasks

Here's a look at how to use the Generator function to perform a real asynchronous task.

  1. var fetch = require('node-fetch');
  2. function* gen(){
  3. var url = 'https://api.github.com/users/github';
  4. var result = yield fetch(url);
  5. console.log(result.bio);
  6. }

In the code above, the Generator function encapsulates an asynchronous operation that reads a remote interface and then parses information from data in JSON format. As mentioned earlier, this code is very much like a synchronization operation, except for the yield command.

Here's how to execute this code.

  1. var g = gen();
  2. var result = g.next();
  3. result.value.then(function(data){
  4. return data.json();
  5. }).then(function(data){
  6. g.next(data);
  7. });

In the above code, first execute the Generator function, get the traverser object, and then use the next method (second line) to perform the first stage of the asynchronous task. Because the Fetch module returns a Promise object, call the next next method with the then method.

As you can see, while the Generator function represents asynchronous operations succinctly, process management is inconvenient (that is, when to perform the first phase and when to perform the second phase).

4. Thunk function

The Thunk function is one way to automate the Generator function.

The value policy for the argument

The Thunk function was born as early as the 1960s.

At that time, programming language is just beginning, computer scientists are still studying, compiler how to write is better. One point of contention is "求值策略" which is when the parameters of the function should be valued.

  1. var x = 1;
  2. function f(m) {
  3. return m * 2;
  4. }
  5. f(x + 5)

The code above defines the function f and then passs the expression x plus 5 to it. Excuse me, when should this expression be evaluated?

One opinion is "call by "传值调用" which is to calculate the value of x plus 5 (equal to 6) before entering the function body, and then pass that value into function f. This strategy is used in the C language.

  1. f(x + 5)
  2. // 传值调用时,等同于
  3. f(6)

Another opinion is the "call by “传名调用” which directly incoming the expression x plus 5 into the function body and evaluating it only when it is used. The Haskell language adopts this strategy.

  1. f(x + 5)
  2. // 传名调用时,等同于
  3. (x + 5) * 2

Which is better for pass-through calls and name-calling calls?

The answer is both pros and cons. The value call is relatively simple, but when the parameter is valued, it is not actually used, which may result in performance loss.

  1. function f(a, b){
  2. return b;
  3. }
  4. f(3 * x * x - 2 * x - 1, x);

In the code above, the first argument to function f is a complex expression, but it is not used at all in the function body. I t is actually unnecessary to value this parameter. As a result, some computer scientists tend to "name-calling", which is to value only at the time of execution.

The meaning of the Thunk function

The “传名调用” implementation often places the argument in a temporary function and then passes the temporary function into the function body. This temporary function is called Thunk function.

  1. function f(m) {
  2. return m * 2;
  3. }
  4. f(x + 5);
  5. // 等同于
  6. var thunk = function () {
  7. return x + 5;
  8. };
  9. function f(thunk) {
  10. return thunk() * 2;
  11. }

In the code above, the parameter x plus 5 of function f is replaced by a function. Wherever the original parameters are used, the Thunk function can be valued.

This is the definition of the Thunk function, which is an implementation strategy for "name-calling" to replace an expression.

The Thunk function of the JavaScript language

The JavaScript language is a pass-through call, and its Thunk function has a different meaning. In the JavaScript language, the Thunk function replaces not an expression, but a multi-argument function, replacing it with a single-argument function that accepts callback functions only as arguments.

  1. // 正常版本的readFile(多参数版本)
  2. fs.readFile(fileName, callback);
  3. // Thunk版本的readFile(单参数版本)
  4. var Thunk = function (fileName) {
  5. return function (callback) {
  6. return fs.readFile(fileName, callback);
  7. };
  8. };
  9. var readFileThunk = Thunk(fileName);
  10. readFileThunk(callback);

In the above code, the readFile method of the fs module is a multi-parameter function with two parameters, the file name and the callback function, respectively. A fter the converter processes, it becomes a single-argument function, accepting only callback functions as arguments. This single-argument version is called the Thunk function.

Any function, as long as the argument has a callback function, can be written as a Thunk function. Here's a simple Thunk function converter.

  1. // ES5版本
  2. var Thunk = function(fn){
  3. return function (){
  4. var args = Array.prototype.slice.call(arguments);
  5. return function (callback){
  6. args.push(callback);
  7. return fn.apply(this, args);
  8. }
  9. };
  10. };
  11. // ES6版本
  12. const Thunk = function(fn) {
  13. return function (...args) {
  14. return function (callback) {
  15. return fn.call(this, ...args, callback);
  16. }
  17. };
  18. };

Use the converter above to generate the Thunk function of fs.readFile.

  1. var readFileThunk = Thunk(fs.readFile);
  2. readFileThunk(fileA)(callback);

Here's another complete example.

  1. function f(a, cb) {
  2. cb(a);
  3. }
  4. const ft = Thunk(f);
  5. ft(1)(console.log) // 1

Thunkify module

A converter for the production environment, using the Thunkify module.

The first is the installation.

  1. $ npm install thunkify

Here's how to use it.

  1. var thunkify = require('thunkify');
  2. var fs = require('fs');
  3. var read = thunkify(fs.readFile);
  4. read('package.json')(function(err, str){
  5. // ...
  6. });

Thunkify's source code is very similar to the simple converter in the last section.

  1. function thunkify(fn) {
  2. return function() {
  3. var args = new Array(arguments.length);
  4. var ctx = this;
  5. for (var i = 0; i < args.length; ++i) {
  6. args[i] = arguments[i];
  7. }
  8. return function (done) {
  9. var called;
  10. args.push(function () {
  11. if (called) return;
  12. called = true;
  13. done.apply(null, arguments);
  14. });
  15. try {
  16. fn.apply(ctx, args);
  17. } catch (err) {
  18. done(err);
  19. }
  20. }
  21. }
  22. };

Its source code is primarily an inspector, and the variable called ensures that the callback function runs only once. T his design is related to the Generator function below. Take a look at the example below.

  1. function f(a, b, callback){
  2. var sum = a + b;
  3. callback(sum);
  4. callback(sum);
  5. }
  6. var ft = thunkify(f);
  7. var print = console.log.bind(console);
  8. ft(1, 2)(print);
  9. // 3

In the code above, because thunkify allows callback functions to execute only once, only one line of results is output.

Process management of generator functions

You might ask, what's the use of the Thunk function? The answer is that it was really useless before, but with the Generator function in ES6, the Thunk function can now be used for automatic process management of the Generator 自动流程管理

Generator functions can be executed automatically.

  1. function* gen() {
  2. // ...
  3. }
  4. var g = gen();
  5. var res = g.next();
  6. while(!res.done){
  7. console.log(res.value);
  8. res = g.next();
  9. }

In the code above, the Generator function gen automatically completes all the steps.

However, this is not suitable for asynchronous operations. I f the previous step must be guaranteed to be completed before the next step can be performed, the above automatic execution is not feasible. A t this point, the Thunk function can be useful. T ake reading a file, for example. The following Generator function encapsulates two asynchronous operations.

  1. var fs = require('fs');
  2. var thunkify = require('thunkify');
  3. var readFileThunk = thunkify(fs.readFile);
  4. var gen = function* (){
  5. var r1 = yield readFileThunk('/etc/fstab');
  6. console.log(r1.toString());
  7. var r2 = yield readFileThunk('/etc/shells');
  8. console.log(r2.toString());
  9. };

In the code above, the yield command is used to move the execution of a program out of the Generator function, and a method is required to return the execution to the Generator function.

This method is the Thunk function because it can return execution to the Generator function in the callback function. For ease of understanding, let's first look at how to perform this Generator function above manually.

  1. var g = gen();
  2. var r1 = g.next();
  3. r1.value(function (err, data) {
  4. if (err) throw err;
  5. var r2 = g.next(data);
  6. r2.value(function (err, data) {
  7. if (err) throw err;
  8. g.next(data);
  9. });
  10. });

In the code above, the variable g is an internal pointer to the Generator function, indicating which step is currently being performed. The next method is responsible for moving the pointer to the next step and returning the information for that step (value property and done property).

A closer look at the code above reveals that the Generator function executes by actually passing the value property of the next method over and over again to the same callback function. This allows us to automate the process with recursive returns.

Automatic process management of the Thunk function

The real power of the Thunk function is that it 自动执行 the Generator function. Below is a Generator executor based on the Thunk function.

  1. function run(fn) {
  2. var gen = fn();
  3. function next(err, data) {
  4. var result = gen.next(data);
  5. if (result.done) return;
  6. result.value(next);
  7. }
  8. next();
  9. }
  10. function* g() {
  11. // ...
  12. }
  13. run(g);

The run function in the code above is an auto-executor of the Generator function. T he internal next function is Thunk's callback function. The next function first moves the pointer to the next step of the Generator function (gen.next method) and then determines whether the Generator function ends (result.done property), and if it does not end, the next function is passed in to the Thunk function (result.value property) or it exits directly.

With this executor, it's much easier to execute Generator functions. R egardless of how many asynchronous operations are inside, pass the Generator function directly into the run function. Of course, the premise is that every asynchronous operation must be a Thunk function, that is, the yield command must be followed by a Thunk function.

  1. var g = function* (){
  2. var f1 = yield readFileThunk('fileA');
  3. var f2 = yield readFileThunk('fileB');
  4. // ...
  5. var fn = yield readFileThunk('fileN');
  6. };
  7. run(g);

In the code above, function g encapsulates n asynchronous read file operations, which are done automatically whenever the run function is executed. In this way, asynchronous operations can be written not only like synchronization operations, but also a line of code can be executed.

The Thunk function is not the only scenario in which the Generator function is automated. B ecause the key to automated execution is that there must be a mechanism that automatically controls the flow of generator functions, receiving and returning the execution rights of the program. Callback functions can do this, and promise objects can do it.

5. co module

Basic usage

The co module is a gadget released in June 2013 by renowned programmer TJ Holowaychuk for the automatic execution of Generator functions.

The following is a Generator function that reads two files in turn.

  1. var gen = function* () {
  2. var f1 = yield readFile('/etc/fstab');
  3. var f2 = yield readFile('/etc/shells');
  4. console.log(f1.toString());
  5. console.log(f2.toString());
  6. };

The co module allows you to write an executor without writing generator functions.

  1. var co = require('co');
  2. co(gen);

In the code above, the Generator function is executed automatically whenever it is passed in to the co function.

The co function returns a Promise object, so you can add callback functions using the then method.

  1. co(gen).then(function (){
  2. console.log('Generator 函数执行完成');
  3. });

In the above code, a line of prompts is output until the execution of the Generator function is over.

The principle of the co module

Why can co automate generator functions?

As mentioned earlier, Generator is a container for asynchronous operations. Its automatic execution requires a mechanism that automatically returns execution rights when asynchronous operations have results.

Two methods can do this.

(1) Callback function. Wrap the asynchronous operation as a Thunk function and hand back execution rights within the callback function.

(2) Promise object. Wrap asynchronous operations as Promise objects and return execution rights using the then method.

The co module is really wrapping two auto-executors (Thunk functions and Promise objects) into one module. T he prerequisite for using co is that the yield command of the Generator function can only be followed by the Thunk function or the Promise object. If the members of an array or object are all Promise objects, you can also use co, see the example below.

The Thunk function-based auto-executors were described in the last section. B elow, the auto-executor based on the Promise object. This is necessary to understand the co module.

Auto-execution based on a Promise object

Or follow the example above. First, wrap the readFile method of the fs module into a Promise object.

  1. var fs = require('fs');
  2. var readFile = function (fileName){
  3. return new Promise(function (resolve, reject){
  4. fs.readFile(fileName, function(error, data){
  5. if (error) return reject(error);
  6. resolve(data);
  7. });
  8. });
  9. };
  10. var gen = function* (){
  11. var f1 = yield readFile('/etc/fstab');
  12. var f2 = yield readFile('/etc/shells');
  13. console.log(f1.toString());
  14. console.log(f2.toString());
  15. };

Then, manually execute the Generator function above.

  1. var g = gen();
  2. g.next().value.then(function(data){
  3. g.next(data).value.then(function(data){
  4. g.next(data);
  5. });
  6. });

Manual execution is actually using the then method, layer by layer to add callback functions. With this in common, you can write out an auto-executor.

  1. function run(gen){
  2. var g = gen();
  3. function next(data){
  4. var result = g.next(data);
  5. if (result.done) return result.value;
  6. result.value.then(function(data){
  7. next(data);
  8. });
  9. }
  10. next();
  11. }
  12. run(gen);

In the above code, the next function calls itself as long as the Generator function has not yet been executed to the last step, thus implementing automatic execution.

The source code for the co module

co is an extension of the auto-executor above, with only a few dozen lines of source code, which is very simple.

First, the co function accepts the Generator function as an argument and returns a Promise object.

  1. function co(gen) {
  2. var ctx = this;
  3. return new Promise(function(resolve, reject) {
  4. });
  5. }

Inside the returned Promise object, co first checks whether the parameter gen is a Generator function. If so, execute the function to get an internal pointer object, if not return, and change the state of the Promise object to resolved.

  1. function co(gen) {
  2. var ctx = this;
  3. return new Promise(function(resolve, reject) {
  4. if (typeof gen === 'function') gen = gen.call(ctx);
  5. if (!gen || typeof gen.next !== 'function') return resolve(gen);
  6. });
  7. }

Next, co wraps the next method of the generator function's internal pointer object into an onFulfilled function. This is primarily to be able to catch thrown errors.

  1. function co(gen) {
  2. var ctx = this;
  3. return new Promise(function(resolve, reject) {
  4. if (typeof gen === 'function') gen = gen.call(ctx);
  5. if (!gen || typeof gen.next !== 'function') return resolve(gen);
  6. onFulfilled();
  7. function onFulfilled(res) {
  8. var ret;
  9. try {
  10. ret = gen.next(res);
  11. } catch (e) {
  12. return reject(e);
  13. }
  14. next(ret);
  15. }
  16. });
  17. }

Finally, the key next function, which calls itself repeatedly.

  1. function next(ret) {
  2. if (ret.done) return resolve(ret.value);
  3. var value = toPromise.call(ctx, ret.value);
  4. if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
  5. return onRejected(
  6. new TypeError(
  7. 'You may only yield a function, promise, generator, array, or object, '
  8. + 'but the following object was passed: "'
  9. + String(ret.value)
  10. + '"'
  11. )
  12. );
  13. }

In the code above, the internal code of the next function has only four lines of commands.

On the first line, check to see if this is the last step of the Generator function and return if so.

The second line, making sure that the return value for each step, is the Promise object.

On the third line, using the then method, add a callback function to the return value, and then call the next function again through the onFulfilled function.

The fourth line terminates execution by changing the state of the Promise object to rejected if the parameters do not meet the requirements (parameters are not Thunk functions and Promise objects).

Handles asynchronous operations that are not the same

co 并发 that allow certain operations to take place at the same time until they are all complete before taking the next step.

At this point, place the same operation inside the array or object, followed by the yield statement.

  1. // 数组的写法
  2. co(function* () {
  3. var res = yield [
  4. Promise.resolve(1),
  5. Promise.resolve(2)
  6. ];
  7. console.log(res);
  8. }).catch(onerror);
  9. // 对象的写法
  10. co(function* () {
  11. var res = yield {
  12. 1: Promise.resolve(1),
  13. 2: Promise.resolve(2),
  14. };
  15. console.log(res);
  16. }).catch(onerror);

Here's another example.

  1. co(function* () {
  2. var values = [n1, n2, n3];
  3. yield values.map(somethingAsync);
  4. });
  5. function* somethingAsync(x) {
  6. // do something async
  7. return y
  8. }

The above code allows three somethingAsync asynchronous operations to be performed at the same time until they are all complete before going to the next step.

Example: ProcessIng Stream

Node Stream mode read and write data, characterized by processing only a portion of the data at once, which is processed in “数据流” stream". T his is great for working with large-scale data. Stream mode uses the EventEmitter API to release three events.

  • Data event: The next block of data is ready.
  • End event: The entire "data stream" is finished.
  • Error event: An error occurred.

Using Promise.race() you can determine which of these three events occurs first, and only enters the processing of the next block of data when the data event occurs first. Thus, we can read all the data through a while loop.

  1. const co = require('co');
  2. const fs = require('fs');
  3. const stream = fs.createReadStream('./les_miserables.txt');
  4. let valjeanCount = 0;
  5. co(function*() {
  6. while(true) {
  7. const res = yield Promise.race([
  8. new Promise(resolve => stream.once('data', resolve)),
  9. new Promise(resolve => stream.once('end', resolve)),
  10. new Promise((resolve, reject) => stream.once('error', reject))
  11. ]);
  12. if (!res) {
  13. break;
  14. }
  15. stream.removeAllListeners('data');
  16. stream.removeAllListeners('end');
  17. stream.removeAllListeners('error');
  18. valjeanCount += (res.toString().match(/valjean/ig) || []).length;
  19. }
  20. console.log('count:', valjeanCount); // count: 1120
  21. });

The above code reads Les Miserables text files in Stream mode, uses the stream.once method for each block, and adds a one-time callback function to the data, end, and error events. The variable res has a value only when the data event occurs, and then adds up the number of times the word valjean appears in each block of data.