Zabawa Koralikami
Łukasz Karpuć, Marzec/Kwiecień 2019
Czym jest RxJS?
RxJS, czyli...
- API wspomagające programowanie reaktywne
- podejście do problemu Producenta i Konsumenta
- implementacja wzorca Obserwatora
- członek rodziny ReactiveX (Extensions)
Programowanie reaktywne
Programowanie reaktywne
onClick() {
makeSquareRed();
makeCirleRed();
}
onMouseOver() {
makeSquareGreen();
makeCircleGreen();
}
onKeypress() {
makeSquareBlue();
makeCirleBlue();
}
Programowanie reaktywne
onClick() {
makeSquareRed();
makeCirleRed();
makeTriangleRed();
}
onMouseOver() {
makeSquareGreen();
makeCircleBlue();
makeTriangleGreen();
}
onKeypress() {
makeSquareBlue();
makeCirleBlue();
makeTriangleBlue();
}
Programowanie reaktywne
onClick() {
color = 'red'
}
onMouseOver() {
color = 'green'
}
onKeypress() {
color = 'blue'
}
setInterval(() => renderAll(), 1000)
Programowanie reaktywne
onClick() {
color = 'red'
}
onMouseOver() {
color = 'green'
}
onKeypress() {
color = 'blue'
}
observeColor(() => renderAll())
Producenci i Konsumenci
Producenci i Konsumenci
Producenci i Konsumenci
SABRE
PROGRAMISTA
$$$$$$$
PUSH
Producenci i Konsumenci
PROGRAMISTA
PULL
URZĄD
SKARBOWY
$$$
Wzorzec Obserwatora
*żródło: https://sourcemaking.com
*żródło: https://sourcemaking.com
ReactiveX
RxJS
interface Observer<T> {
closed?: boolean;
next: (value: T) => void;
error: (err: any) => void;
complete: () => void;
}
interface Subscribable<T> {
subscribe(observer: Observer<T>): Subscription;
}
interface Observable<T> extends Subscribable<T> {
new (observer: Observer<T>): this;
}
interface Subject<T, R> extends Observer<T>, Subscribable<R> {
}
RxJS
interface Observer<T> {
// REACTS to data changes
}
interface Subscribable<T> {
// is a SOURCE of data
}
interface Observable<T> extends Subscribable<T> {
// DELIVERS data; is mostly stateless
}
interface Subject<T, R> extends Observer<T>, Subscribable<R> {
// PROPAGATES data; is mostly stateful
}
Observable vs Subject
Observable
Jest źródłem danych.
Nie jest odbiorcą danych.
Nie posiada stanu.
Każda subskrypcja powoduje ponowne przetworzenie kolekcji.
Subject
Jest źródłem danych.
Jest odbiorcą danych.
Posiada stan.
Subskrypcje są z nim powiązane. Nie muszą otrzymywać tych samych danych.
RxJS
// Observable example
const observable = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => {
subscriber.next(4);
subscriber.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');
// just before subscribe
// got value 1
// got value 2
// got value 3
// just after subscribe
// got value 4
// done
RxJS
// Subject example
const subject = new ReplaySubject(3); // buffer 3 values for new subscribers
subject.subscribe({
next: (v) => console.log(`observerA: ${v}`)
});
subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);
subject.subscribe({
next: (v) => console.log(`observerB: ${v}`)
});
subject.next(5);
// Logs:
// observerA: 1
// observerA: 2
// observerA: 3
// observerA: 4
// observerB: 2
// observerB: 3
// observerB: 4
// observerA: 5
// observerB: 5
RxJS
// map operator example
//emit ({name: 'Joe', age: 30}, {name: 'Frank', age: 20},{name: 'Ryan', age: 50})
const source = from([
{ name: 'Joe', age: 30 },
{ name: 'Frank', age: 20 },
{ name: 'Ryan', age: 50 }
]);
//grab each persons name, could also use pluck for this scenario
const example = source.pipe(
map(({ name }) => name)
);
//output: "Joe","Frank","Ryan"
const subscribe = example.subscribe(val => console.log(val));
RxJS
// filter example
//emit every second
const source = interval(1000);
//filter out all values until interval is greater than 5
const example = source.pipe(
filter(num => num > 5)
);
/*
"Number greater than 5: 6"
"Number greater than 5: 7"
"Number greater than 5: 8"
"Number greater than 5: 9"
*/
const subscribe = example.subscribe(val =>
console.log(`Number greater than 5: ${val}`)
);
RxJS
// scan example (continuous reduce)
const subject = new Subject();
//scan example building an object over time
const example = subject.pipe(
scan((acc, curr) => Object.assign({}, acc, curr), {})
);
//log accumulated values
const subscribe = example.subscribe(val => console.log('Accumulated object:', val));
//next values into subject, adding properties to object
// {name: 'Joe'}
subject.next({ name: 'Joe' });
// {name: 'Joe', age: 30}
subject.next({ age: 30 });
// {name: 'Joe', age: 30, favoriteLanguage: 'JavaScript'}
subject.next({ favoriteLanguage: 'JavaScript' });
RxJS
// custom operator example
let store = sidepanelService.getStore(); // Redux Store
let store$ = Rx.Observable.from(store); // Store's state update
let mutations$ = store$
.pipe(observeMutation(KeyValueMutation.calculateMutation)); // Store's state mutation
mutations$
.pipe(filter(it => it.mutatedKeys.visibleItem)) // Mutation of given attribute
.subscribe(it => this.getSidepanelWidget()._setVisibleItemKey(it.new.visibleItem));
mutations$
.pipe(filter(it => mutation.mutatedKeys.isWideMode)) // Mutation of given attribute
.subscribe(it => this.getSidepanelWidget()._handleWideModeChange(it.new.isWideMode));
Jakie są korzyści?
Zapełniamy lukę
Zapełniamy lukę
pull | push | |
---|---|---|
skalar | function() | Promise |
kolekcja | function*() |
Zapełniamy lukę
pull | push | |
---|---|---|
skalar | function() | Promise |
kolekcja | function*() | bservable |
Uzgadniamy język
Uzgadniamy język
- Czemu Pan ukradł to piwo?
- Herr Wysoki Sądzie, ja nic nie ukradłem... Pisało "bier", to wziąłem!
Tworzymy wspólny ekosystem
Tworzymy wspólny ekosystem
- uniwersalne operatory
- uniwersalne narzędzia
Używamy znanych funkcji/metod
Używamy znanych funkcji/metod
- map()
- filter()
- reduce()
- zip()
- concat()
- etc.
Używamy znanych funkcji/metod
Dobre katalogi operatorów na:
- https://www.learnrxjs.io
- https://rxmarbles.com
Jak to testować?
jest + rxjs-marbles
it('test with jest only', (done) => {
// given
let source = from([1, 2, 3, 4]);
let expected = [2, 4, 6, 8];
// when
let result = source.pipe(map(it => it * 2));
result.subscribe(function(value) {
// then
expect(value).toEqual(expected.shift());
if( !expected.length ) {
done();
}
});
});
jest + rxjs-marbles
it('test with rxjs-marbles', marbles(m => {
let numbers = [...new Array(10).keys()];
// given
let source = m.hot( '-0-1-2-3-|', numbers);
let subs = '^--------!';
let expected = m.cold( '-0-2-4-6-|', numbers);
// when
let result = source.pipe(map(it => it * 2));
// then
m.expect(result).toBeObservable(expected);
m.expect(source).toHaveSubscriptions(subs);
}));
jest + rxjs-marbles
it('test with jasmine-marbles', () => {
let numbers = [...new Array(10).keys()];
// given
let source = jm.hot( '-0-1-2-3-|', numbers);
let subs = '^--------!';
let expected = jm.cold( '-0-2-4-6-|', numbers);
// when
let result = source.pipe(map(it => it * 2));
// then
expect(result).toBeObservable(expected);
expect(source).toHaveSubscriptions(subs);
});
jasmine-marbles
jest + rxjs-marbles
it('test with jasmine-marbles', () => {
let numbers = [...new Array(10).keys()];
// given
let source = jm.hot( '-0-1-2-3-|', numbers);
let expected = jm.cold( '-0-2-4-6-|', numbers);
// when
let result = source.pipe(map(it => it * 2));
// then
expect(result).toBeObservable(expected);
});
jasmine-marbles
jest + rxjs-marbles
it('test with rxjs/testing', () => {
let scheduler = new TestScheduler((actual, expected) =>
expect(actual).toEqual(expected));
scheduler.run(m => {
let numbers = [...new Array(10).keys()];
// given
let source = m.hot( '-0-1-2-3-|', numbers);
let subs = '^--------!';
let expected = '-0-2-4-6-|';
// when
let result = source.pipe(map(it => it * 2));
// then
m.expectObservable(result, subs).toBe(expected, numbers);
m.expectSubscriptions(result.subscriptions).toBe(subs);
});
});
/testing ?
jest + rxjs-marbles
it('test with rxjs/testing', () => {
let scheduler = new TestScheduler((actual, expected) =>
expect(actual).toEqual(expected));
scheduler.run(m => {
let numbers = [...new Array(10).keys()];
// given
let source = m.hot( '-0-1-2-3-|', numbers);
let subs = '^--------!';
let expected = '-0-2-4-6-|';
// when
let result = source.pipe(map(it => it * 2));
// then
m.expectObservable(result, subs).toBe(expected, numbers);
m.expectSubscriptions(result.subscriptions).toBe(subs);
});
});
/testing ?
Składnia koralikowa
abo Marble Syntax
Składnia koralikowa
Składnia koralikowa
-1---2-----3-4---5-|
---A--B--CD--------|
---X-YZ--STR-W---U-|
{ X: '1A', Y: '2A', Z: '2B', /* ... */ U: '5D' }
Składnia koralikowa
-
----------
Wirtualna ramka czasu
Hot Observable / Cold Observable / Subskrypcje
Składnia koralikowa
[0-9]+[ms|s|m]
- 8ms -
Upływ czasu
Hot Observable / Cold Observable / Subskrypcje
Składnia koralikowa
|
- 8ms -|
Koniec kolekcji (.complete())
Hot Observable / Cold Observable / Subskrypcje
Składnia koralikowa
#
- 8ms -#
Błąd (.error())
Hot Observable / Cold Observable / Subskrypcje
Składnia koralikowa
[a-z0-9]
- 8ms -a-b-c-|
Kolejna wartość (.next())
Hot Observable / Cold Observable / Subskrypcje
Składnia koralikowa
()
- 8ms -a-b-(c|)
Zdarzenia równoległe
Hot Observable / Cold Observable / Subskrypcje
Składnia koralikowa
^
^ 8ms ----------
- 8ms -a^b-(c|)
Początek subskrypcji (.subscribe())
Hot Observable / Cold Observable / Subskrypcje
Składnia koralikowa
!
^ 8ms --------!
Koniec subskrypcji (.unsubscribe())
Hot Observable / Cold Observable / Subskrypcje
Składnia koralikowa
const source = hot('--a--a--a--a--a--a--a--');
const sub1 = ' --^-----------!';
const sub2 = ' ---------^--------!';
const expect1 = ' --a--a--a--a--';
const expect2 = ' -----------a--a--a-';
expectObservable(source, sub1).toBe(expect1);
expectObservable(source, sub2).toBe(expect2);
Przykład z dokumentacji
Hot vs Cold
Observable
Hot
Nie jest bezpośrednim źródłem danych.
Producent najpewniej żyje już gdzieś w systemie.
Dlatego dla tego rodzaju observable'a można zaznaczyć moment rozpoczęcia subskrypcji.
Cold
Jest bezpośrednim źródłem danych.
Tworzy producenta (w zasadzie sam observable nim jest).
That's all folks!
Post Scriptum
- http://reactivex.io/
- https://github.com/ReactiveX/rxjs
- https://www.learnrxjs.io
- https://rxmarbles.com
- https://github.com/cartant/rxjs-marbles
- lukasz.karpuc@sabre.com
Zabawa Koralikami
By Lukasz K
Zabawa Koralikami
- 341