Preface:

I’ve collected some examples from the web and my own practice to give you a taste of the advantages of RXJS when dealing with asynchrony.

Main purpose of this paper:

1. Get some students interested in RXJS ‘concise handling of slightly complex asynchrony (no need to know API, just feel the advantages of RXJS)

2. Let some students who lack practical cases practice their hands (there are more API cases on the Internet, but fewer business-related cases, which is related to the low popularity of RXJS)

3. Summary of my preliminary study

Start with a single byte interview question: Control the maximum number of concurrent requests

The title is as follows:

Implement a batch request function multiRequest(urls, maxNum) with the following requirements: • maxNum specifies the maximum number of concurrent requests • Each time a request returns, a space is left for new requests to be added • When all requests are completed, the results are typed in the order in urlsCopy the code

You can think about what you would do if you didn’t use RXJS, but how easy is it to use RXJS

Function httpGet(url) {return new Promise(resolve => setTimeout(() => resolve(' Result: ${url}`), 2000)); }Copy the code

This interview problem can be solved with only 1 line of code using RXJS, and a version will be written later to see how cumbersome the promise implementation can be without RXJS.

const array = [
  'https://httpbin.org/ip'.'https://httpbin.org/user-agent'.'https://httpbin.org/delay/3',];MergeMap is an RXJS operator designed to handle concurrent processing
// mergeMap the second parameter, 2, means that the number of concurrent requests from(array) is 2, and the array is fetched only after the promise execution ends
// mergeMap's first parameter, httpGet, means concurrency at a time. How to wrap data from(array), in this case as an argument to httpGet
const source = from(array).pipe(mergeMap(httpGet, 2)).subscribe(val= > console.log(val));
Copy the code

Online code preview: stackblitz.com/edit/rxjs-q… (Note the printing order of the console)

Here’s a version of Promise, with lots of code and procedural spaghetti code (if you don’t use RXJS, general scenarios recommend using ramda library, “flow” or function composition to write functions, keep your function modules away from spaghetti code (spaghetti code = difficult to maintain procedural code). The article ends with a small talk about how to use Ramda in uncomplicated business scenarios.

Train of thought is to use promise to solve the above interview questions below, you can see a lot of temporary variables, while function, if statement, make the code becomes difficult to maintain (not reject this code, elegant interface, after all, the back is likely to be the realization of the “dirty”), but if you have tools to help you directly use elegant interface, reduces the complexity, Why not

function multiRequest(urls = [], maxNum{
  // Total number of requests
  const len = urls.length;
  // Create an array based on the number of requests to hold the results of the requests
  const result = new Array(len).fill(false);
  // The current number of completions
  let count = 0;
 
  return new Promise((resolve, reject) = > {
    // Request maxNum
    while (count < maxNum) {
      next();
    }
    function next({
      let current = count++;
      // Handle boundary conditions
      if (current >= len) {
        // When the request completes, set the promise to success and return result as a promise! result.includes(false) && resolve(result);
        return;
      }
      const url = urls[current];
      console.log(` began${current}`.new Date().toLocaleString());
      fetch(url)
        .then((res) = > {
          // Save the request result
          result[current] = res;
          console.log(Completed `${current}`.new Date().toLocaleString());
          // If the request is not complete, it is recursive
          if (current < len) {
            next();
          }
        })
        .catch((err) = > {
          console.log(End of `${current}`.new Date().toLocaleString());
          result[current] = err;
          // If the request is not complete, it is recursive
          if(current < len) { next(); }}); }}); }Copy the code

Let’s take another interview question, which is about the concurrency of Ajax requests that I met with Tencent. At that time, THE answer was not very good at the front end. The title is as follows:

To elaborate further

When button A is pressed, the ajax request data is displayed in the Input Type =text box, as is button B.

The problem is that if you press A first, the Ajax is sent, but the data is not returned, we can’t wait, press B immediately, and the data requested by A button is returned first, which is embarrassing. If you press B, the data returned by A button is displayed first. How to solve this problem?

This problem can be cancelled when the A button is pressed and then the B button is pressed. Ajax and FETCH have methods to implement. Ajax has its own cancel method.

function abortableFetch(request, opts) {
  const controller = new AbortController();
  const signal = controller.signal;

  return {
    abort: () = > controller.abort(),
    ready: fetch(request, { ... opts, signal }) }; }Copy the code

It’s A bit of A hassle to use, and it’s A bit of A coupling, because I call abort on button B’s onClick event.

Ok, let’s write a generic solution based on RXJS. (In terms of decoupling between functions, publish-subscribe is a bit of a general-purpose idea, as is RXJS ‘new Subject.)

import { Subject } from 'rxjs';
import { switchMap } from 'rxjs/operators';

// Suppose this is your HTTP request function
function httpGet(url: any) :any {
  return new Promise(resolve= >
    setTimeout(() = > resolve(`Result: ${url}`), 2000)); }class abortableFetch {
  search: Subject<any>;
  constructor() {
    this.search = new Subject();
    this.init();
  }
  init() {
    this.search
      .pipe((switchMap as any)((value: any): any= > httpGet(value)))
      .subscribe(val= > console.log(val));
  }

  trigger(value) {
    this.search.next(value); }}// It is very simple to use a trigger method
const switchFetch = new abortableFetch();

switchFetch.trigger(123);
setTimeout(() = > {
  switchFetch.trigger(456);
}, 1000);

Copy the code

Note that the console output in this case is 456, not 123, because the output after 456 overwrites the previous 123, which cancels the previous request

Preview this case online: stackblitz.com/edit/rxjs-z…

Ok, the two examples above show the biggest advantages of RXJS:

1, functional programming in writing some small functions, decoupling is very simple, naturally meet the high cohesion, low coupling

2, RXJS in dealing with asynchronous (such as network IO and UI interaction), write some small functions, less code, strong semantic

But the problem is also outstanding, is to master RXJS, it is really not easy, let alone RXJS, ramda library or functional library front-end in my experience is very few.

Next, here are some examples based on RXJS.

1. Examples of buffer operators

BufferTime: For example, if you write an online chat room based on Websocket, it is not possible for WS to render immediately every time a new message is received, which would generally cause performance issues when many people are talking at the same time.

So you need to collect messages over a period of time and then render them all together, like batch render every second. Writing in native JS, you need to maintain a queue pool and a timer that takes care of rendering the message when it receives it, puts it in the queue pool, and then renders it:

let messagePool = []
ws.on('message', (message) => {
    messagePool.push(message)
})

setInterval(() => {
    render(messagePool)
    messagePool = []
}, 1000)
Copy the code

This is as simple as it gets, but the logic is still broken, and there are cleanup timers to consider. If you use RxJS, the code looks much better

import { fromEvent } from 'rxjs';
import { switchMap } from 'rxjs/operators';
 fromEvent(ws, 'message')
     .pipe(bufferTime(1000))
    .subscribe(messages => render(messages))
Copy the code

Record the number of mouse clicks in two seconds

fromEvent(document,'click').pipe( bufferTime(2000), Map (array=>array.length).subscribe(count => {console.log(" number of hits in 2 seconds ", count); });Copy the code

BufferCount: Another example is when we’re writing a game that pops up hidden eggs when the user types “up, down, left, right, right, BABA” in a row. With native JS, we also need to maintain a queue that holds the last 12 user inputs. And then every time you press a button, it identifies if the egg is triggered. RxJS is much simpler, mainly with less logic to maintain queues:

const code = [
   "ArrowUp"."ArrowUp"."ArrowDown"."ArrowDown"."ArrowLeft"."ArrowRight"."ArrowLeft"."ArrowRight"."KeyB"."KeyA"."KeyB"."KeyA"
]

fromEvent(document.'keyup').pipe(
   map(e= > e.code),
   bufferCount(12.1)
).subscribe(last12key= > {
        if (_.isEqual(last12key, code)) {
            console.log('Hidden egg \(^o^)/~')}})Copy the code

Of course, RxJS can also be much more complicated logic, such as requiring that the eggs can only be triggered if the chests are entered consecutively within two seconds

import { fromEvent } from 'rxjs'; import { bufferCount, map, auditTime } from 'rxjs/operators'; const code = ['KeyA', 'KeyB', 'KeyA']; fromEvent(document, 'keyup') .pipe( map(e => (e as any).code), bufferCount(3, 1), AuditTime (2000)).subscribe(last3Key => {if (_.isequal (last3key, code)) {console.log(' Hidden message \(^o^)/~')}});Copy the code

2. Simple drag

The implementation content is as follows:

1. First there is an element on the page (#drag)

2. Start monitoring the position of the mousemove when the mouse is left mousedown on the #drag element

3. When the left mouse button is released (mouseup), stop monitoring the movement of the mouse

4. Modify the original style properties when mouse movement is monitored

import { of, fromEvent} from 'rxjs'; 
import { map, concatMap, takeUntil, withLatestFrom } from 'rxjs/operators';

 // Style elision is absolute positioning
const dragEle = document.getElementById('drag')
const mouseDown = fromEvent(dragEle, 'mousedown')
const mouseUp = fromEvent(document.'mouseup')
const mouseMove = fromEvent(document.'mousemove')

mouseDown.pipe(
  concatMap(e= > mouseMove.pipe(takeUntil(mouseUp))),
  withLatestFrom(mouseDown, (move: MouseEvent, down: MouseEvent) = > {
        return {
            x: move.clientX - down.offsetX,
            y: move.clientY - down.offsetY
        }
  })
).subscribe(pos= > {
        dragEle.style.top = pos.y + 'px';
        dragEle.style.left = pos.x + 'px';
})
Copy the code

Preview online: stackblitz.com/edit/rxjs-s…

3. Simple autoComponent functionality

The implementation content is as follows:

1. Prepare HTML and CSS for put#search and ul#suggest-list

Input# search, wait 100 milliseconds, if no input, send HTTP Request

3. When the Response does not come back, the user enters the next text, abandons the previous one, and sends a new Request again

4. Display drop-down options after receiving Response

5, the left mouse button to select the corresponding drop-down, instead of put#search text

import { fromEvent } from "rxjs";
import { map, debounceTime, switchMap } from "rxjs/operators";

const url = 'https://zh.wikipedia.org/w/api.php?action=opensearch&format=json&limit=5&origin=*';

const getSuggestList = (keyword) = > fetch(url + '&search=' + keyword, { method: 'GET'.mode: 'cors' })
                                    .then(res= > res.json())

const searchInput = document.getElementById('search');
const suggestList = document.getElementById('suggest-list');

const keyword = fromEvent(searchInput, 'input');
const selectItem = fromEvent(suggestList, 'click');

const render = (suggestArr = []) = > suggestList.innerHTML = suggestArr.map(item= > '<li>'+ item +'</li>').join(' ')

keyword.pipe(
  debounceTime(100),
  switchMap(
    (e: any) = > getSuggestList(e.target.value),
    (e, res) = > res[1]
  )
).subscribe(list= > render(list))
  
 

selectItem.pipe(
  map(e= > e.target.innerText)
).subscribe(text= > { 
      searchInput.value = text;
      render();
  })
Copy the code

Preview online: stackblitz.com/edit/rxjs-x…

Well, having said that, my personal feeling is that RXJS is pretty simple in certain scenarios when it comes to handling the logic at the network layer and UI layer. I recommend two very good tutorials here, and I can’t see anyone on the web recommending these two RXJS tutorials (some of the examples are from here).

Through RXJS ren du second pulse: ithelp.ithome.com.tw/users/20020…

Proficient in 30 days RXJS: ithelp.ithome.com.tw/articles/10…

Finally, Amway’s other functional programming library, RamdaJS, RXJS, is a recent addition and has not yet been used in the project

I’ve been using RamdaJS for about 3 months now, and I’ve learned a few things about it. Once you write it, the code becomes much more maintainable. The reason is that you have to follow the single responsibility principle of design pattern, and all functions that can be reused are extracted (functional programming forces you into this habit).

Attach a snippet of ramdaJS code from my own project, over.

// Terminates the function chain if one of the functions returns null or undefined
const pipeWhileNotNil = R.pipeWith((f, res) = >
  R.isNil(res) ? res : f(res),
);

pipeWhileNotNil([
  // checkData is used for form verification. If null is not returned, the function chain is terminated
  // R__ is a placeholder for your function's parameters. After r.urry is converted, r.__ can be used as a placeholder for your parameters
  // The parameters in the array need to be customized
  checkData(R.__, ['sdExchangeRate'.'sdEffectDate']),
  // This function is used to filter, equivalent to the find method of the array. Format2YYYYMMDD is used by DayJS to format the date
  R.find(
      (v) = >v? .effectDate === format2YYYYMMDD(sdEffectDate), ),// pipeP is a stream of promise functions. The first argument must be the Promise function, and the following functions are equivalent to the then functions in the Promise
  R.pipeP(
    // The result of the previous function is passed to the existEqualEffectDate
    // promiseModal is a popup component that asks if you want to continue an action
    async (existEqualEffectDate) => {
      if (existEqualEffectDate) {
        return await promiseModal({
          title: The effective date already exists, do you want to modify the exchange rate directly? `}); }else {
        return await promiseModal({
          title: 'Save effective date can not be modified, determine to save? `}); }},// Perform the final operation depending on whether the last result returned is true or false, r.ipe is the synchronization chain method, and dispatch is the redux dispatch
    (isGo) = > isGo ? R.pipe(R.tail, saveData(dispatch))(data) : dispatch({
      type: 'currencyAndExchange/getExchangePairList',}); , ])(record);Copy the code