Angular 2 et la programmation réactive
"Angular is a data-driven framework"
tout le monde
"$scope events in Angular are terrible!"
presque tout le monde
"Angular 2 is a reactive framework"
Victor Savkin
D'après le reactive manifesto, les applications réactives sont :
-
« Responsive » : capable d’offrir une expérience utilisateur optimale
-
« Resilient » : de mieux tolérer les erreurs et les pannes
-
« Elactic » : de mieux utiliser la puissance des machines
-
« Message-driven » : dirigée par les événements
On parle de modèle de développement :
-
« event driven » : dirigé par les événements
-
« push based application », les données sont poussées dès qu’elles sont disponibles
-
« Hollywood » : Don't call us, we'll call you
-
« réactif », qui réagit aux événements
Idéal pour les applications qui interagissent en temps réel avec les utilisateurs
Events vs Data
Il est facile de travailler avec des données
Il existe pleins d'outils pour manipuler des données
var numbers = [1, 2, 3, 4, 5];
function double(number) {
return number * 2;
}
var doubledNumbers = numbers.map(double);
//[2, 4, 6, 8, 10]
.map
var numbers = [1, 2, 3, 4, 5];
function greaterThanThree(value) {
return value > 3;
}
var numbersGreaterThanThree =
numbers.filter(greaterThanThree);
//[4, 5]
.filter
var numbers = [1, 2, 3, 4, 5];
function sum(total, value) {
return total + value;
}
var total = numbers.reduce(sum, 0);
//15
.reduce
function(x) { return x + 1; }
(x) => { return x + 1; }
x => x + 1
ES6 arrow functions
var numbers = [1, 2, 3, 4, 5];
var doubledNumbers = numbers.map(n => n * 2);
//[2, 4, 6, 8, 10]
ES6 Iterables/Iterators
Les notions iterable/iterator permettent de créer une collection
Quelque chose est iterable si on peut parcourir ses valeurs
On peut demander (« pull ») la prochaine valeur
On utilise un iterator pour accéder à ces valeurs
var numbers = [1, 2, 3];
//.values() returns an iterator
var numIterator = numbers.values();
numIterator.next() //{value: 1, done: false }
numIterator.next() //{value: 2, done: false }
numIterator.next() //{value: 3, done: false }
numIterator.next() //{done: true}
Array as Iterables
var mySet = new Set();
mySet.add(1);
mySet.add(2);
mySet.add(3);
//.values() returns an iterator
var numIterator = mySet.values();
numIterator.next() //{value: 1, done: false }
numIterator.next() //{value: 2, done: false }
numIterator.next() //{value: 3, done: false }
numIterator.next() //{done: true}
Set as Iterables
var myMap = new Map();
myMap.set('a', 1);
myMap.set('b', 2);
myMap.set('c', 3);
//.values() returns an iterator
var valueIterator = myMap.values();
valueIterator.next() //{value: 1, done: false }
valueIterator.next() //{value: 2, done: false }
valueIterator.next() //{value: 3, done: false }
valueIterator.next() //{done: true}
//.keys() returns an iterator
var keyIterator = myMap.keys();
keyIterator.next() //{value: 'a', done: false }
keyIterator.next() //{value: 'b', done: false }
keyIterator.next() //{value: 'c', done: false }
keyIterator.next() //{done: true}
//.entries() returns an iterator
var entryIterator = myMap.entries();
entryIterator.next() //{value: ['a', 1], done: false }
entryIterator.next() //{value: ['b', 2], done: false }
entryIterator.next() //{value: ['c', 3], done: false }
entryIterator.next() //{done: true}
Map as Iterables
function* lazyNumbers(){
yield 1;
yield 2;
yield 3;
}
//generator functions return an iterator
var valueIterator = lazyNumbers();
valueIterator.next() //{value: 1, done: false }
valueIterator.next() //{value: 2, done: false }
valueIterator.next() //{value: 3, done: false }
valueIterator.next() //{done: true}
Generators
C'est top !
Mais...
Cela fonctionne uniquement avec des objets synchrones :
Si on demande une valeur à partir d'une collection (« pull »), elle doit être disponible tout de suite
Async
Si synchronous as « pull »...
alors asynchronous as « push »
Avec Javascript, l'Async est géré par les événements :
La boucle d'événement Javascript nous pousse (« push ») des valeurs
var myButton = document.getElementById('myButton');
function doSomethingOnClick(event){
doSomething(event);
//make sure you remember to tidy up after yourself!
myButton.removeEventListener('click', doSomethingOnClick);
}
myButton.addEventListener('click', doSomethingOnClick);
Evénements du DOM
Les callbacks sont des événements !
getStuff(function(result){
getMoreStuff(function(results){
getSomeStuffForEachResult(results, function(moreStuff){
// note my code running off the page...^
doTheThingWeWantedToDo(moreStuff);
});
});
});
(ugh)
function getStuff(url, callback){
var req = new XMLHttpRequest();
function reqListener () {
callback(JSON.parse(req.responseText));
}
req.addEventListener("load", reqListener);
req.open("GET", url);
req.send();
}
getStuff('foos.json', function(foos){
getStuff('bars.json', function(bars){
//do stuff with foos and bars
});
});
XMLHttpRequest
Allô ? Et les Promises alors ?
fetch('foos.json')
.then(function(res){
return res.json();
})
.then(function(foos){
return fetch('bars.json')
.then(function(res){
return res.json();
})
.then(function(bars){
return [foos, bars];
})
})
.then(function(results){
//do stuff with foos and bars
});
fetch
Les Promises, c'est top !
Mais...
Les Promises ne résolvent pas tout
On ne peut pas annuler une promise
Les Promises sont idéales pour gérer une valeur
Mais ce n'est pas le cas lorsqu'il faut gérer plusieurs valeurs
Et si on pouvait unifier la manipulation des événements de la même manière que les iterables/iterators unifient la manipulation des données ?
...gérer plusieurs valeurs facilement...
...libérer les ressources automatiquement...
...annuler ce qui n'est plus nécessaire...
et...
...vous vous rappelez des librairies gérant les données ? map/filter/reduce/etc ?
Et si on pouvait les utiliser également pour manipuler nos événements ?
Comment ?
"Reactive programming is programming with asynchronous data streams"
Un « stream » c'est simplement une collection qui se constitue dans le temps
Collection
Stream
[ 1, 2, 3]
1
2
3
Observables
Un Observable c'est comme une collection...
Sauf que les données arrivent dans le temps, de manière asynchrone
Un Observable c'est comme une Promise...
Sauf que l'on peut manipuler plusieurs valeurs
On peut les annuler
Et en plus, on peut utiliser
.map, .filter, .reduce
(et bien plus encore !)
An API for asynchronous programming
with observable streams
The Observer pattern done right
ReactiveX is a combination of the best ideas from
the Observer pattern, the Iterator pattern, and functional programming
Available in Javascript with RxJS
Observable API
//Observable constructor
const myObservable = new Observable(observer => {
//the observer lets us *push* values
observer.next(1);
observer.next(2);
observer.next(3);
//it lets us propagate errors
observer.error('oops');
//and lets us (optionally) complete the stream
observer.complete();
});
Observable API
//Observable constructor
const myObservable = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
myObservable.subscribe(
val => console.log(val),
err => console.log(err),
_ => console.log('done')
);
Observable API
//Observable constructor
const myObservable = new Observable(observer => {
let count = 0;
const interval = setInterval(() => {
observer.next(count++);
}, 100);
//disposal function
return () => {
clearInterval(interval);
}
});
const subscriber = myObservable.subscribe(
val => console.log(val),
err => console.log(err),
_ => console.log('done')
);
subscriber.unsubscribe();
Observable en image
(Marble diagram)
Observable en ASCII
--a---b-c---d---X---|->
a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
(Marble diagram)
Duality
function* values(){
yield 1;
yield 2;
yield 3;
}
const i = values();
i.next() //{value: 1, done: false}
i.next() //{value: 2, done: false}
i.next() //{value: 3, done: false}
i.next() //{done: true}
const o = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
o.subscribe(
value => console.log(value),
err => console.error(err),
_ => console.log('done')
);
Pull
Push
Promise to not use Promise
Brian Holt (NetFlix)
Single return value | Multiple return values | |
Pull/Synchronous/Interactive | object | Iterables |
Push/Asynchronous/Reactive | Promise | Observables |
Si on résume...
const myButton = document.getElementById('myButton');
const clicks$ = new Observable(observer => {
const onClick = ev => observer.next(ev);
myButton.addEventListener('click', onClick);
return () => {
myButton.removeEventListener('click', onClick);
}
});
const clickListener = clicks$.subscribe(event => console.log(event));
//later...
clickListener.unsubscribe();
DOM events
const myButton = document.getElementById('myButton');
const clicks$ = Observable.fromEvent(myButton, 'click');
const clickListener = clicks$.subscribe(event => console.log(event));
//ClickEvent
//ClickEvent
//ClickEvent
//later...
clickListener.unsubscribe();
DOM Events
const myInput = document.getElementById('myInput');
//stream of keyup Events
const keyups$ = Observable.fromEvent(myInput, 'keyup');
//map to the values
const inputs$ = keyups$.map(ev => ev.target.value);
inputs$.subscribe(text => console.log(text));
//h
//he
//hel
//hell
//hello
.map
---1----8--3----2------6-->
vvvvv map(v => v + 1) vvvv
---2----9--4----3------7-->
const incrementButton = document.getElementById('increment');
const decrementButton = document.getElementById('decrement');
const counterOutput = document.getElementById('output');
const getValue = ev => parseInt(ev.target.value,10);
const increments$ = Observable.fromEvent(incrementButton, 'click');
const decrements$ = Observable.fromEvent(decrementButton, 'click');
//merge into a single stream
const changes$ = Observable.merge(increments$, decrements$);
//map to int values
const values$ = changes$.map(getValue);
//scan (reduce) to track the state
const total$ = values$.scan((total, value) => total + value, 0);
//set count
total$.subscribe(count => {
counterOutput.innerText = count;
console.log(count);
});
.scan/.reduce
.scan/.reduce en image
incrementStream(input): --(in)---(in)----------(in)--->
decrementStream(input): ---------------(de)----------->
changesStream(merge): --(in)---(in)--(de)----(in)--->
valuesStream (map): --(+1)---(+1)--(-1)----(+1)--->
totalStream (scan): --(+1)---(+2)--(+1)----(+2)--->
La démo en ASCII
Angular 2
import { Http } from '@angular/http'
const myInput = document.getElementById('myInput');
//stream of keyups Events
const keyups$ = Observable.fromEvent(myInput, 'keyup');
//map to the values
const inputs$ = keyups$.map(ev => ev.target.value);
//flatMap our inputs to *http responses*
const responses$ = inputs$
.flatMap(text => Http.get(`foo.com/search/${text}`))
.map(res => res.json());
responses$.subscribe(text => console.log(text));
//[{name: 'harry'},{name: 'hettie'}, {name: 'hellboy'}];
//[{name: 'harry'}, {name: 'hellboy'}];
//[{name: 'hellboy'}];
Angular 2 Http (with .flatMap)
.flatMap en image
requestStream: --a-----b--c--------------|->
responseStream: -----A--------B-----C-----|->
(la requête en minuscule, la réponse en majuscules)
import { Http } from '@angular/http'
const myInput = document.getElementById('myInput');
//stream of click Events
const clicks$ = Observable.fromEvent(myInput, 'keyup');
//map to the values
const inputs$ = clicks$.map(ev => ev.target.value);
//flatMap our inputs to *http responses*
const responses$ = inputs$
.switchMap(text => Http.get(`foo.com/search/${text}`))
.map(res => res.json());
responses$.subscribe(text => console.log(text));
//[{name: 'harry'},{name: 'hettie'}, {name: 'hellboy'}];
//[{name: 'harry'}, {name: 'hellboy'}];
//[{name: 'hellboy'}];
.switchMap (.flatMapLatest)
const responses$ =
http.get('somebadconnection.json')
.retry(3)
.map(res => res.json());
responses$.subscribe(
res => console.log(res),
err => console.log('could not connect!')
);
Angular2 Http Retry
const ticks$ = Observable.interval(5000);
const responses$ =
ticks$
.switchMap(() => http.get('stocks.json'))
.map(res => res.json());
const stockPoller = responses$.subscribe(res => console.log(res));
//later
stockPoller.unsubscribe();
Angular2 Http Polling
class MyAutoCompleteComponent {
constructor(http:Http, formBuilder:FormBuilder){
this.searchForm = formBuilder.group({
name: ['', Validators.required]
});
this.people = searchForm.controls.name.valueChanges
.switchMap(text => Http.get(`foo.com/search/${text}`))
.map(res => res.json());
}
}
Angular2 Reactive Forms...
<form novalidate [formGroup]="searchForm">
<input type="text" formControlName="name">
</form>
<div>
<ul>
<li *ngFor="let person of people | async">{{person.name}}</li>
</ul>
</div>
...and Async Pipe
Angular 2 routing
import { Http } from '@angular/http';
import { ActivatedRoute } from '@angular/router';
export class MyComponent {
constructor(
private http: Http,
private route: ActivatedRoute) {}
ngOnInit() {
this.data = this.route.params
.map(params => params['id'])
.switchMap(id => this.http.get('${id}/data.json'));
}
}
Angular 2 & ngrx/store (Redux)
import { Store } from '@ngrx/store';
import { INCREMENT, DECREMENT, RESET } from './counter';
interface AppState {
counter: number;
}
@Component({
selector: 'my-app',
template: `
<button (click)="increment()">Increment</button>
<div>Current Count: {{ counter | async }}</div>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset Counter</button>
`
})
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
increment(){
this.store.dispatch({ type: INCREMENT });
}
decrement(){
this.store.dispatch({ type: DECREMENT });
}
reset(){
this.store.dispatch({ type: RESET });
}
}
Ressources
Questions ?
Angular 2 et la programmation réactive
By Bruno Baia
Angular 2 et la programmation réactive
Angular 2 adopte les Observables et la programmation Reactive. Apprenez ce qu'est un Observable et comment Angular 2 les utilise pour créer des applications avec une expérience utilisateur optimale.
- 1,601