This is the 11th day of my participation in the Gwen Challenge in November. Check out the details: The last Gwen Challenge in 2021
Asynchronous calls are like water pipes: the more tangled pipes there are, the more likely they are to leak. How to skillfully connect the water pipe, so that the whole system has enough elasticity, need to seriously think 🤔
For JavaScript asynchronous understanding, many people feel confused: Js is a single thread, how to achieve asynchronous? In fact, the Js engine does this by mixing two types of in-memory data structures: stacks and queues. The interaction between stack and queue is also known as Js event loop ~~
For example, 🌰
function fooB(){
console.log('fooB: called');
}
function fooA(){
fooB();
console.log('fooA: called');
}
fooA();
// -> fooB: called
// -> fooA: called
Copy the code
Js engine parsing is as follows:
1. push fooA to stack
<stack>
|fooA| <- push
2. push fooB to stack
<stack>
|fooB| <- push
|fooA|
3. pop fooB from stack and execute
<stack>
|fooB| <- pop
|fooA|
// -> fooB: called
<stack>
|fooA|
4. pop fooA from stack and execute
<stack>
|fooA| <- pop
// -> fooA: called
<stack>
| | <- stack is empty
Copy the code
FooA, fooB, fooB, fooA, fooB, fooB, fooA, fooB, fooB, fooB, fooA, fooB, fooB
Functions that contain asynchronous operations, of course:
- setTimeout
- setInterval
- promise
- ajax
- DOM events
For example, 🌰
function fooB(){
setTimeout(()=>console.log('API call B'));
console.log('fooB: called');
}
function fooA(){
setTimeout(()=>console.log('API call A'));
fooB();
console.log('fooA: called');
}
fooA();
// -> fooB: called
// -> fooA: called
// -> API call A
// -> API call B
Copy the code
Js engine parsing is as follows:
1. push fooA to stack <stack> |fooA| <- push 2. push 'API call A' to queue <queue>|'API call A'| <- push 3. push fooB to stack <stack> |fooB| <- push |fooA| 4. push 'API call B' to queue <queue>|'API call A'|'API call B'| <- push 5. pop fooB from stack and execute <stack> |fooB| <- pop |fooA| // -> fooB: called <stack> |fooA| 6. pop fooA from stack and execute <stack> |fooA| <- pop // -> fooA: called <stack> <- stack is empty | | 7. pop 'API call A' from queue and execute <queue>|'API call A'| <- pop |'API call B'| // -> API call A <queue>|'API call B'| 8. pop 'API call B' from queue and execute <queue>|'API call B'| <- pop // -> API call B <queue>| | <- queue is emptyCopy the code
GIF gifs are interpreted as follows:
After a brief review of how stacks and queues interact in Js memory (without going into the details of microtasks and macro tasks), let’s take a look at how we currently organize this interaction
Yes, the following three ways of organizing are the core of this article:
- Callback
- Promise
- Observer
Callback=>Promise=>Observer, the latter is based on the evolution of the previous ~
Callback
How to understand Callback? In the case of calling customer service, there are two options:
- Waiting in line for customer service;
- Choose customer service to call you back when available.
The second option is JavaScript Callback mode, which allows you to do other things while waiting for the customer service to respond, and then call you back when the customer service is available
function success(res){
console.log("API call successful");
}
function fail(err){
console.log("API call failed");
}
function callApiFoo(success, fail){
fetch(url)
.then(res => success(res))
.catch(err => fail(err));
};
callApiFoo(success, fail);
Copy the code
The drawback of Callback is that nested calls create Callback hell, as follows;
callApiFooA((resA)=>{
callApiFooB((resB)=>{
callApiFooC((resC)=>{
console.log(resC);
}), fail);
}), fail);
}), fail);
Copy the code
Promise
As we all know, Promise was here to solve callback hell
function callApiFooA(){
return fetch(url); // JS fetch method returns a Promise
}
function callApiFooB(resA){
return fetch(url+'/'+resA.id);
}
function callApiFooC(resB){
return fetch(url+'/'+resB.id);
}
callApiFooA()
.then(callApiFooB)
.then(callApiFooC)
.catch(fail)
Copy the code
At the same time, Promise also provides many other more scalable solutions, such as Promise.all, promise.race, etc.
// promise. all: executes concurrently, calling.then only when all is resolved or reject;
function callApiFooA(){
return fetch(urlA);
}
function callApiFooB(){
return fetch(urlB);
}
function callApiFooC([resA, resB]){
return fetch(url+'/'+resA.id+'/'+resB.id);
}
function callApiFooD(resC){
return fetch(url+'/'+resC.id);
}
Promise.all([callApiFooA(), callApiFooB()])
.then(callApiFooC)
.then(callApiFooD)
.catch(fail)
Copy the code
Promises make the code look cleaner, but the evolution isn’t over yet; If you want to handle complex data flows, using Promise can be cumbersome……
Observer
Working with multiple asynchronous operational data streams can be complex, especially when they are interdependent, and we have to combine them in more ingenious ways; The Observer!
An observer creates (publishes) a data stream that needs to be changed, a SUBSCRIBE call (subscribes) data stream; Take RxJs as an example:
function callApiFooA(){
return fetch(urlA);
}
function callApiFooB(){
return fetch( urlB );
}
function callApiFooC( [resAId, resBId] ){
return fetch(url +'/'+ resAId +'/'+ resBId);
}
function callApiFooD( resC ){
return fetch(url +'/'+ resC.id);
}
Observable.from(Promise.all([callApiFooA() , callApiFooB() ])).pipe(
map(([resA, resB]) => ([resA.id, resB.id])), // <- extract ids
switchMap((resIds) => Observable.from(callApiFooC( resIds ) )),
switchMap((resC) => Observable.from(callApiFooD( resC ) )),
tap((resD) => console.log(resD))
).subscribe();
Copy the code
Detailed process:
-
Observable. From transforms a Promises array into an Observable, which is an array of results based on callApiFooA and callApiFooB.
-
Map – Retrieve ids from API functions A and B;
-
SwitchMap – Calls callApiFooC with the id of the previous result and returns a new Observable, which is the return result of callApiFooC(resIds);
-
SwitchMap – Call callApiFooD with the result of the callApiFooC function;
-
Tap – Retrieves the results of previous executions and prints them in the console;
-
Subscribe – starts listening to an Observable;
An Observable, a producer of multiple data values, is more powerful and flexible at handling asynchronous data flows, and is used in front-end frameworks such as Angular
Knock! Isn’t this notation, isn’t this pattern just a functor in functional programming? An Observable is a wrapped functor that goes on and on, forming a chain, calling subscribe (lazy evaluation), and then executing and consuming at the last step!
What are the benefits of this?
The core reason is the separation of creation (publishing) and invocation (subscription consumption)!
Another example is 🌰
var observable = Rx.Observable.create(function (observer) {
observer.next(1);
observer.next(2);
observer.next(3);
setTimeout(() => {
observer.next(4);
observer.complete();
}, 1000);
});
console.log('just before subscribe');
observable.subscribe({
next: x => console.log('got value ' + x),
error: err => console.error('something wrong occurred: ' + err),
complete: () => console.log('done'),
});
console.log('just after subscribe');
Copy the code
Observable publishes (synchronously) 1, 2, and 3 values; After 1 second, continue to publish the value of 4, and finally finish;
Subscribe C. Subscription. Unsubscribe () can suspend execution during a process;
Console print result:
just before subscribe
got value 1
got value 2
got value 3
just after subscribe
got value 4
done
Copy the code
Js asynchronous processing evolution is divided into 3 stages: Callback=>Promise=>Observer Observer is like a functional programming functor, encapsulation, transfer chain, deferred execution, almost the same, but with more emphasis on publish and subscribe! The creation and execution of the partition function as two separate fields is essential for elastic assembly of asynchronous water pipes!!
The above! As mentioned in a previous article, lazy evaluation seems to connect the most important elements of JS closure and asynchracy, and this is especially true now
👍👍👍
I am Anthony Nuggets, the public account of the same name, every day a pawn, dig a gold, goodbye ~
Reference for this article:
-
the-evolution-of-asynchronous-patterns-in-javascript
-
Observable, the core RxJs concept