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:

  1. Waiting in line for customer service;
  2. 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