Reaktivitet med RxJS:kraftpress

RxJS er et reaktivt programmeringsbibliotek for JavaScript, som utnytter observerbare sekvenser for å komponere asynkrone eller hendelsesbaserte programmer. Som en del av Reactive Extensions-prosjektet kombinerer arkitekturen til RxJS de beste delene fra Observeren mønsteret, Iteratoren mønster og funksjonell programmering .

Hvis du har brukt et JavaScript-verktøybibliotek som Lodash før, kan du tenke på RxJS som Lodash for arrangementer.

RxJS er ikke lenger et nytt JavaScript-bibliotek. Faktisk, når dette skrives, er den nyeste versjonen av biblioteket 6.3.3 , som er den siste av over 105 utgivelser.

I denne opplæringen vil vi utnytte reaktiv programmering ved å bruke RxJS for å implementere tvungen pressedeteksjon og håndtering for vanlige DOM-musehendelser.

Her er kraftpressdemoen på Code Sandbox . Naviger til koblingen og trykk og hold volumkontrollene for å se kraftpressen i aksjon.

Denne opplæringen bør ikke brukes som en erstatning for en skikkelig RxJS nybegynnerveiledning, selv om den kort forklarer et par reaktive programmeringskonsepter og operatører.

Observable og operatører

Observabler er kjernen i RxJS-arkitekturen . En observerbar kan sammenlignes med en påkallelig strøm av verdier eller hendelser som kommer fra en kilde. Kildene kan være tidsintervaller, AJAX-forespørsler, DOM-hendelser osv.

En observerbar:

  • er lat (den avgir ingen verdi før den har blitt abonnert på)
  • kan ha én eller flere observatører lytte etter verdiene
  • kan transformeres til en annen observerbar av en kjede av operatører

Operatorer er rene funksjoner som kan returnere en ny observerbar fra en observerbar . Dette mønsteret gjør det mulig å kjede operatører siden en observerbar alltid returneres på slutten.

Faktisk viser nyere versjoner av RxJS en .pipe() instansmetoden på <Observable> klasse, som kan brukes til å kjede operatører som funksjonsanrop.

En operatør lytter i utgangspunktet etter verdier fra den observerbare kilden, implementerer en viss definert logikk på de mottatte verdiene, og returnerer en ny observerbar emitterende verdi basert på logikken.

Tving trykk

Tvingstrykk refererer ganske enkelt til en DOM-pressehendelse som keydown og mousedown , vedvarende over en periode før den tilsvarende DOM-utgivelseshendelsen aktiveres, for eksempel keyup og mouseup i dette tilfellet.

Enkelt sagt er et tvangstrykk synonymt med å trykke og holde.

Det er mange områder i brukergrensesnitt der en kraftpress kan være aktuelt. Tenk deg å ha et sett med volumkontroller for en musikkspiller-widget, og du vil øke volumet fra 30 til 70.

I utgangspunktet kan du oppnå dette på to måter:

  1. trykk på VOLUM OPP-knappen flere ganger til du når ønsket volum — dette trykk kan muligens gjøres 40 ganger
  2. tving trykk (trykk og hold) VOLUM OPP-knappen til du når eller er nær ønsket volum, og juster deretter til du når ønsket volum

Her er en enkel demo av denne illustrasjonen:

Tvingspress med vanilje-JavaScript

Implementering av kraftpresse med vanilje JavaScript, lik det vi har ovenfor, er ikke en herkulisk oppgave. Denne implementeringen vil kreve:

Flere flotte artikler fra LogRocket:

  • Ikke gå glipp av et øyeblikk med The Replay, et kuratert nyhetsbrev fra LogRocket
  • Bruk Reacts useEffect for å optimalisere applikasjonens ytelse
  • Bytt mellom flere versjoner av Node
  • Finn ut hvordan du animerer React-appen din med AnimXYZ
  • Utforsk Tauri, et nytt rammeverk for å bygge binærfiler
  • Sammenlign NestJS vs. Express.js
  • Oppdag populære ORM-er som brukes i TypeScript-landskapet
  • lytter etter mousedown hendelser på volumkontrollknappen
  • ved å bruke setInterval() for å justere volumet kontinuerlig til en mouseup hendelsen skjer

