stream()
Descrizione
Uno stream è una struttura dati reattiva, simile alle celle di un foglio di calcolo.
Ad esempio, in un foglio di calcolo, se A1 = B1 + C1
, modificando il valore di B1
o C1
il valore di A1
cambia automaticamente.
Allo stesso modo, puoi definire uno stream in modo che dipenda da altri stream: la modifica del valore di uno aggiornerà automaticamente l'altro. Questo è utile quando hai calcoli complessi e vuoi eseguirli solo quando necessario, invece che, ad esempio, ad ogni ridisegno.
Gli stream NON sono inclusi nella distribuzione principale di Mithril.js. Per includere il modulo Stream, usa:
var Stream = require('mithril/stream');
Puoi anche scaricare il modulo direttamente se il tuo ambiente non supporta un sistema di bundling.
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
Quando viene caricata direttamente con un tag <script>
(invece di essere richiesto), la libreria stream sarà esposta come window.m.stream
. Se window.m
è già definito (ad esempio, perché usi anche lo script principale di Mithril.js), si attaccherà all'oggetto esistente. Altrimenti crea un nuovo window.m
. Se desideri utilizzare gli stream insieme a Mithril.js tramite tag script, dovresti includere Mithril.js nella tua pagina prima di mithril/stream
, perché altrimenti mithril
sovrascriverà l'oggetto window.m
definito da mithril/stream
. Questo non è un problema quando le librerie vengono utilizzate come moduli CommonJS (usando require(...)
).
Firma
Crea uno stream.
stream = Stream(value)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
value | any | No | Se presente, imposta il valore iniziale dello stream. |
restituisce | Stream | Restituisce uno stream. |
Membri statici
Stream.combine
Crea uno stream calcolato che si aggiorna in modo reattivo quando uno qualsiasi degli stream da cui dipende viene aggiornato. Vedi combinazione di stream.
stream = Stream.combine(combiner, streams)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
combiner | (Stream..., Array) -> any | Sì | Vedi l'argomento combiner. |
streams | Array<Stream> | Sì | Un array di stream da combinare. |
restituisce | Stream | Restituisce uno stream. |
combiner
Specifica come viene generato il valore dello stream calcolato. Vedi combinazione di stream.
any = combiner(streams..., changed)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
streams... | splat di Streams | No | Elenco di zero o più stream che corrispondono agli stream passati come secondo argomento a stream.combine . |
changed | Array<Stream> | Sì | Elenco degli stream che sono stati modificati da un aggiornamento. |
restituisce | any | Restituisce un valore calcolato. |
Stream.merge
Crea uno stream il cui valore è un array contenente i valori di un altro array di stream.
stream = Stream.merge(streams)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
streams | Array<Stream> | Sì | Un array di stream. |
restituisce | Stream | Restituisce uno stream il cui valore è un array di valori degli stream di input. |
Stream.scan
Crea un nuovo stream con i risultati della funzione chiamata su ogni valore dello stream, utilizzando un accumulatore e il valore in ingresso.
Nota che puoi impedire l'aggiornamento degli stream dipendenti restituendo il valore speciale stream.SKIP
.
stream = Stream.scan(fn, accumulator, stream)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
fn | (accumulator, value) -> result | SKIP | Sì | Una funzione che accetta un accumulatore e un valore, e restituisce un nuovo valore accumulatore dello stesso tipo. |
accumulator | any | Sì | Il valore iniziale per l'accumulatore. |
stream | Stream | Sì | Lo stream contenente i valori. |
restituisce | Stream | Restituisce un nuovo stream contenente il risultato. |
Stream.scanMerge
Prende un array di coppie stream/funzione scan e unisce tutti quegli stream usando le funzioni fornite in un singolo stream.
stream = Stream.scanMerge(pairs, accumulator)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | Sì | Un array di tuple stream/funzione scan. |
accumulator | any | Sì | Il valore iniziale per l'accumulatore. |
restituisce | Stream | Restituisce un nuovo stream contenente il risultato. |
Stream.lift
Crea uno stream calcolato che si aggiorna in modo reattivo quando uno qualsiasi degli stream da cui dipende viene aggiornato. Vedi combinazione di stream. A differenza di combine
, gli stream di input sono passati come un numero variabile di argomenti (invece di un array) e la callback riceve i valori degli stream invece degli stream stessi. Non c'è un parametro changed
. Questa è generalmente una funzione più intuitiva per le applicazioni rispetto a combine
.
stream = Stream.lift(lifter, stream1, stream2, ...)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
lifter | (any...) -> any | Sì | Vedi l'argomento lifter. |
streams... | lista di Streams | Sì | Stream da combinare. |
restituisce | Stream | Restituisce uno stream. |
lifter
Specifica come viene generato il valore dello stream calcolato. Vedi combinazione di stream.
any = lifter(streams...)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
streams... | splat di Streams | No | Elenco di zero o più valori che corrispondono ai valori degli stream passati a stream.lift . |
restituisce | any | Restituisce un valore calcolato. |
Stream.SKIP
Un valore speciale che può essere restituito dalle callback degli stream per saltare l'esecuzione degli stream dipendenti.
Stream["fantasy-land/of"]
Questo metodo è funzionalmente identico a stream
. Esiste per conformarsi alla specifica Applicative di Fantasy Land. Per maggiori informazioni, vedi la sezione Cos'è Fantasy Land.
stream = Stream["fantasy-land/of"](value)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
value | any | No | Se presente, imposta il valore iniziale dello stream. |
restituisce | Stream | Restituisce uno stream. |
Membri dell'istanza
stream.map
Crea uno stream dipendente il cui valore è impostato sul risultato della funzione di callback. Questo metodo è un alias di stream["fantasy-land/map"]
.
dependentStream = stream().map(callback)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
callback | any -> any | Sì | Una callback il cui valore di ritorno diventa il valore dello stream. |
restituisce | Stream | Restituisce uno stream. |
stream.end
Uno stream co-dipendente che annulla la registrazione degli stream dipendenti quando impostato su true
. Vedi stato ended.
endStream = stream().end
stream["fantasy-land/of"]
Questo metodo è funzionalmente identico a stream
. Esiste per conformarsi alla specifica Applicative di Fantasy Land. Per maggiori informazioni, vedi la sezione Cos'è Fantasy Land.
stream = stream()["fantasy-land/of"](value)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
value | any | No | Se presente, imposta il valore iniziale dello stream. |
restituisce | Stream | Restituisce uno stream. |
stream["fantasy-land/map"]
Crea uno stream dipendente il cui valore è impostato sul risultato della funzione di callback. Vedi concatenamento di stream.
Questo metodo esiste per conformarsi alla specifica Applicative di Fantasy Land. Per maggiori informazioni, vedi la sezione Cos'è Fantasy Land.
dependentStream = stream()["fantasy-land/map"](callback)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
callback | any -> any | Sì | Una callback il cui valore di ritorno diventa il valore dello stream. |
restituisce | Stream | Restituisce uno stream. |
stream["fantasy-land/ap"]
Il nome di questo metodo sta per apply
(applica). Se uno stream a
ha una funzione come suo valore, un altro stream b
può usarlo come argomento per b.ap(a)
. Chiamare ap
chiamerà la funzione con il valore dello stream b
come suo argomento, e restituirà un altro stream il cui valore è il risultato della chiamata di funzione. Questo metodo esiste per conformarsi alla specifica Applicative di Fantasy Land. Per maggiori informazioni, vedi la sezione Cos'è Fantasy Land.
stream = stream()["fantasy-land/ap"](apply)
Argomento | Tipo | Richiesto | Descrizione |
---|---|---|---|
apply | Stream | Sì | Uno stream che contiene una funzione. |
restituisce | Stream | Restituisce uno stream. |
Utilizzo base
Gli stream non sono inclusi nella distribuzione principale di Mithril.js. Per includerli in un progetto, richiedi il modulo:
var stream = require('mithril/stream');
Stream come variabili
stream()
restituisce uno stream. A livello più elementare, uno stream funziona in modo simile a una variabile o a una proprietà getter-setter: può contenere uno stato, che può essere modificato.
var username = stream('John');
console.log(username()); // stampa "John"
username('John Doe');
console.log(username()); // stampa "John Doe"
La differenza principale è che uno stream è una funzione, e quindi può essere composto in funzioni di ordine superiore.
var users = stream();
// request users from a server using the fetch API
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
Nell'esempio sopra, lo stream users
viene popolato con i dati di risposta quando la richiesta viene completata.
Binding bidirezionali
Gli stream possono anche essere aggiornati tramite callback di eventi e simili.
// uno stream
var user = stream('');
// un binding bidirezionale allo stream
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
Nell'esempio sopra, quando l'utente inserisce un valore nell'input, lo stream user
viene aggiornato con il valore del campo di input.
Proprietà calcolate
Gli stream sono utili per implementare proprietà calcolate.
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // stampa "hello-world"
Nell'esempio sopra, il valore di slug
viene calcolato quando title
viene aggiornato, non quando slug
viene letto.
È possibile calcolare proprietà basate anche su più stream.
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // stampa "John Doe"
firstName('Mary');
console.log(fullName()); // stampa "Mary Doe"
Le proprietà calcolate in Mithril.js vengono aggiornate in modo atomico: gli stream che dipendono da più stream non verranno mai chiamati più di una volta per aggiornamento del valore, non importa quanto sia complesso il grafico di dipendenza della proprietà calcolata.
Concatenamento di stream
Gli stream possono essere concatenati utilizzando il metodo map
. Uno stream concatenato è anche noto come stream dipendente.
// stream genitore
var value = stream(1);
// stream dipendente
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // stampa 2
Gli stream dipendenti sono reattivi: i loro valori vengono aggiornati ogni volta che il valore del loro stream genitore viene aggiornato. Questo accade indipendentemente dal fatto che lo stream dipendente sia stato creato prima o dopo che il valore dello stream genitore sia stato impostato.
Puoi impedire l'aggiornamento degli stream dipendenti restituendo il valore speciale stream.SKIP
.
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// non viene mai eseguito
});
Combinazione di stream
Gli stream possono dipendere da più di uno stream genitore. Questi tipi di stream possono essere creati utilizzando stream.merge()
var a = stream('hello');
var b = stream('world');
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
console.log(greeting()); // stampa "hello world"
Oppure puoi usare la funzione helper stream.lift()
var a = stream('hello');
var b = stream('world');
var greeting = stream.lift(
function (_a, _b) {
return _a + ' ' + _b;
},
a,
b
);
console.log(greeting()); // stampa "hello world"
Esiste anche un metodo di livello inferiore chiamato stream.combine()
che espone gli stream stessi nei calcoli reattivi per casi d'uso più avanzati.
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // stampa 12
Uno stream può dipendere da un numero qualsiasi di stream ed è garantito che si aggiorni atomicamente. Ad esempio, se uno stream A ha due stream dipendenti B e C, e un quarto stream D dipende sia da B che da C, lo stream D si aggiornerà solo una volta se il valore di A cambia. Questo garantisce che la callback per lo stream D non venga mai chiamata con valori inconsistenti, ad esempio quando B ha un nuovo valore ma C ha il vecchio valore. L'atomicità porta anche vantaggi in termini di prestazioni, evitando ricalcoli inutili degli stream dipendenti.
Puoi impedire l'aggiornamento degli stream dipendenti restituendo il valore speciale stream.SKIP
.
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// non viene mai eseguito
});
Stati dello stream
In un dato momento, uno stream può trovarsi in uno dei tre stati: pending (in sospeso), active (attivo) ed ended (terminato).
Stato pending
Gli stream in sospeso possono essere creati chiamando stream()
senza parametri.
var pending = stream();
Se uno stream dipende da più di uno stream e uno qualsiasi dei suoi stream genitori è in uno stato in sospeso, anche lo stream dipendente è in uno stato in sospeso e non aggiorna il suo valore.
var a = stream(5);
var b = stream(); // pending stream
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // stampa undefined
Nell'esempio sopra, added
è uno stream in stato di attesa, perché il suo genitore b
è anch'esso in sospeso.
Questo vale anche per gli stream dipendenti creati tramite stream.map
:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // stampa undefined perché `doubled` è in sospeso
Stato active
Quando uno stream riceve un valore, diventa attivo (a meno che lo stream non sia terminato).
var stream1 = stream('hello'); // stream1 è attivo
var stream2 = stream(); // stream2 inizia in sospeso
stream2('world'); // poi diventa attivo
Uno stream dipendente con più genitori diventa attivo se tutti i suoi genitori sono attivi.
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
Nell'esempio sopra, lo stream a
è attivo, ma b
è in sospeso. Impostare b("world")
farebbe sì che b
diventasse attivo, e quindi anche greeting
diventerebbe attivo e verrebbe aggiornato per avere il valore "hello world"
.
Stato ended
Uno stream può smettere di influenzare i suoi stream dipendenti chiamando stream.end(true)
. Questo rimuove la connessione tra uno stream e i suoi stream dipendenti.
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // impostato allo stato ended
value(5);
console.log(doubled());
// stampa undefined perché `doubled` non dipende più da `value`
Gli stream terminati mantengono comunque una semantica di contenitore di stato, cioè puoi ancora usarli come getter-setter, anche dopo che sono stati terminati.
var value = stream(1);
value.end(true); // impostato allo stato ended
console.log(value(1)); // stampa 1
value(2);
console.log(value()); // stampa 2
Terminare uno stream può essere utile nei casi in cui uno stream ha una durata limitata (ad esempio, reagire agli eventi mousemove
solo mentre un elemento DOM viene trascinato, ma non dopo che è stato rilasciato).
Serializzazione di stream
Gli stream implementano un metodo .toJSON()
. Quando uno stream viene passato come argomento a JSON.stringify()
, il valore dello stream viene serializzato.
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // stampa 123
Gli stream non attivano il rendering
A differenza di librerie come Knockout, gli stream di Mithril.js non attivano il re-rendering dei template. Il ridisegno avviene in risposta ai gestori di eventi definiti nelle viste dei componenti di Mithril.js, alle modifiche di rotta oppure dopo che le chiamate a m.request
sono state completate.
Se desideri il ridisegno in risposta ad altri eventi asincroni (ad esempio setTimeout
/setInterval
, sottoscrizione websocket, gestore di eventi di librerie di terze parti, ecc.), è necessario chiamare manualmente m.redraw()
.
Cos'è Fantasy Land
Fantasy Land specifica l'interoperabilità delle strutture algebriche comuni. In parole povere, ciò significa che le librerie conformi alle specifiche di Fantasy Land possono essere utilizzate per scrivere codice generico in stile funzionale, che funziona indipendentemente da come queste librerie implementano i costrutti.
Ad esempio, supponiamo di voler creare una funzione generica chiamata plusOne
.
function plusOne(a) {
return a + 1;
}
Il problema con questa implementazione è che può essere utilizzata solo con numeri. Tuttavia, è possibile che qualsiasi logica che produce un valore per a
potrebbe anche produrre uno stato di errore (avvolto in un Maybe o un Either da una libreria come Sanctuary o Ramda-Fantasy), oppure potrebbe essere uno stream di Mithril.js, uno stream di Flyd, ecc. Idealmente, non vorremmo scrivere una versione simile della stessa funzione per ogni possibile tipo che a
potrebbe avere e non vorremmo scrivere ripetutamente codice di wrapping, unwrapping e gestione degli errori.
È qui che Fantasy Land può essere utile. Riscriviamo quella funzione in termini di un'algebra di Fantasy Land:
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
Ora questo metodo funziona con qualsiasi Functor conforme a Fantasy Land, come R.Maybe
, S.Either
, stream
, ecc.
Questo esempio potrebbe sembrare complesso, ma è un compromesso in termini di complessità: l'implementazione naive di plusOne
ha senso se hai un sistema semplice e incrementi solo numeri, ma l'implementazione di Fantasy Land diventa più potente se hai un sistema grande con molte astrazioni di wrapper e algoritmi riutilizzati.
Quando decidi se adottare Fantasy Land, dovresti considerare la familiarità del tuo team con la programmazione funzionale ed essere realistico riguardo al livello di disciplina che il tuo team può impegnarsi a mantenere la qualità del codice (rispetto alla pressione di scrivere nuove funzionalità e rispettare le scadenze). La programmazione in stile funzionale dipende fortemente dalla compilazione, dalla cura e dalla padronanza di un ampio set di funzioni piccole e definite con precisione, e quindi non è adatta ai team che non hanno solide pratiche di documentazione e/o mancano di esperienza in linguaggi orientati alla funzionalità.