JavaScript Asynchronous Programming Syntax

This article talks about the development process of asynchronous programming syntax, and how asynchronous method invocation gradually becomes synchronous syntax.

There are probably asynchronous programming methods下面四种

ECMAScript 6 (abbreviated as ES6), as the next generation JavaScript language, brings JavaScript asynchronous programming to a new stage. ** The topic of this series of articles is to introduce more powerful and complete ES6 asynchronous programming methods. **

The new method is more abstract, at first, I often feel puzzled, until a long time later to figure out, ** The syntax goal of asynchronous programming is how to make it more like synchronous programming. **

This article is about the relationship between these grammars. If you need a deep understanding of any grammar during reading, you can take a look.阮一峰老师的ES6语法入门

Generator

In general, async is syntactic sugar for the Generator function and has been improved on the Generator function.

Generator function

The Generator function is a Finite-State Machine that encapsulates multiple internal states. Execution of the Generator function will return a traverser object, which can traverse each state inside the Generator function in turn, but only by calling the’next ‘method will the next internal state be traversed, so it actually provides a function that can pause execution. The’yield’ expression is the pause flag.

There is such a piece of code:

1
2
3
4
5
6
7
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}

var hw = helloWorldGenerator();

Call and run results:

1
2
3
4
hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }

As can be seen from the results, ‘Generator function’ does not execute when called, but only executes when the’next method ‘is called and the internal pointer points to the statement,’ that is, the function can be suspended or resumed ‘. Each time the next method of the traverser object is called, an object with two properties,’ value ‘and’done’, is returned. The’value ‘property represents the value of the current internal state, which is the value of the expression following the yield expression; the’done’ property is a boolean value indicating whether the traversal has ended.

Generator function pause resume execution principle

To understand why functions can pause and resume, you first need to understand the concept of coroutines.

When a thread (or function) is halfway through execution, it can suspend execution, give the execution right to another thread (or function), and resume execution when the execution right is recovered later. This kind of thread (or function) that can execute in parallel and exchange execution rights is called coroutine.

Coroutines are more lightweight than threads. Ordinary threads are preemptive and compete for cpu resources, while coroutines are cooperative. Coroutines can be regarded as tasks running on threads. There can be multiple coroutines on a thread, but only one coroutine can be executed on the thread at the same time. Its running process is roughly as follows:

  1. Coroutine’A 'starts execution
  2. Coroutine’A ‘executes to a certain stage, enters a pause, and the execution right is transferred to coroutine’B’
  3. The execution of coroutine’B ‘is completed or suspended, and the execution right is returned to’A’
  4. Coroutine’A 'resumes execution

The coroutine pauses when it encounters the yield command, waits until the execution right returns (calls the next method), and then continues to execute from the paused place. 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.

Actuator

Usually, we encapsulate the code that executes the generator into a function, and call the function that executes the generator code an executor.

** Generator is a container for asynchronous operations. Its automatic execution requires a mechanism to automatically hand back the execution right when the asynchronous operation has a result **. Two methods can do this:

  1. Callback function. Wrap asynchronous operations as Thunk 函数Return the execution right in the callback function.
  2. Promise object. Wrap the asynchronous operation into a Promise object, and use the then method to return the execution right.

Actuator for callback function (yield

The following is a Generator executor based on Thunk function.

1
2
3
4
5
6
7
8
9
10
11
12
13
function run(fn) {
var gen = fn();

function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}

next();
}

run(gen);

The run function of the above code is an automatic executor of the Generator function. The internal next function is the callback function of Thunk. 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). If it does not end, pass the next function to the Thunk function (result.value property), otherwise exit directly.

With this executor, executing the Generator function is much easier. No matter how many asynchronous operations there are, just pass in the run function directly. Of course, the premise is that each asynchronous operation must be a Thunk function, that is, the yield command must be followed by a Thunk function.

1
2
3
4
5
6
7
8
var gen = function* (){
Var f1 = yield readFile ('fileA');//readFile is a Thunk function that returns a function to pass in the callback function
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile ('fileN');
};