La oss si at markeringen for volumkontrollene våre ser slik ut:


<div id="volume-control">
  <button type="button" data-volume="decrease" aria-label="Decrease Volume"> - </button>
  <button type="button" data-volume="increase" aria-label="Increase Volume"> + </button>
</div>

Følgende kodebit viser hvordan kraftpressimplementeringen vil se ut ved bruk av vanilla JavaScript. For korthets skyld er implementeringene av increaseVolume() og decreaseVolume() funksjoner er utelatt:

const control = document.getElementById('volume-control');
const buttons = control.querySelectorAll('button');

let timeout = null;
let interval = null;

buttons.forEach($button => {
  const increase = $button.getAttribute('data-volume') === 'increase';
  const fn = increase ? increaseVolume : decreaseVolume;
  
  $button.addEventListener('mousedown', evt => {
    evt.preventDefault();
    fn();
    
    timeout = setTimeout(() => {
      interval = setInterval(fn, 100);
    }, 500);
    
    document.addEventListener('mouseup', resetForcePress);
  });
});

function resetForcePress(evt) {
  evt.preventDefault();
  timeout && clearTimeout(timeout);
  interval && clearInterval(interval);
  
  timeout = null;
  interval = null;
  
  document.removeEventListener('mouseup', resetForcePress);
}

Denne kraftpressimplementeringen med vanilje-JavaScript ser veldig enkel ut, og et bibliotek som RxJS ser derfor ikke ut til å være nødvendig.

En rask observasjon av kodebiten vil vise at volumet kontinuerlig vil bli justert med like mye med like tidsintervaller til en mouseup arrangementet avfyres. Dette er en lineær progresjon .

Implementeringen begynner imidlertid å bli kompleks når vi ønsker litt mer avansert kontroll over kraftpressen. La oss for eksempel si at vi ønsker en form for eksponentiell progresjon av volumet. Dette betyr at volumet bør endres raskere for lengre krafttrykk.

Her er en enkel illustrasjon som viser forskjellen:

En implementering som eksponentiell volumprogresjon vil være ganske utfordrende ved å bruke vanilla JavaScript, siden du kanskje må holde styr på hvor lenge kraftpressen lever for å bestemme hvor raskt volumet skal endres.

Saker som dette er best egnet for RxJS-biblioteket. Med RxJS kommer enda mer kraft til å komponere observerbare sekvenser for å håndtere komplekse asynkrone oppgaver.

Tvingstrykk med RxJS

La oss gå videre og implementere kraftpressen på nytt med lineær volumprogresjon ved hjelp av RxJS. Slik vil det se ut:

import { fromEvent, timer } from 'rxjs';
import { map, switchMap, startWith, takeUntil } from 'rxjs/operators';

const control = document.getElementById('volume-control');
const buttons = control.querySelectorAll('button');

const documentMouseup$ = fromEvent(document, 'mouseup');

const forcepress = fn => {
  return timer(500, 100).pipe(
    startWith(fn()),
    takeUntil(documentMouseup$),
    map(fn)
  );
};

buttons.forEach($button => {
  const increase = $button.getAttribute('data-volume') === 'increase';
  const fn = increase ? increaseVolume : decreaseVolume;
  
  fromEvent($button, 'mousedown').pipe(
    switchMap(evt => {
      evt.preventDefault();
      return forcepress(fn);
    })
  ).subscribe();
});

En nøye observasjon av denne kodebiten vil vise at vi har importert noen funksjoner og operatører fra RxJS-biblioteket. Forutsetningen er at du allerede har RxJS installert som en avhengighet for prosjektet ditt.

