stream()
Popis
Stream je reaktivní datová struktura, podobná buňkám v tabulkových aplikacích.
Například, v tabulce, pokud A1 = B1 + C1
, pak změna hodnoty B1
nebo C1
automaticky změní hodnotu A1
.
Podobně můžete vytvořit stream, který závisí na jiných streamech, takže změna hodnoty jednoho automaticky aktualizuje druhý. To je užitečné, když máte výpočetně náročné operace a chcete je spouštět pouze v případě potřeby, například ne při každém překreslení.
Streamy nejsou součástí základní distribuce Mithril.js. Chcete-li zahrnout modul Stream, použijte:
var Stream = require('mithril/stream');
Modul si můžete také stáhnout přímo, pokud vaše prostředí nepodporuje nástroj pro vytváření balíčků:
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
Při načítání přímo pomocí tagu <script>
(spíše než pomocí require
), bude knihovna streamů zpřístupněna jako window.m.stream
. Pokud již existuje window.m
(např. protože také používáte hlavní skript Mithril.js), připojí se k existujícímu objektu. Jinak vytvoří nový window.m
. Pokud chcete používat streamy ve spojení s Mithril.js jako surové skriptové tagy, měli byste zahrnout Mithril.js do své stránky před mithril/stream
, protože mithril
jinak přepíše objekt window.m
definovaný mithril/stream
. To se netýká případů, kdy jsou knihovny používány jako moduly CommonJS (pomocí require(...)
).
Signatura
Vytvoří nový stream.
stream = Stream(value)
Argument | Typ | Povinný | Popis |
---|---|---|---|
value | any | Ne | Pokud je tento argument přítomen, hodnota streamu je na něj nastavena. |
returns | Stream | Vrací instanci streamu. |
Statické členy
Stream.combine
Vytvoří stream, který se počítá a reaktivně aktualizuje, pokud se aktualizuje některý z jeho upstreamů. Viz kombinování streamů.
stream = Stream.combine(combiner, streams)
Argument | Typ | Povinný | Popis |
---|---|---|---|
combiner | (Stream..., Array) -> any | Ano | Viz argument combiner. |
streams | Array<Stream> | Ano | Seznam streamů, které budou kombinovány. |
returns | Stream | Vrací instanci streamu. |
combiner
Určuje, jak je generována hodnota počítaného streamu. Viz kombinování streamů.
any = combiner(streams..., changed)
Argument | Typ | Povinný | Popis |
---|---|---|---|
streams... | splat of Streams | Ne | Splat nula nebo více streamů, které odpovídají streamům předaným jako druhý argument do stream.combine . |
changed | Array<Stream> | Ano | Seznam streamů, které byly ovlivněny aktualizací. |
returns | any | Vrací vypočítanou hodnotu. |
Stream.merge
Vytvoří stream, jehož hodnota je pole hodnot z pole streamů.
stream = Stream.merge(streams)
Argument | Typ | Povinný | Popis |
---|---|---|---|
streams | Array<Stream> | Ano | Seznam streamů. |
returns | Stream | Vrací stream, jehož hodnota je pole hodnot vstupních streamů. |
Stream.scan
Vytvoří nový stream s výsledky volání funkce na každé hodnotě ve streamu s akumulátorem a příchozí hodnotou.
Všimněte si, že můžete zabránit aktualizaci závislých streamů tím, že vrátíte speciální hodnotu stream.SKIP
uvnitř akumulační funkce.
stream = Stream.scan(fn, accumulator, stream)
Argument | Typ | Povinný | Popis |
---|---|---|---|
fn | (accumulator, value) -> result | SKIP | Ano | Funkce, která bere akumulátor a hodnotu jako parametry a vrací novou hodnotu akumulátoru stejného typu. |
accumulator | any | Ano | Počáteční hodnota pro akumulátor. |
stream | Stream | Ano | Stream obsahující hodnoty. |
returns | Stream | Vrací nový stream, který obsahuje výsledek. |
Stream.scanMerge
Vezme pole párů streamů a scanovacích funkcí a sloučí všechny tyto streamy pomocí daných funkcí do jednoho streamu.
stream = Stream.scanMerge(pairs, accumulator)
Argument | Typ | Povinný | Popis |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | Ano | Pole párů streamu a scanovacích funkcí. |
accumulator | any | Ano | Počáteční hodnota pro akumulátor. |
returns | Stream | Vrací nový stream, který obsahuje výsledek. |
Stream.lift
Vytvoří stream, který se počítá a reaktivně aktualizuje, pokud se aktualizuje některý z jeho upstreamů. Viz kombinování streamů. Na rozdíl od combine
jsou vstupní streamy proměnný počet argumentů (místo pole) a callback obdrží hodnoty streamu místo streamů. Neexistuje parametr changed
. Toto je obecně uživatelsky přívětivější funkce pro aplikace než combine
.
stream = Stream.lift(lifter, stream1, stream2, ...)
Argument | Typ | Povinný | Popis |
---|---|---|---|
lifter | (any...) -> any | Ano | Viz argument lifter. |
streams... | list of Streams | Ano | Streamy, které mají být "lifted". |
returns | Stream | Vrací instanci streamu. |
lifter
Určuje, jak je generována hodnota počítaného streamu. Viz kombinování streamů.
any = lifter(streams...)
Argument | Typ | Povinný | Popis |
---|---|---|---|
streams... | splat of Streams | Ne | Splat nula nebo více hodnot, které odpovídají hodnotám streamů předaných do funkce stream.lift . |
returns | any | Vrací vypočítanou hodnotu. |
Stream.SKIP
Speciální hodnota, která může být vrácena do callbacků streamu, aby se přeskočilo spuštění downstreamů.
Stream["fantasy-land/of"]
Tato metoda je funkčně identická s stream
. Existuje, aby vyhovovala specifikaci Applicative Fantasy Land. Pro více informací viz sekce Co je Fantasy Land.
stream = Stream["fantasy-land/of"](value)
Argument | Typ | Povinný | Popis |
---|---|---|---|
value | any | Ne | Pokud je tento argument přítomen, hodnota streamu je na něj nastavena. |
returns | Stream | Vrací instanci streamu. |
Členy instance
stream.map
Vytvoří stream, který závisí na jiném a jehož hodnota je nastavena na výsledek callback funkce. Tato metoda je aliasem stream["fantasy-land/map"].
dependentStream = stream().map(callback)
Argument | Typ | Povinný | Popis |
---|---|---|---|
callback | any -> any | Ano | Callback, jehož návratová hodnota se stane hodnotou streamu. |
returns | Stream | Vrací instanci streamu. |
stream.end
Spolu-závislý stream, který odregistruje závislé streamy, když je nastaven na true. Viz stav ended (ukončený).
endStream = stream().end
stream["fantasy-land/of"]
Tato metoda je funkčně identická s stream
. Existuje, aby vyhovovala specifikaci Applicative Fantasy Land. Pro více informací viz sekce Co je Fantasy Land.
stream = stream()["fantasy-land/of"](value)
Argument | Typ | Povinný | Popis |
---|---|---|---|
value | any | Ne | Pokud je tento argument přítomen, hodnota streamu je na něj nastavena. |
returns | Stream | Vrací instanci streamu. |
stream["fantasy-land/map"]
Vytvoří stream, který závisí na jiném a jehož hodnota je nastavena na výsledek callback funkce. Viz řetězení streamů.
Tato metoda slouží k tomu, aby vyhověla specifikaci Applicative Fantasy Land. Pro více informací viz sekce Co je Fantasy Land.
dependentStream = stream()["fantasy-land/map"](callback)
Argument | Typ | Povinný | Popis |
---|---|---|---|
callback | any -> any | Ano | Callback, jehož návratová hodnota se stane hodnotou streamu. |
returns | Stream | Vrací instanci streamu. |
stream["fantasy-land/ap"]
Název této metody znamená apply
(aplikovat). Pokud má stream a
jako svou hodnotu funkci, jiný stream b
ji může použít jako argument pro b.ap(a)
. Volání ap
zavolá funkci s hodnotou streamu b
jako argumentem a vrátí jiný stream, jehož hodnota je výsledkem volání funkce. Tato metoda existuje, aby vyhovovala specifikaci Applicative Fantasy Land. Pro více informací viz sekce Co je Fantasy Land.
stream = stream()["fantasy-land/ap"](apply)
Argument | Typ | Povinný | Popis |
---|---|---|---|
apply | Stream | Ano | Stream, jehož hodnota je funkce. |
returns | Stream | Vrací instanci streamu. |
Základní použití
Streamy nejsou součástí základní distribuce Mithril.js. Chcete-li je zahrnout do projektu, vyžadujte jeho modul:
var stream = require('mithril/stream');
Streamy jako proměnné
stream()
vrací stream. Na své nejzákladnější úrovni funguje stream podobně jako proměnná nebo vlastnost s getterem a setterem: může uchovávat stav, který lze upravit.
var username = stream('John');
console.log(username()); // zobrazí "John"
username('John Doe');
console.log(username()); // zobrazí "John Doe"
Hlavní rozdíl je v tom, že stream je funkce, a proto může být složen do funkcí vyššího řádu.
var users = stream();
// požádej o uživatele ze serveru pomocí Fetch API
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
Ve výše uvedeném příkladu je stream users
naplněn daty odpovědi, když se požadavek vyřeší.
Obousměrné vazby
Streamy lze také naplnit pomocí callbacků událostí a podobně.
// stream
var user = stream('');
// obousměrná vazba na stream
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
Ve výše uvedeném příkladu, když uživatel píše do vstupu, stream user
je aktualizován na hodnotu vstupního pole.
Počítané vlastnosti
Streamy jsou užitečné pro implementaci počítaných vlastností:
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // zobrazí "hello-world"
Ve výše uvedeném příkladu je hodnota slug
vypočítána, když je title
aktualizován, ne když je slug
čten.
Je samozřejmě také možné počítat vlastnosti na základě více streamů:
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // zobrazí "John Doe"
firstName('Mary');
console.log(fullName()); // zobrazí "Mary Doe"
Počítané vlastnosti v Mithril.js jsou aktualizovány atomicky: streamy, které závisí na více streamech, nebudou nikdy volány více než jednou na aktualizaci hodnoty, bez ohledu na to, jak složitý je graf závislostí počítané vlastnosti.
Řetězení streamů
Streamy mohou být řetězeny pomocí metody map
. Stream zřetězený pomocí map
je také známý jako závislý stream.
// rodičovský stream
var value = stream(1);
// závislý stream
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // zobrazí 2
Závislé streamy jsou reaktivní: jejich hodnoty jsou aktualizovány pokaždé, když je aktualizována hodnota jejich rodičovského streamu. To se děje bez ohledu na to, zda byl závislý stream vytvořen před nebo po nastavení hodnoty rodičovského streamu.
Můžete zabránit aktualizaci závislých streamů tím, že vrátíte speciální hodnotu stream.SKIP
.
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// nikdy se nespustí
});
Kombinování streamů
Streamy mohou záviset na více než jednom rodičovském streamu. Tyto druhy streamů lze vytvořit pomocí 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()); // zobrazí "hello world"
Nebo můžete použít pomocnou funkci 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()); // zobrazí "hello world"
Existuje také metoda nižší úrovně nazvaná stream.combine()
, která zpřístupňuje samotné streamy v reaktivních výpočtech pro pokročilejší případy použití.
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // zobrazí 12
Stream může záviset na libovolném počtu streamů a je zaručeno, že se aktualizuje atomicky. Například, pokud má stream A dva závislé streamy B a C a čtvrtý stream D je závislý na B i C, stream D se aktualizuje pouze jednou, pokud se změní hodnota A. To zaručuje, že callback pro stream D není nikdy volán s nestabilními hodnotami, například když má B novou hodnotu, ale C má starou hodnotu. Atomicita také přináší výkonnostní výhody tím, že se zbytečně nepřepočítávají downstreamy.
Můžete zabránit aktualizaci závislých streamů tím, že vrátíte speciální hodnotu stream.SKIP
.
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// nikdy se nespustí
});
Stavy streamu
V daném okamžiku může být stream v jednom ze tří stavů: pending, active a ended.
Stav pending (čekající)
Streamy ve stavu pending lze vytvořit voláním stream()
bez parametrů.
var pending = stream();
Pokud stream závisí na více než jednom rodičovském streamu a některý z jeho rodičovských streamů je ve stavu pending, je závislý stream také ve stavu pending a neaktualizuje svou hodnotu.
var a = stream(5);
var b = stream(); // čekající stream
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // zobrazí undefined
Ve výše uvedeném příkladu je added
čekající stream, protože jeho rodič b
je také čekající.
To platí také pro závislé streamy vytvořené pomocí stream.map
:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // zobrazí undefined, protože `doubled` je čekající
Stav active (aktivní)
Když stream obdrží hodnotu, je ve stavu active (pokud stream není ukončen).
var stream1 = stream('hello'); // stream1 je ve stavu active
var stream2 = stream(); // stream2 začíná jako čekající
stream2('world'); // pak se stane aktivním
Závislý stream s více rodiči je ve stavu active, pokud jsou všichni jeho rodiče aktivní.
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
Ve výše uvedeném příkladu je stream a
aktivní, ale b
je čekající. Nastavení b("world")
by způsobilo, že se b
stane aktivním, a proto by se greeting
také stal aktivním a byl by aktualizován tak, aby měl hodnotu "hello world"
.
Stav ended (ukončený)
Stream může přestat ovlivňovat své závislé streamy voláním stream.end(true)
. Tím se efektivně odstraní spojení mezi streamem a jeho závislými streamy.
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // nastavit na stav ended
value(5);
console.log(doubled());
// zobrazí undefined, protože `doubled` již nezávisí na `value`
Ukončené streamy si stále zachovávají sémantiku kontejneru stavu, tj. můžete je stále používat jako getter-settery, i když jsou ukončeny.
var value = stream(1);
value.end(true); // nastavit na stav ended
console.log(value(1)); // zobrazí 1
value(2);
console.log(value()); // zobrazí 2
Ukončení streamu může být užitečné v případech, kdy má stream omezenou životnost (například reakce na události mousemove
pouze tehdy, když je prvek DOM přetahován, ale ne poté, co byl upuštěn).
Serializace streamů
Streamy implementují metodu .toJSON()
. Když je stream předán jako argument do JSON.stringify()
, je hodnota streamu serializována.
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // zobrazí 123
Streamy nespouštějí vykreslování
Na rozdíl od knihoven jako Knockout, streamy v Mithril.js nespouštějí automatické překreslování šablon. Překreslování se děje v reakci na obslužné rutiny událostí definované v zobrazeních komponent Mithril.js, změny tras nebo po vyřešení volání m.request
.
Pokud je potřeba překreslování v reakci na jiné asynchronní události (např. setTimeout
/setInterval
, odběr websocketu, obslužná rutina událostí knihovny třetí strany atd.), měli byste ručně zavolat m.redraw()
.
Co je Fantasy Land
Fantasy Land specifikuje interoperabilitu běžných algebraických struktur. Jednoduše řečeno, to znamená, že knihovny, které vyhovují specifikacím Fantasy Land, lze použít k psaní generického funkčního kódu, který funguje bez ohledu na to, jak tyto knihovny implementují konstrukty.
Řekněme například, že chceme vytvořit generickou funkci nazvanou plusOne
. Naivní implementace by vypadala takto:
function plusOne(a) {
return a + 1;
}
Problém s touto implementací je, že ji lze použít pouze s číslem. Je však možné, že jakákoli logika, která vytváří hodnotu pro a
, by mohla také vytvořit chybový stav (zabalený v Maybe nebo Either z knihovny jako Sanctuary nebo Ramda-Fantasy), nebo by to mohl být stream Mithril.js, stream Flyd atd. V ideálním případě bychom nechtěli psát podobnou verzi stejné funkce pro každý možný typ, který by a
mohl mít, a nechtěli bychom opakovaně psát kód pro balení/rozbalování/zpracování chyb.
Zde může pomoci Fantasy Land. Přepišme tuto funkci z hlediska algebry Fantasy Land:
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
Nyní tato metoda funguje s jakýmkoli Funktorem kompatibilním s Fantasy Land, jako je R.Maybe
, S.Either
, stream
atd.
Tento příklad se může zdát komplikovaný, ale je to kompromis ve složitosti: naivní implementace plusOne
dává smysl, pokud máte jednoduchý systém a pouze inkrementujete čísla, ale implementace Fantasy Land se stává výkonnější, pokud máte velký systém s mnoha abstrakcemi obalů a opakovaně používanými algoritmy.
Při rozhodování, zda byste měli přijmout Fantasy Land, byste měli zvážit znalost funkcionálního programování vašeho týmu a být realističtí ohledně úrovně disciplíny, kterou se váš tým může zavázat k udržování kvality kódu (vs tlak na psaní nových funkcí a dodržování termínů). Funkcionální programování silně závisí na kompilaci, správě a zvládnutí velkého množství malých, přesně definovaných funkcí. Proto není vhodné pro týmy, které nemají zavedené postupy dokumentace a/nebo nemají zkušenosti s funkcionálně orientovanými jazyky.