run(gen);

In the above code, function gen encapsulates n asynchronous read file operations, which will be automatically completed as long as the run function is executed. In this way, asynchronous operations can not only be written like synchronous operations, but also executed in one line of code.

Thunk function is not the only solution for automatic execution of Generator function. Because the key to automatic execution is that there must be a mechanism to automatically control the flow of Generator function, receive and return the execution rights of the program. The callback function can do this, and so can the Promise object.

The executor of the Promise object (yield

A simple automatic executor based on a Promise object (or an executor that can only yield Promises):

1
2
3
4
5
6
7
8
9
10
11
12
function run(gen){
Var g = gen ();//call generator to generate iterator g

Function next (data) {//Defines the next function of the executor
Var result = g.next (data);//call the next function of iterator g and run until the next yield is encountered
If (result.done) return result.value;//if the iterator completes, return the result
Result.value.then (function (data) {//result.value is a promise, continue to call the next method of the executor in then, and pass in the promise. resolve data
next(data);
});
}
Next (); // directly call the next method of the executor once
}

When we use it, we can use it like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
function* foo() {
Let response1 = yield fetch ('https://xxx')//return promise object
console.log('response1')
console.log(response1)
Let response2 = yield fetch ('https://xxx')//return promise object
console.log('response2')
console.log(response2)
}
run(foo);
//1. g = foo (); Generator foo generates iterator g after execution
//2. Run the next method of the executor for the first time without passing parameters. At this time, the data of result = g.next (data) is undefined, and the result is {value: fetch ('https://xxx'), done: false}
//3.done is false, continue execution, result.value.then is actually fetch ('https://xxx').then, the data passed in the callback function of then is the result of the fetch function, and the callback function is called again The next method of the executor passes in the result of fetch at this time.
//4. Run the next method of the executor for the second time, the incoming data is the result returned by the previous fetch, continue to run the foo function through g.next (data), assign the data to response1, and then print the data, until it meets The second yield, then the result.value in the next function of the executor is equal to the second fetch. Repeat the previous step.

In the above code, as long as the Generator function has not been executed to the last step, the next function calls itself to achieve automatic execution. By using generators and executors, asynchronous code can be written in a synchronous way, which greatly enhances the readability of the code.

** The above method looks a lot like async/await. Replace the yield above with await and the function * with async. In fact, await is waiting for the executor to return the running permission. ** At one point, the callback or then are straightened out through the constant callbacks of the built-in executor, and it is synchronized. ** The execution right of the thread is handed over through yield. After the asynchronous function in the executor returns the data, the data and execution right are passed to the generator through the next method **.

COS function

There is a Generator function that reads two files in sequence.

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

** The comma function library lets you avoid writing executors for Generator functions. **

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

In the above code, the Generator function will be automatically executed as long as it is passed in to the co function.

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

1
2
3
co(gen).then(function (){
Console.log ('Generator function completed');
})

In the above code, when the Generator function finishes executing, a line of prompts will be output.

COS function source code

First, the co function takes the Generator function as a parameter and returns a Promise object.

1
2
3
4
5
6
function co(gen) {
var ctx = this;

return new Promise(function(resolve, reject) {
});
}

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

1
2
3
4
5
6
7
8
function co(gen) {
var ctx = this;

return new Promise(function(resolve, reject) {
if (typeof gen = 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next ! 'function') return resolve(gen);
});
}

Next, co wraps the next method of the internal pointer object of the Generator function as an onFulefilled function. This is mainly to be able to catch thrown errors.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function co(gen) {
var ctx = this;

return new Promise(function(resolve, reject) {
if (typeof gen = 'function') gen = gen.call(ctx);
if (!gen || typeof gen.next ! 'function') return resolve(gen);

onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
});
}

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

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

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

First line, check if the current is

The second line, ensuring the return value of each step, is

Third line, use

In the fourth line, if the parameter does not meet the requirements (the parameter is not

async/await

The introduction of async/await in ES7 can completely bid farewell to executors and generators and achieve more intuitive and concise code. According to the MDN definition, async is a function that executes asynchronously and implicitly returns a Promise as the result. It can be said that async is the syntactic sugar of Generator function, and the Generator function has been improved.

The previous code, implemented with’async ', looks like this:

1
2
3
4
5
6
7
8
const foo = async () => {
let response1 = await fetch('https://xxx')
console.log('response1')
console.log(response1)
let response2 = await fetch('https://xxx')
console.log('response2')
console.log(response2)
}

A comparison shows that the async function replaces the asterisk (*) of the Generator function with async, replaces yield with await, and that’s it.

The async function improves the Generator function in the following four points:

  1. Built-in executor. The execution of the Generator function must rely on the executor, while the async function has its own executor, so there is no need to manually execute the next () method.
  2. ‘Better semantics’. Async and await have clearer semantics than asterisks and yield. Async means that there is an asynchronous operation in the function, and await means that the following expression needs to wait for the result.
  3. ‘Wider applicability’. The co module convention stipulates that only Thunk function or Promise object can be behind the yield command, while the await command of async function can be Promise object and primitive type values (numeric, string, and boolean values, but this will be automatically converted to immediately resolved Promise object).
  4. The return value is a Promise. The return value of async function is a Promise object, which is more convenient than the Iterator object returned by Generator function and can be called directly using then () method.

The key point here is that it has its own executor, which is equivalent to encapsulating everything we need to do extra (write executor/dependent co module) internally. For example:

1
2
3
async function fn(args) {
// ...
}

Equivalent to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function fn(args) {
return spawn(function* () {
// ...
});
}

Function spawn (genF) {//spawn function is an automatic executor, the same idea as the simple version, with more Promise and fault tolerance
return new Promise(function(resolve, reject) {
Const gen = genF (); // execute generator
function step(nextF) {
let next;
try {
Next = next F (); // next is gen.next ()
} catch(e) {
return reject(e);
}
If (next.done) {//if all awaits are executed, then async can be resolved
return resolve(next.value);
}
Promise.resolve (next.value).then (function (v) {//if there is still await, then change the content of the next await (which can be a promise, or a value, etc.) to Promsie
Step (function () {return gen.next (v ); }); // execute gen.next (v) in then, handing the execution right back to the async function along with the data v to continue execution to the next await.
}, function(e) {
step(function() { return gen.throw(e); });
});
}
Step (function () {return gen.next (undefined ); }); // call step, pass in a function, function returns gen.next ()
});
}

To sum up, that is to say, async/await actually encapsulates the generator and the executor, encapsulates the function to be executed into the generator, and executes it with a built-in executor.

Summary

The Generator can temporarily hand over the thread execution right while waiting for the asynchronous method (callback function, Promise) to return, while the executor can help execute the generator while waiting for the asynchronous method to return and call the generator’s next method, returning the asynchronous method. The return data and execution rights are returned to the generator.

In general, we can use callbacks, Promises and other methods for asynchronous programming. Promises solve the callback nightmare, but they are still different from synchronous programming syntax, so we use the generator Generator to first yiled the result of the Promise. When the Promise resolves, pass the execution right to the generator together with the result of the Promise through the next method to continue execution until the next yield or end.

At this point, it looks like synchronous programming. Yield continues to execute after waiting for the asynchronous result to return, but in the above process, we need to write an executor to execute the generator. When yielding, the execution right is handed over. After the Promise resolves of yield, call the next of the Generator to hand over the execution right. This executor is known as CO at the beginning. It can help us execute the Generator, but the yield can only be Thunk or Promise.

So later came async/await, ** The function defined with async will be put into a generator and then return a Promise. It not only makes the syntax better understood, but also has a built-in support for a wider range of executors (Promises) to execute this generator, this Promise is an executor, and then uses the recursion callback of the internal function of the executor to call the next method of the generator **, to execute all asynchronous functions one by one, until all awaits are executed. Call the resolve of the Promise and return the result.

Reference link:

http://www.ruanyifeng.com/blog/2015/05/co.html