Det er noen viktige deler av kodebiten som er verdt å fremheve.

Linje 7

const documentMouseup$ = fromEvent(document, 'mouseup');

fromEvent hjelpefunksjonen oppretter en ny observerbar som sendes ut hver gang den angitte hendelsen utløses på en DOM-node.

For eksempel, på linjen ovenfor, fromEvent oppretter en observerbar som sender ut et hendelsesobjekt hver gang en mouseup avfyres på document node. fromEvent funksjonen brukes også i Line 21 for å lytte etter mousedown hendelser på en volumkontrollknapp.

Legg merke til at det observerbare er lagret i en konstant kalt documentMouseup$ . Det er vanlig praksis å legge ved en $ etter navnet på en variabel som brukes til å lagre en observerbar.

Linje 9–15

const forcepress = fn => {
  return timer(500, 100).pipe(
    startWith(fn()),
    takeUntil(documentMouseup$),
    map(fn)
  );
};

forcepress() funksjonen tar en behandlerfunksjon fn som sitt argument og returnerer en observerbar. Den returnerte observerbare opprettes fra en tidtaker som bruker timer() funksjon og transformert ved hjelp av en kjede av operatører.

La oss bryte ned koden linje for linje:

timer(500, 100)

Denne timer() funksjonskall oppretter en ny observerbar som sender ut et heltall som starter fra null (0 ). Det første heltall sendes ut etter 500ms og deretter sendes påfølgende heltall ut ved 100ms intervaller.

 .pipe() metode på en observerbar brukes til å kjede operatører ved å bruke dem som vanlige funksjoner fra venstre til høyre.

startMed

timer(500, 100).pipe(
  startWith(fn())
)

startWith() operatør mottar en verdi som et argument som bør sendes ut først av det observerbare. Dette er nyttig for å sende ut en startverdi fra en observerbar.

Her er startWith() operator brukes til å utføre behandleren fn og avgi den returnerte verdien.

takeTil

timer(500, 100).pipe(
  takeUntil(documentMouseup$)
)

takeUntil() operatør brukes til å slutte å sende ut verdier fra kilden som kan observeres basert på en annen observerbar. Den mottar en observerbar som argumentasjon. I det øyeblikket denne observerbare sender ut sin første verdi, sendes det ikke ut mer verdi fra den observerbare kilden.

I kodebiten vår er documentMouseup$ observable sendes til takeUntil() operatør. Dette sikrer at det ikke sendes ut flere verdier fra tidtakeren i det øyeblikket en mouseup hendelsen utløses på document node.

kart

timer(500, 100).pipe(
  map(fn)
)

map() operatør er veldig lik Array.map() for JavaScript-matriser. Den tar en kartleggingsfunksjon som argument som mottar den utsendte verdien fra den observerbare kilden og returnerer en transformert verdi.

Her sender vi bare fn fungerer som tilordningsfunksjonen til map() operatør.

Linje 21–26

fromEvent($button, 'mousedown').pipe(
  switchMap(evt => {
    evt.preventDefault();
    return forcepress(fn);
  })
).subscribe();

Disse linjene tilordner ganske enkelt mousedown hendelse på en volumkontrollknapp for å tvinge trykk-handlingen ved å bruke switchMap() operatør.

Den oppretter først en observerbar av mousedown hendelser på knappeelementet. Deretter bruker den switchMap() operatør for å kartlegge den utsendte verdien til en indre observerbar hvis verdier vil bli sendt ut. I vår kodebit returneres den indre observerbare fra utføring av forcepress() funksjon.

Legg merke til at vi passerte fn til forcepress() fungerer som definert. Det er også veldig viktig å merke seg at vi abonnerte på det observerbare ved å bruke subscribe() metode. Husk at observerbare er late. Hvis de ikke abonnerer, avgir de ingen verdi.

Forbedre kraftpressen

