The meaning and usage of Generator function
This article is the first in a series of articles “In-depth mastery of ECMAScript 6 asynchronous programming”.
- The meaning and usage of Generator function
- The meaning and usage of Thunk function
- The meaning and usage of co function library
- The meaning and usage of async function
Asynchronous programming is too important for the JavaScript language. JavaScript has only one thread. If there is no asynchronous programming, it is useless at all and it has to be stuck.
In the past, there were about four methods for asynchronous programming .
- Callback
- Event monitoring
- Publish/Subscribe
- Promise object
ECMAScript 6 (ES6 for short), as the next-generation JavaScript language, brings JavaScript asynchronous programming into a whole new stage. The theme of this series of articles is to introduce more powerful and complete ES6 asynchronous programming methods.
The new method is relatively abstract. When I was a beginner, I often felt puzzled. I didn’t figure it out until a long time later. The grammatical goal of asynchronous programming is how to make it more like synchronous programming. This series of articles will help you understand the essence of JavaScript asynchronous programming. All the content to be talked about has been realized. In other words, it can be used immediately, and a slogan is applied, which means “the future is here”.
1. What is asynchronous?
The so-called “asynchronous” simply means that a task is divided into two segments. The first segment is executed first, and then other tasks are executed. When you are ready, you can go back and execute the second segment. For example, a task is to read a file for processing, and the asynchronous execution process is as follows.
In the above figure, the first stage of the task is to send a request to the operating system to read the file. Then, the program performs other tasks, waits until the operating system returns the file, and then executes the second stage of the task (processing the file).
This kind of discontinuous execution is called asynchronous. Correspondingly, continuous execution is called synchronization.
The above figure is the synchronous execution method. Because it is executed continuously and other tasks cannot be inserted, the program can only wait while the operating system reads files from the hard disk.
Second, the concept of callback function
The realization of asynchronous programming in JavaScript language is the callback function. The so-called callback function is to write the second paragraph of the task separately in a function, and call this function directly when the task is executed again. Its English name callback, literally translated is “recall”.
Read the file for processing, it is written like this.
fs.readFile('/etc/passwd', function (err, data) { if (err) throw err; console.log(data); });
In the above code, the second parameter of the readFile function is the callback function, which is the second paragraph of the task. The callback function will not be executed until the operating system returns the /etc/passwd file.
An interesting question is, why does Node.js agree that the first parameter of the callback function must be the error object err (if there is no error, the parameter is null)? The reason is that the execution is divided into two sections. Errors thrown between these two sections cannot be captured by the program and can only be used as parameters and passed to the second section.
Three, Promise
There is no problem with the callback function itself. Its problem arises in the nesting of multiple callback functions. Suppose that after reading the A file, then reading the B file, the code is as follows.
fs.readFile(fileA, function (err, data) { fs.readFile(fileB, function (err, data) { // ... }); });
It is not difficult to imagine that if multiple files are read in sequence, multiple nesting will occur. The code is not developed vertically, but horizontally, and it will soon become messy and unmanageable. This situation is called ” callback hell ” (callback hell).
Promise is proposed to solve this problem. It is not a new grammatical function, but a new way of writing that allows the horizontal loading of the callback function to be changed to vertical loading. Use Promise to read multiple files continuously, written as follows.
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); });
In the above code, I used the fs-readfile-promise module, and its function is to return a Promise version of the readFile function. Promise provides the then method to load the callback function, and the catch method captures errors thrown during execution.
It can be seen that the writing of Promise is just an improvement of the callback function. After using the then method, the two-stage execution of the asynchronous task can be seen more clearly. Other than that, there is nothing new.
The biggest problem with Promise is code redundancy. The original task is wrapped by Promise. No matter what the operation is, it is a pile of then at first glance, and the original semantics becomes very unclear.
So, is there a better way to write it?
Fourth, the coroutine
Traditional programming languages have long had solutions for asynchronous programming (in fact, they are multitasking solutions). One of them is called ” coroutine”, which means that multiple threads cooperate with each other to complete asynchronous tasks.
A coroutine is a bit like a function, but also a bit like a thread. Its running process is roughly as follows.
In the first step, the coroutine A starts to execute.
In the second step, the execution of coroutine A is halfway through, enters a pause, and the execution right is transferred to coroutine B.
The third step, (after a period of time), the coroutine B returns the right of execution.
In the fourth step, the coroutine A resumes execution.
The coroutine A of the above process is an asynchronous task because it is executed in two (or more) stages.
For example, the way to read the file is as follows.
function asnycJob() { // ...其他代码 var f = yield readFile(fileA); // ...其他代码 }
The function asyncJob in the above code is a coroutine, and its secret lies in the yield command. It means that execution ends here, and execution rights will be handed over to other coroutines. In other words, the yield command is the dividing line between the two phases of asynchronous.
The coroutine suspends when it encounters the yield command, waits until the execution right returns, and then continues execution from the place where it was suspended. Its biggest advantage is that the code is written very much like a synchronous operation, if the yield command is removed, it will be exactly the same.
Five, the concept of Generator function
The Generator function is the implementation of the coroutine in ES6. The biggest feature is that it can hand over the execution rights of the function (that is, suspend execution).
function* gen(x){ var y = yield x + 2; return y; }
The above code is a Generator function. It is different from ordinary functions in that execution can be suspended, so an asterisk must be added before the function name to show the difference.
The entire Generator function is an encapsulated asynchronous task, or a container for asynchronous tasks. Where asynchronous operations need to be suspended, use the yield statement to indicate. The execution method of the Generator function is as follows.
var g = gen(1); g.next() // { value: 3, done: false } g.next() // { value: undefined, done: true }
In the above code, calling the Generator function will return an internal pointer (ie iterator ) g. This is another difference between the Generator function and the normal function, that is, executing it will not return a result, it will return a pointer object. Calling the next method of the pointer g will move the internal pointer (that is, the first segment of the asynchronous task) to point to the first yield statement encountered. The above example is executed until x + 2 is reached.
In other words, the role of the next method is to execute the Generator function in stages. Each time the next method is called, an object will be returned, representing the current stage information (value attribute and done attribute). The value attribute is the value of the expression after the yield statement, which represents the value of the current stage; the done attribute is a boolean value, which indicates whether the Generator function has been executed, that is, whether there is another stage.
6. Data exchange and error handling of the Generator function
The Generator function can suspend execution and resume execution, which is the fundamental reason why it can encapsulate asynchronous tasks. In addition, it has two features that make it a complete solution for asynchronous programming: data exchange inside and outside the function body and error handling mechanism.
The value attribute of the return value of the next method is the generator function to output data; the next method can also accept parameters, which is to input data into the generator function body.
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true }
In the above code, the value attribute of the first next method returns the value of the expression x + 2 (3). The second next method takes parameter 2. This parameter can be passed into the Generator function, as the return result of the asynchronous task of the previous stage, and received by the variable y in the function body. Therefore, the value attribute of this step returns 2 (the value of variable y).
Error handling code can also be deployed inside the Generator function to capture errors thrown outside the function.
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); g.next(); g.throw('出错了'); // 出错了
In the last line of the above code, outside the Generator function, errors thrown by the throw method of the pointer object can be caught by the try…catch code block in the function body. This means that the error code and the error-handling code are separated in time and space, which is undoubtedly very important for asynchronous programming.
Seven, the usage of Generator function
Let’s take a look at how to use the Generator function to perform a real asynchronous task.
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); }
In the above code, the Generator function encapsulates an asynchronous operation, which first reads a remote interface, and then parses the information from the data in JSON format. As mentioned earlier, this code is very similar to a synchronous operation, except for the addition of the yield command.
The method to execute this code is as follows.
var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });
In the above code, first execute the Generator function to obtain the iterator object, and then use the next method (the second line) to execute the first phase of the asynchronous task. Since the Fetch module returns a Promise object, the next method needs to be called with the then method.
As you can see, although the Generator function expresses asynchronous operations very concisely, the process management is not convenient (that is, when to execute the first stage and when to execute the second stage). The later part of this series will introduce how to automate the process management of asynchronous tasks. In addition, the introduction to the Generator function in this article is very simple. For a detailed tutorial, please read “Introduction to ECMAScript 6” written by me .
(over)