stream()
Beschreibung
Ein Stream ist eine reaktive Datenstruktur, ähnlich wie Zellen in Tabellenkalkulationsprogrammen.
Wenn beispielsweise in einer Tabellenkalkulation die Formel A1 = B1 + C1
steht, ändert sich der Wert von A1
automatisch, wenn sich der Wert von B1
oder C1
ändert.
In ähnlicher Weise können Sie einen Stream von anderen Streams abhängig machen, sodass die Änderung des Werts eines Streams automatisch den anderen aktualisiert. Dies ist nützlich, wenn Sie rechenintensive Operationen haben und diese nur bei Bedarf ausführen möchten, anstatt beispielsweise bei jedem Neuzeichnen.
Streams sind nicht im Core-Paket von Mithril.js enthalten. Um das Streams-Modul einzubinden, verwenden Sie:
var Stream = require('mithril/stream');
Sie können das Modul auch direkt herunterladen, wenn Ihre Umgebung keine Bündelungs-Toolchain unterstützt:
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
Wenn die Stream-Bibliothek direkt per <script>
-Tag geladen wird (anstatt über require
), wird sie als window.m.stream
verfügbar gemacht. Wenn window.m
bereits definiert ist (z. B. weil Sie auch das Haupt-Skript von Mithril.js verwenden), wird sie an das vorhandene Objekt angehängt. Andernfalls wird ein neues window.m
erstellt. Wenn Sie Streams in Verbindung mit Mithril.js als reine Skript-Tags verwenden möchten, sollten Sie Mithril.js in Ihre Seite einbinden, bevor Sie mithril/stream
einbinden, da mithril
andernfalls das von mithril/stream
definierte window.m
-Objekt überschreiben würde. Dies ist kein Problem, wenn die Bibliotheken als CommonJS-Module verwendet werden (mit require(...)
).
Signatur
Erstellt einen Stream.
stream = Stream(value)
Argument | Type | Required | Description |
---|---|---|---|
value | any | No | Wenn dieses Argument vorhanden ist, wird der Wert des Streams auf diesen Wert gesetzt. |
returns | Stream | Gibt den Stream zurück. |
Statische Member
Stream.combine
Erstellt einen berechneten Stream, der reaktiv aktualisiert wird, wenn einer seiner Upstream-Streams aktualisiert wird. Siehe Kombinieren von Streams.
stream = Stream.combine(combiner, streams)
Argument | Type | Required | Description |
---|---|---|---|
combiner | (Stream..., Array) -> any | Yes | Siehe Argument combiner. |
streams | Array<Stream> | Yes | Eine Liste von Streams, die kombiniert werden sollen. |
returns | Stream | Gibt den Stream zurück. |
combiner
Gibt an, wie der Wert eines berechneten Streams generiert wird. Siehe Kombinieren von Streams.
any = combiner(streams..., changed)
Argument | Type | Required | Description |
---|---|---|---|
streams... | Splat von Streams | No | Splat von null oder mehr Streams, die den als zweites Argument an stream.combine übergebenen Streams entsprechen. |
changed | Array<Stream> | Yes | Liste der Streams, die durch eine Aktualisierung verändert wurden. |
returns | any | Gibt den berechneten Wert zurück. |
Stream.merge
Erstellt einen Stream, dessen Wert ein Array von Werten aus mehreren Streams ist.
stream = Stream.merge(streams)
Argument | Type | Required | Description |
---|---|---|---|
streams | Array<Stream> | Yes | Eine Liste von Streams. |
returns | Stream | Gibt einen Stream zurück, dessen Wert ein Array von Eingabe-Stream-Werten ist. |
Stream.scan
Erstellt einen neuen Stream mit den Ergebnissen des Aufrufs der Funktion für jeden Wert im Stream, wobei ein Akkumulator und der eingehende Wert verwendet werden.
Sie können verhindern, dass abhängige Streams aktualisiert werden, indem Sie den speziellen Wert stream.SKIP
innerhalb der Akkumulatorfunktion zurückgeben.
stream = Stream.scan(fn, accumulator, stream)
Argument | Type | Required | Description |
---|---|---|---|
fn | (accumulator, value) -> result | SKIP | Yes | Eine Funktion, die einen Akkumulator und einen Wert als Parameter entgegennimmt und einen neuen Akkumulatorwert desselben Typs zurückgibt. |
accumulator | any | Yes | Der Startwert für den Akkumulator. |
stream | Stream | Yes | Stream, der die Werte enthält. |
returns | Stream | Gibt einen neuen Stream zurück, der das Ergebnis enthält. |
Stream.scanMerge
Akzeptiert ein Array von Paaren von Streams und Scan-Funktionen und führt alle diese Streams mithilfe der angegebenen Funktionen zu einem einzigen Stream zusammen.
stream = Stream.scanMerge(pairs, accumulator)
Argument | Type | Required | Description |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | Yes | Ein Array von Tupeln aus Stream- und Scan-Funktionen. |
accumulator | any | Yes | Der Startwert für den Akkumulator. |
returns | Stream | Gibt einen neuen Stream zurück, der das Ergebnis enthält. |
Stream.lift
Erstellt einen berechneten Stream, der reaktiv aktualisiert wird, wenn einer seiner Upstream-Streams aktualisiert wird. Siehe Kombinieren von Streams. Im Gegensatz zu combine
ist die Anzahl der Eingabe-Streams variabel (anstelle eines Arrays), und der Callback empfängt die Stream-Werte anstelle von Streams. Es gibt keinen changed
-Parameter. Dies ist im Allgemeinen eine benutzerfreundlichere Funktion für Anwendungen als combine
.
stream = Stream.lift(lifter, stream1, stream2, ...)
Argument | Type | Required | Description |
---|---|---|---|
lifter | (any...) -> any | Yes | Siehe Argument lifter. |
streams... | Liste von Streams | Yes | Zu liftende Streams. |
returns | Stream | Gibt den Stream zurück. |
lifter
Gibt an, wie der Wert eines berechneten Streams generiert wird. Siehe Kombinieren von Streams.
any = lifter(streams...)
Argument | Type | Required | Description |
---|---|---|---|
streams... | Splat von Streams | No | Splat von null oder mehr Werten, die den Werten der an stream.lift übergebenen Streams entsprechen. |
returns | any | Gibt den berechneten Wert zurück. |
Stream.SKIP
Ein spezieller Wert, der an Stream-Callbacks zurückgegeben werden kann, um die Ausführung von Downstream-Streams zu überspringen.
Stream["fantasy-land/of"]
Diese Methode hat dieselbe Funktion wie stream
. Sie existiert, um die Applicative-Spezifikation von Fantasy Land zu erfüllen. Weitere Informationen finden Sie im Abschnitt Was ist Fantasy Land.
stream = Stream["fantasy-land/of"](value)
Argument | Type | Required | Description |
---|---|---|---|
value | any | No | Wenn dieses Argument vorhanden ist, wird der Wert des Streams auf diesen Wert gesetzt. |
returns | Stream | Gibt den Stream zurück. |
Instanz-Member
stream.map
Erstellt einen abhängigen Stream, dessen Wert auf das Ergebnis der Callback-Funktion gesetzt wird. Diese Methode ist ein Alias von stream["fantasy-land/map"].
dependentStream = stream().map(callback)
Argument | Type | Required | Description |
---|---|---|---|
callback | any -> any | Yes | Ein Callback, dessen Rückgabewert zum Wert des Streams wird. |
returns | Stream | Gibt einen Stream zurück. |
stream.end
Ein koabhängiger Stream, der abhängige Streams abmeldet, wenn er auf true
gesetzt wird. Siehe Beendeter Zustand.
endStream = stream().end
stream["fantasy-land/of"]
Diese Methode hat dieselbe Funktion wie stream
. Sie existiert, um die Applicative-Spezifikation von Fantasy Land zu erfüllen. Weitere Informationen finden Sie im Abschnitt Was ist Fantasy Land.
stream = stream()["fantasy-land/of"](value)
Argument | Type | Required | Description |
---|---|---|---|
value | any | No | Wenn dieses Argument vorhanden ist, wird der Wert des Streams auf diesen Wert gesetzt. |
returns | Stream | Gibt einen Stream zurück. |
stream["fantasy-land/map"]
Erstellt einen abhängigen Stream, dessen Wert auf das Ergebnis der Callback-Funktion gesetzt wird. Siehe Verketten von Streams.
Diese Methode existiert, um die Applicative-Spezifikation von Fantasy Land zu erfüllen. Weitere Informationen finden Sie im Abschnitt Was ist Fantasy Land.
dependentStream = stream()["fantasy-land/map"](callback)
Argument | Type | Required | Description |
---|---|---|---|
callback | any -> any | Yes | Ein Callback, dessen Rückgabewert zum Wert des Streams wird. |
returns | Stream | Gibt einen Stream zurück. |
stream["fantasy-land/ap"]
Der Name dieser Methode steht für apply
(anwenden). Wenn ein Stream a
eine Funktion als Wert hat, kann ein anderer Stream b
ihn als Argument für b.ap(a)
verwenden. Der Aufruf von ap
ruft die Funktion mit dem Wert von Stream b
als Argument auf und gibt einen anderen Stream zurück, dessen Wert das Ergebnis des Funktionsaufrufs ist. Diese Methode existiert, um die Applicative-Spezifikation von Fantasy Land zu erfüllen. Weitere Informationen finden Sie im Abschnitt Was ist Fantasy Land.
stream = stream()["fantasy-land/ap"](apply)
Argument | Type | Required | Description |
---|---|---|---|
apply | Stream | Yes | Ein Stream, dessen Wert eine Funktion ist. |
returns | Stream | Gibt einen Stream zurück. |
Grundlegende Verwendung
Streams sind nicht Teil der Core-Distribution von Mithril.js. Um sie in ein Projekt einzubinden, binden Sie das Modul ein:
var stream = require('mithril/stream');
Streams als Variablen
stream()
gibt einen Stream zurück. Auf der einfachsten Ebene funktioniert ein Stream ähnlich wie eine Variable oder eine Getter-Setter-Eigenschaft: Er kann einen Zustand enthalten, der geändert werden kann.
var username = stream('John');
console.log(username()); // logs "John"
username('John Doe');
console.log(username()); // logs "John Doe"
Der Hauptunterschied besteht darin, dass ein Stream eine Funktion ist und daher zu Funktionen höherer Ordnung zusammengesetzt werden kann.
var users = stream();
// Nutzer von einem Server mit der Fetch API anfordern
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
Im obigen Beispiel wird der users
-Stream mit den Antwortdaten gefüllt, wenn die Anfrage abgeschlossen ist.
Bidirektionale Bindungen
Streams können auch von Ereignis-Callbacks und ähnlichem gefüllt werden.
// ein Stream
var user = stream('');
// eine bidirektionale Bindung an den Stream
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
Im obigen Beispiel wird der user
-Stream auf den Wert des Eingabefelds aktualisiert, wenn der Benutzer in die Eingabe tippt.
Berechnete Eigenschaften
Streams sind nützlich für die Implementierung berechneter Eigenschaften:
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // logs "hello-world"
Im obigen Beispiel wird der Wert von slug
berechnet, wenn title
aktualisiert wird, nicht wenn slug
gelesen wird.
Es ist natürlich auch möglich, Eigenschaften basierend auf mehreren Streams zu berechnen:
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // logs "John Doe"
firstName('Mary');
console.log(fullName()); // logs "Mary Doe"
Berechnete Eigenschaften in Mithril.js werden atomar aktualisiert: Streams, die von mehreren Streams abhängen, werden niemals mehr als einmal pro Wertaktualisierung aufgerufen, unabhängig davon, wie komplex der Abhängigkeitsgraph der berechneten Eigenschaft ist.
Verketten von Streams
Streams können mit der map
-Methode verkettet werden. Ein verketteter Stream wird auch als abhängiger Stream bezeichnet.
// Eltern-Stream
var value = stream(1);
// abhängiger Stream
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // logs 2
Abhängige Streams sind reaktiv: Ihre Werte werden jedes Mal aktualisiert, wenn der Wert ihres Eltern-Streams aktualisiert wird. Dies geschieht unabhängig davon, ob der abhängige Stream vor oder nach dem Festlegen des Werts des Eltern-Streams erstellt wurde.
Sie können verhindern, dass abhängige Streams aktualisiert werden, indem Sie den speziellen Wert stream.SKIP
zurückgeben.
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// wird niemals ausgeführt
});
Kombinieren von Streams
Streams können von mehr als einem Eltern-Stream abhängen. Diese Art von Streams kann über stream.merge()
erstellt werden.
var a = stream('hello');
var b = stream('world');
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
console.log(greeting()); // logs "hello world"
Oder Sie können die Hilfsfunktion stream.lift()
verwenden.
var a = stream('hello');
var b = stream('world');
var greeting = stream.lift(
function (_a, _b) {
return _a + ' ' + _b;
},
a,
b
);
console.log(greeting()); // logs "hello world"
Es gibt auch eine Methode auf niedrigerer Ebene namens stream.combine()
, die die Streams selbst in den reaktiven Berechnungen für fortgeschrittenere Anwendungsfälle verfügbar macht.
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // logs 12
Ein Stream kann von einer beliebigen Anzahl von Streams abhängen, und es wird garantiert, dass er atomar aktualisiert wird. Wenn beispielsweise ein Stream A zwei abhängige Streams B und C hat und ein vierter Stream D sowohl von B als auch von C abhängt, wird der Stream D nur einmal aktualisiert, wenn sich der Wert von A ändert. Dies garantiert, dass der Callback für Stream D niemals mit inkonsistenten Werten aufgerufen wird, z. B. wenn B einen neuen Wert hat, C aber den alten Wert hat. Atomarität bringt auch den Leistungsvorteil, dass Downstream-Streams nicht unnötig neu berechnet werden.
Sie können verhindern, dass abhängige Streams aktualisiert werden, indem Sie den speziellen Wert stream.SKIP
zurückgeben.
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// wird niemals ausgeführt
});
Stream-Zustände
Zu einem bestimmten Zeitpunkt kann sich ein Stream in einem von drei Zuständen befinden: ausstehend (pending), aktiv (active) und beendet (ended).
Ausstehender Zustand
Ausstehende Streams können erstellt werden, indem stream()
ohne Parameter aufgerufen wird.
var pending = stream();
Wenn ein Stream von mehr als einem Stream abhängt und sich einer seiner Eltern-Streams in einem ausstehenden Zustand befindet, befindet sich auch der abhängige Stream in einem ausstehenden Zustand und aktualisiert seinen Wert nicht.
var a = stream(5);
var b = stream(); // pending stream
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // logs undefined
Im obigen Beispiel ist added
ein ausstehender Stream, da sein Eltern-Stream b
ebenfalls ausstehend ist.
Dies gilt auch für abhängige Streams, die über stream.map
erstellt wurden:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // logs undefined, da `doubled` ausstehend ist
Aktiver Zustand
Wenn ein Stream einen Wert empfängt, wird er aktiv (es sei denn, der Stream ist beendet).
var stream1 = stream('hello'); // stream1 ist aktiv
var stream2 = stream(); // stream2 beginnt als ausstehend
stream2('world'); // wird dann aktiv
Ein abhängiger Stream mit mehreren Eltern-Streams wird aktiv, wenn alle seine Eltern-Streams aktiv sind.
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
Im obigen Beispiel ist der a
-Stream aktiv, aber b
ist ausstehend. Das Setzen von b("world")
würde dazu führen, dass b
aktiv wird, und daher würde auch greeting
aktiv werden und auf den Wert "hello world"
aktualisiert werden.
Beendeter Zustand
Ein Stream kann aufhören, seine abhängigen Streams zu beeinflussen, indem stream().end(true)
aufgerufen wird. Dadurch wird die Verbindung zwischen einem Stream und seinen abhängigen Streams effektiv entfernt.
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // in den beendeten Zustand versetzen
value(5);
console.log(doubled());
// logs undefined, da `doubled` nicht mehr von `value` abhängt
Beendete Streams haben immer noch eine Semantik für Zustandscontainer, d. h. Sie können sie weiterhin als Getter-Setter verwenden, auch nachdem sie beendet wurden.
var value = stream(1);
value.end(true); // in den beendeten Zustand versetzen
console.log(value(1)); // logs 1
value(2);
console.log(value()); // logs 2
Das Beenden eines Streams kann in Fällen nützlich sein, in denen ein Stream eine begrenzte Lebensdauer hat (z. B. die Reaktion auf mousemove
-Ereignisse nur, während ein DOM-Element gezogen wird, aber nicht, nachdem es abgelegt wurde).
Serialisieren von Streams
Streams implementieren eine .toJSON()
-Methode. Wenn ein Stream als Argument an JSON.stringify()
übergeben wird, wird der Wert des Streams serialisiert.
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // logs 123
Streams lösen kein Rendering aus
Im Gegensatz zu Bibliotheken wie Knockout lösen Mithril.js-Streams kein erneutes Rendern von Vorlagen aus. Das erneute Rendering erfolgt als Reaktion auf Ereignis-Handler, die in Mithril.js-Komponenten definiert sind, auf Routenänderungen oder nachdem m.request
-Aufrufe abgeschlossen wurden.
Wenn das Neuzeichnen als Reaktion auf andere asynchrone Ereignisse (z. B. setTimeout
/setInterval
, WebSocket-Abonnement, Ereignis-Handler von Bibliotheken von Drittanbietern usw.) gewünscht wird, sollten Sie m.redraw()
manuell aufrufen.
Was ist Fantasy Land
Fantasy Land spezifiziert die Interoperabilität gängiger algebraischer Strukturen. Vereinfacht gesagt bedeutet dies, dass Bibliotheken, die den Fantasy Land-Spezifikationen entsprechen, verwendet werden können, um generischen funktionalen Code zu schreiben, der unabhängig davon funktioniert, wie diese Bibliotheken die Konstrukte implementieren.
Nehmen wir beispielsweise an, wir möchten eine generische Funktion namens plusOne
erstellen. Die naive Implementierung würde wie folgt aussehen:
function plusOne(a) {
return a + 1;
}
Das Problem dieser Implementierung ist, dass sie nur mit Zahlen verwendet werden kann. Es ist jedoch möglich, dass jede Logik, die einen Wert für a
erzeugt, auch einen Fehlerzustand erzeugen könnte (eingewickelt in ein Maybe oder ein Either aus einer Bibliothek wie Sanctuary oder Ramda-Fantasy), oder es könnte ein Mithril.js-Stream, ein Flyd-Stream usw. sein. Idealerweise möchten wir nicht für jeden möglichen Typ, den a
haben könnte, eine ähnliche Version derselben Funktion schreiben, und wir möchten nicht wiederholt Code zum Ein- und Auspacken/zur Fehlerbehandlung schreiben.
Hier kann Fantasy Land Unterstützung bieten. Schreiben wir diese Funktion in Bezug auf eine Fantasy Land-Algebra um:
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
Jetzt funktioniert diese Methode mit jedem Fantasy Land-kompatiblen Functor, wie z. B. R.Maybe
, S.Either
, stream
usw.
Dieses Beispiel mag kompliziert erscheinen, aber es ist ein Kompromiss in Bezug auf die Komplexität: Die naive plusOne
-Implementierung ist sinnvoll, wenn Sie ein einfaches System haben und nur Zahlen inkrementieren, aber die Fantasy Land-Implementierung wird leistungsfähiger, wenn Sie ein großes System mit vielen Wrapper-Abstraktionen und wiederverwendeten Algorithmen haben.
Bei der Entscheidung, ob Sie Fantasy Land übernehmen sollten, sollten Sie die Vertrautheit Ihres Teams mit der funktionalen Programmierung berücksichtigen und realistisch einschätzen, inwieweit sich Ihr Team zur Pflege der Codequalität verpflichten kann (im Vergleich zum Druck, neue Funktionen zu schreiben und Termine einzuhalten). Die funktionale Programmierung hängt stark von der Zusammenstellung, Pflege und Beherrschung einer großen Anzahl kleiner, präzise definierter Funktionen ab und ist daher nicht für Teams geeignet, die keine soliden Dokumentationspraktiken haben und/oder keine Erfahrung in funktional orientierten Sprachen haben.