Noen få ting kan gjøres for å forbedre kraftpressen ved å bruke RxJS-operatører. En forbedring vil være å implementere en eksponentiell volumprogresjon i stedet for den lineære progresjonen som vi så før.

Eksponentiell volumprogresjon

Å gjøre dette med RxJS er veldig enkelt. La oss anta at den nåværende implementeringen av volumjusteringsfunksjonene våre ser slik ut:

let VOLUME = 0;

const boundedVolume = volume => {
  return Math.max(0, Math.min(volume, 100));
};

const increaseVolume = () => {
  VOLUME = boundedVolume(VOLUME + 1);
  return VOLUME;
};

const decreaseVolume = () => {
  VOLUME = boundedVolume(VOLUME - 1);
  return VOLUME;
};

Vi kan endre volumjusteringsfunksjonene litt for å akseptere en volumtrinnfaktor. Disse modifikasjonene vil gjøre det mulig for oss å oppnå den eksponentielle progresjonen som vi vil se om et øyeblikk.

Følgende kodebit viser endringene:

const increaseVolume = (factor = 1) => {
  VOLUME = boundedVolume(VOLUME + 1 * factor);
  return VOLUME;
};

const decreaseVolume = (factor = 1) => {
  VOLUME = boundedVolume(VOLUME - 1 * factor);
  return VOLUME;
};

Med disse modifikasjonene kan vi nå sende en factor til volumjusteringsfunksjonene for å spesifisere hvor mye volumet skal justeres. Kalle disse funksjonene uten å sende en factor vil ganske enkelt justere volumet ett trinn om gangen.

Nå kan vi endre forcepress() funksjon vi opprettet tidligere som følger:

import { fromEvent, timer } from 'rxjs';
import { map, switchMap, startWith, takeUntil, withLatestFrom } from 'rxjs/operators';

const computedFactor = n => Math.round(
  Math.pow(1.25 + n / 10, 1 + n / 5)
);

const forcepress = fn => {
  return timer(500, 100).pipe(
    startWith(fn()),
    takeUntil(documentMouseup$),
    withLatestFrom(
      timer(1000, 500).pipe(startWith(0))
    ),
    map(([t, n]) => fn(computedFactor(n)))
  );
};

Med denne modifikasjonen har vi implementert tvangstrykk på volumkontrollknappene med en eksponentiell volumprogresjon.

computedFactor

Her har vi lagt til en enkel funksjon kalt computedFactor for å beregne volumjusteringsfaktoren. Denne funksjonen tar et heltallsargument n som den beregner faktoren med.

Vi beregner ganske enkelt dette uttrykket:

Math.round(Math.pow(1.25 + n / 10, 1 + n / 5));

Her bruker vi Math.pow() for å progressivt beregne eksponenter basert på verdien av n . Dette uttrykket kan modifiseres for å passe den eksponentielle progresjonen som kreves. For eksempel kan det være så enkelt som dette:

Math.pow(2, n);

Legg også merke til at vi bruker Math.round() her for å sikre at vi får en heltallsfaktor siden beregningen involverer mange flyttall.

Her er et sammendrag av de ti første verdiene som returneres av computedFactor() funksjon. Det virker som den perfekte funksjonen for å beregne faktorene:

0 => Math.round(Math.pow(1.25, 1.0)) => 1
1 => Math.round(Math.pow(1.35, 1.2)) => 1
2 => Math.round(Math.pow(1.45, 1.4)) => 2
3 => Math.round(Math.pow(1.55, 1.6)) => 2
4 => Math.round(Math.pow(1.65, 1.8)) => 2
5 => Math.round(Math.pow(1.75, 2.0)) => 3
6 => Math.round(Math.pow(1.85, 2.2)) => 4
7 => Math.round(Math.pow(1.95, 2.4)) => 5
8 => Math.round(Math.pow(2.05, 2.6)) => 6
9 => Math.round(Math.pow(2.15, 2.8)) => 9

medSistefra

En nøye observasjon av forcepress() funksjonen vil vise at denne linjen:

map(fn)

er erstattet med disse linjene:

withLatestFrom(
  timer(1000, 500).pipe(startWith(0))
),
map(([t, n]) => fn(computedFactor(n)))

Her har vi introdusert en annen RxJS-operatør withLatestFrom() . Det tar en annen observerbar som sitt første argument. Denne operatoren er nyttig for å sende ut verdier fra flere observerbare som en rekke verdier.

Den sender imidlertid bare ut hver gang den observerbare kilden sender ut, og sender ut de siste verdiene fra alle de observerbare i rekkefølge hver gang.

I vårt eksempel sendte vi inn en annen observerbar opprettet med timer() funksjon til withLatestFrom() operatør.

Tidtakeren som kan observeres, sender ut et heltall først etter 1000ms og deretter hver 500ms . startWith() operatøren sendes til tidtakeren som kan observeres, noe som får den til å starte med en startverdi på 0 .

Kartleggingsfunksjonen ble sendt til map() operatoren forventer en matrise som sitt første argument, siden withLatestFrom() operatøren sender ut en rekke verdier.

Her er kartoperatøren igjen:

map(([t, n]) => fn(computedFactor(n)))

I denne kodebiten er t representerer verdien som sendes ut av den første observerbare, som i dette tilfellet er den observerbare kilden. n representerer verdien som sendes ut av den andre observerbare, som er tidtakeren.

Til slutt ringer vi fn() som før, bare denne gangen passerer vi en beregnet volumjusteringsfaktor utledet fra å ringe computedFactor() funksjon med n .

Her er sammenligningen mellom den lineære og eksponentielle progresjonen som viser varigheten av å øke volumet fra 0 til 100 :

Forbedret presseavslutning

Foreløpig avslutter vi den tvangspressede volumjusteringen én gang en mouseup hendelsen utløses på document node. Vi kan imidlertid forbedre den ytterligere for å tillate avslutning av kraftpressen når volumet når noen av grensene, enten 0 eller 100 .

Vi kan lage en tilpasset operatørfunksjon som vi kan sende til kilden som kan observeres for å forhindre at den sender ut i det øyeblikket noe av dette skjer:

  • en mouseup hendelsen utløses på document node
  • volumet når enten 0 eller 100

Her er den tilpassede operatørfunksjonen kalt limitVolume() :

import { timer } from 'rxjs';
import { takeUntil, takeWhile, zip, last } from 'rxjs/operators';

const timerUntilMouseup$ = timer(10, 10).pipe(
  takeUntil(documentMouseup$)
);

const timerWithinLimits$ = timer(10, 10).pipe(
  takeWhile(() => VOLUME > 0 && VOLUME < 100)
);

const volumeStop$ = timerUntilMouseup$.pipe(
  zip(timerWithinLimits$),
  last()
);

const limitVolume = () => source$ => {
  return source$.pipe(
    takeUntil(volumeStop$)
  );
};

Her opprettet vi to timer observerbare, nemlig timerUntilMouseup$ og timerWithinLimits$ som avsluttes basert på de to betingelsene vi oppga.

Så komponerte vi volumeStop$ observerbar fra de to observerbare ved hjelp av zip() og last() operatører for å sikre at denne observerbare bare sender ut én verdi for den første av de to observerbare som avsluttes.

Til slutt bruker vi takeUntil() operatør i limitVolume() tilpasset operatørfunksjon for å sikre at source$ observerbar avsluttes når volumeStop$ observerbar sender ut sin første verdi.

Legg merke til at limitVolume() returnerer en funksjon som tar en observerbar som argument og returnerer en annen observerbar. Denne implementeringen er avgjørende for at den skal brukes som en RxJS-operatør.

Med limitVolume() tilpasset operatør, kan vi nå endre forcepress() som følger:

const forcepress = fn => {
  return timer(500, 100).pipe(
    startWith(fn()),
    limitVolume(),
    withLatestFrom(
      timer(1000, 500).pipe(startWith(0))
    ),
    map(([t, n]) => fn(computedFactor(n)))
  );
};

Mer krafttrykk for kalenderen

Mye er allerede gjort i implementering av styrkepresse. La oss imidlertid vurdere en annen kraftpressdemo som involverer å sykle gjennom kalendermåneder og -år.

Tenk deg at du bygde en kalenderwidget og du ville at brukeren skulle gå gjennom måneder og år på kalenderen. Dette høres ut som et ganske fint bruksområde for kraftpressing.

Her er et skjermbilde av demoen:

I denne demoen er det lagt til litt krydder til kraftpressen for å aktivere nøkkeldeteksjon. Legg merke til at når SHIFT tasten trykkes, skifter syklingen fra måneder til år.

Legg også merke til at hastigheten på sykling gjennom månedene er høyere enn syklingen gjennom årene.

Implementere noe slikt med setTimeout() og vanilje JavaScript vil være ganske komplisert. Det er imidlertid mye enklere med RxJS.

Følgende kodebit viser implementeringen. Måneds- og årssykkelfunksjonene er utelatt for korthets skyld:

import { fromEvent, timer, merge } from 'rxjs';
import { map, switchMap, startWith, takeUntil, filter, distinctUntilChanged } from 'rxjs/operators';

const control = document.getElementById('calendar-month-control');
const buttons = control.querySelectorAll('button');

const documentMouseup$ = fromEvent(document, 'mouseup');

const documentKeydownShifting$ = fromEvent(document, 'keydown').pipe(
  map(evt => {
    evt.preventDefault();
    return evt.shiftKey ? true : null;
  })
);

const documentKeyupShifting$ = fromEvent(document, 'keyup').pipe(
  map(evt => {
    evt.preventDefault();
    return evt.shiftKey ? null : false;
  })
);

const shifting = (initial = false) => {
  return merge(documentKeydownShifting$, documentKeyupShifting$).pipe(
    startWith(initial),
    filter(pressed => typeof pressed === 'boolean')
  );
};

const forcepress = evt => {
  evt.preventDefault();
  const next = evt.target.getAttribute('data-direction') === 'next';
  
  return shifting(evt.shiftKey).pipe(
    distinctUntilChanged(),
    switchMap(shift => {
      const period = shift ? 200 : 150;
      
      const fn = shift
        ? next ? nextYear : previousYear
        : next ? nextMonth : previousMonth;
      
      return timer(100, period).pipe(
        map(fn)
      );
    }),
    takeUntil(documentMouseup$)
  );
};

buttons.forEach($button => {
  fromEvent($button, 'mousedown').pipe(
    switchMap(forcepress)
  ).subscribe();
});

Jeg lar deg finne ut hvordan kodebiten fungerer i dette eksemplet. Du kan imidlertid få en live demo på Code Sandbox .

Konklusjon

RxJS er et veldig kraftig bibliotek for å komponere asynkrone hendelser og sekvenser. Den kan brukes til å bygge komplekse asynkrone programmer som ikke kan bygges enkelt ved å bruke vanlig JavaScript.

I denne opplæringen har vi lært hvordan du implementerer forbedret kraftpressing (trykk og hold ) ved å bruke RxJS. Selv om vi fokuserte på tvangspressing på musehendelser, kan det samme også implementeres for tastaturhendelser.

Klapp og følg

Hvis du syntes denne artikkelen var innsiktsfull, kan du gjerne gi noen runder med applaus hvis du ikke har noe imot det.

Du kan også følge meg på Medium (Glad Chinda) for mer innsiktsfulle artikler du kan finne nyttige. Du kan også følge meg på Twitter (@gladchinda).

Gled deg over koding...