stream()
描述
Stream 是一種響應式資料結構,類似於試算表應用程式中的儲存格。
例如,在試算表中,如果 A1 = B1 + C1
,則變更 B1
或 C1
的值會自動變更 A1
的值。
同樣地,您可以讓一個 Stream 依賴於其他 Stream,以便變更一個 Stream 的值會自動更新另一個 Stream。當您有非常耗費資源的計算,並且只想在必要時執行它們,而不是在每次重繪時都執行時,這非常有用。
Stream 沒有與 Mithril.js 的核心發布版本捆綁在一起。要包含 Stream 模組,請使用:
var Stream = require('mithril/stream');
如果您的環境不支援打包工具鏈,可以直接下載該模組:
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
當直接使用 <script>
標籤載入(而不是使用 require
)時,Stream 庫會暴露為 window.m.stream
。如果 window.m
已經定義(例如,因為您也使用了主要的 Mithril.js 腳本),它將會附加到現有的物件。否則,它會建立一個新的 window.m
。如果您想將 Stream 與 Mithril.js 一起作為原始腳本標籤使用,您應該在 mithril/stream
之前於頁面中引入 Mithril.js,因為 mithril
會覆寫由 mithril/stream
定義的 window.m
物件。當函式庫作為 CommonJS 模組使用(使用 require(...)
)時,這不是問題。
簽名
創建一個 Stream
stream = Stream(value)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
value | any | 否 | 如果此參數存在,Stream 的值將設定為該值 |
返回 | Stream | 返回一個 Stream |
靜態成員
Stream.combine
創建一個計算 Stream,當其任何上游 Stream 更新時,會響應式地更新。請參閱 組合 Stream
stream = Stream.combine(combiner, streams)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
combiner | (Stream..., Array) -> any | 是 | 請參閱 combiner 參數 |
streams | Array<Stream> | 是 | 要合併的 Stream 列表 |
返回 | Stream | 返回一個 Stream |
combiner
指定如何產生計算 Stream 的值。請參閱 組合 Stream
any = combiner(streams..., changed)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
streams... | splat of Streams | 否 | 對應於作為第二個參數傳遞給 stream.combine 的 Stream 的零個或多個 Stream 的 splat (展開運算子) |
changed | Array<Stream> | 是 | 受更新影響的 Stream 列表 |
返回 | any | 返回一個計算值 |
Stream.merge
創建一個 Stream,其值是來自 Stream 陣列之值的陣列
stream = Stream.merge(streams)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
streams | Array<Stream> | 是 | Stream 列表 |
返回 | Stream | 返回一個 Stream,其值是輸入 Stream 值的陣列 |
Stream.scan
創建一個新的 Stream,其中包含對 Stream 中每個值呼叫函式的結果,該函式帶有累加器和傳入值。
請注意,您可以通過在累加器函數中返回特殊值 stream.SKIP
來防止相依 Stream 被更新。
stream = Stream.scan(fn, accumulator, stream)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
fn | (accumulator, value) -> result | SKIP | 是 | 一個接受累加器和值參數並返回相同類型的新累加器值的函式 |
accumulator | any | 是 | 累加器的起始值 |
stream | Stream | 是 | 包含值的 Stream |
返回 | Stream | 返回一個包含結果的新 Stream |
Stream.scanMerge
接受 Stream 和掃描函式配對的陣列,並使用給定的函式將所有 Stream 合併到單個 Stream。
stream = Stream.scanMerge(pairs, accumulator)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | 是 | Stream 和掃描函式的元組陣列 |
accumulator | any | 是 | 累加器的起始值 |
返回 | Stream | 返回一個包含結果的新 Stream |
Stream.lift
創建一個計算 Stream,當其任何上游 Stream 更新時,會響應式地更新。請參閱 組合 Stream。與 combine
不同,輸入 Stream 是可變數量的參數(而不是陣列),並且回呼接收 Stream 值而不是 Stream。沒有 changed
參數。對於應用程式而言,這通常是比 combine
更容易使用的函式。
stream = Stream.lift(lifter, stream1, stream2, ...)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
lifter | (any...) -> any | 是 | 請參閱 lifter 參數 |
streams... | list of Streams | 是 | 要提升的 Stream |
返回 | Stream | 返回一個 Stream |
lifter
指定如何產生計算 Stream 的值。請參閱 組合 Stream
any = lifter(streams...)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
streams... | splat of Streams | 否 | 對應於傳遞給 stream.lift 的 Stream 值的零個或多個值的 splat (展開運算子) |
返回 | any | 返回一個計算值 |
Stream.SKIP
一個特殊值,可以返回給 Stream 回呼以跳過下游 Stream 的執行
Stream["fantasy-land/of"]
此方法在功能上與 stream
相同。它的存在是為了符合 Fantasy Land 的 Applicative 規範。有關更多資訊,請參閱 什麼是 Fantasy Land 部分。
stream = Stream["fantasy-land/of"](value)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
value | any | 否 | 如果此參數存在,Stream 的值將設定為該值 |
返回 | Stream | 返回一個 Stream |
實例成員
stream.map
創建一個相依 Stream,其值設定為回呼函數的結果。此方法是 stream["fantasy-land/map"] 的別名。
dependentStream = stream().map(callback)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
callback | any -> any | 是 | 一個回呼,其返回值成為 Stream 的值 |
返回 | Stream | 返回一個 Stream |
stream.end
一個共同相依的 Stream,當設定為 true
時,會取消註冊相依 Stream。請參閱 已結束狀態。
endStream = stream().end
stream["fantasy-land/of"]
此方法在功能上與 stream
相同。它的存在是為了符合 Fantasy Land 的 Applicative 規範。有關更多資訊,請參閱 什麼是 Fantasy Land 部分。
stream = stream()["fantasy-land/of"](value)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
value | any | 否 | 如果此參數存在,Stream 的值將設定為該值 |
返回 | Stream | 返回一個 Stream |
stream["fantasy-land/map"]
創建一個相依 Stream,其值設定為回呼函數的結果。請參閱 串鏈 Stream
此方法的存在是為了符合 Fantasy Land 的 Applicative 規範。有關更多資訊,請參閱 什麼是 Fantasy Land 部分。
dependentStream = stream()["fantasy-land/map"](callback)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
callback | any -> any | 是 | 一個回呼,其返回值成為 Stream 的值 |
返回 | Stream | 返回一個 Stream |
stream["fantasy-land/ap"]
此方法的名稱代表 apply
(應用)。如果 Stream a
具有一個函式作為其值,則另一個 Stream b
可以將其用作 b.ap(a)
的參數。呼叫 ap
將會使用 Stream b
的值作為其參數來呼叫該函式,並且它將會返回另一個 Stream,其值是函式呼叫的結果。此方法的存在是為了符合 Fantasy Land 的 Applicative 規範。有關更多資訊,請參閱 什麼是 Fantasy Land 部分。
stream = stream()["fantasy-land/ap"](apply)
參數 | 類型 | 是否必填 | 描述 |
---|---|---|---|
apply | Stream | 是 | 一個值為函式的 Stream |
返回 | Stream | 返回一個 Stream |
基本用法
Stream 並非 Mithril.js 核心發布版本的一部分。若要將它們包含在專案中,請請求其模組:
var stream = require('mithril/stream');
Stream 作為變數
stream()
返回一個 Stream。在其最基本的層面上,Stream 的運作方式類似於變數或 getter-setter 屬性:它可以保存狀態,而且該狀態可以被修改。
var username = stream('John');
console.log(username()); // 記錄 "John"
username('John Doe');
console.log(username()); // 記錄 "John Doe"
主要區別在於 Stream 是一個函式,因此可以組合成高階函式。
var users = stream();
// 使用 fetch API 從伺服器請求使用者
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
在上面的範例中,當請求解析時,users
Stream 會填充回應資料。
雙向綁定
Stream 也可以從事件回呼等來填充。
// 一個 Stream
var user = stream('');
// 到 Stream 的雙向綁定
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
在上面的範例中,當使用者在輸入中輸入時,user
Stream 會更新為輸入欄位的值。
計算屬性
Stream 對於實現計算屬性非常有用:
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // 記錄 "hello-world"
在上面的範例中,slug
的值是在 title
更新時計算的,而不是在讀取 slug
時計算的。
當然,也可以根據多個 Stream 計算屬性:
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // 記錄 "John Doe"
firstName('Mary');
console.log(fullName()); // 記錄 "Mary Doe"
Mithril.js 中的計算屬性會以原子性更新:依賴於多個 Stream 的 Stream 對於每個值更新永遠不會被呼叫超過一次,無論計算屬性的依賴圖有多複雜。
串鏈 Stream
可以使用 map
方法串鏈 Stream。串鏈的 Stream 也稱為_相依 Stream_。
// 父 Stream
var value = stream(1);
// 相依 Stream
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // 記錄 2
相依 Stream 是_響應式_的:它們的值會在父 Stream 的值更新時隨時更新。無論相依 Stream 是在父 Stream 的值設定之前還是之後建立的,都會發生這種情況。
您可以通過返回特殊值 stream.SKIP
來防止相依 Stream 被更新
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// 永遠不會執行
});
組合 Stream
Stream 可以依賴於多個父 Stream。這些 Stream 可以透過 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()); // 記錄 "hello world"
或者您可以使用輔助函式 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()); // 記錄 "hello world"
還有一個稱為 stream.combine()
的較低層級的方法,它在響應式計算中公開 Stream 本身,以用於更進階的使用案例
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // 記錄 12
一個 Stream 可以依賴於任意數量的 Stream,並且保證以原子性更新。例如,如果 Stream A 有兩個相依 Stream B 和 C,並且第四個 Stream D 依賴於 B 和 C,則如果 A 的值變更,Stream D 將只更新一次。這保證了 Stream D 的回呼永遠不會以不穩定的值呼叫,例如當 B 有一個新值但 C 有舊值時。原子性也帶來了不必要地重新計算下游 Stream 的效能優勢。
您可以通過返回特殊值 stream.SKIP
來防止相依 Stream 被更新
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// 永遠不會執行
});
Stream 狀態
在任何給定時間,Stream 可以處於三種狀態之一:pending (暫停)、active (活動) 和 ended (已結束)。
暫停狀態
可以透過呼叫不帶參數的 stream()
來建立暫停 Stream。
var pending = stream();
如果一個 Stream 依賴於多個 Stream,並且其任何父 Stream 處於暫停狀態,則相依 Stream 也處於暫停狀態,並且不會更新其值。
var a = stream(5);
var b = stream(); // 暫停 Stream
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // 記錄 undefined
在上面的範例中,added
是一個暫停 Stream,因為其父 Stream b
也是暫停的。
這也適用於透過 stream.map
建立的相依 Stream:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // 記錄 undefined,因為 `doubled` 是暫停的
活動狀態
當 Stream 接收到一個值時,它會變成活動狀態 (除非該 Stream 已結束)。
var stream1 = stream('hello'); // stream1 是活動的
var stream2 = stream(); // stream2 從暫停開始
stream2('world'); // 然後變成活動的
如果一個具有多個父 Stream 的相依 Stream 的所有父 Stream 都是活動的,則該相依 Stream 變成活動的。
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
在上面的範例中,a
Stream 是活動的,但 b
是暫停的。設定 b("world")
會導致 b
變成活動的,因此 greeting
也會變成活動的,並且會更新為具有值 "hello world"
已結束狀態
Stream 可以透過呼叫 stream.end(true)
來停止影響其相依 Stream。這有效地移除了 Stream 與其相依 Stream 之間的連接。
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // 設定為已結束狀態
value(5);
console.log(doubled());
// 記錄 undefined,因為 `doubled` 不再依賴於 `value`
已結束的 Stream 仍然具有狀態容器語義,即,即使在它們結束之後,您仍然可以將它們用作 getter-setter。
var value = stream(1);
value.end(true); // 設定為已結束狀態
console.log(value(1)); // 記錄 1
value(2);
console.log(value()); // 記錄 2
在 Stream 具有有限生命週期的情況下,結束 Stream 可能很有用(例如,僅在拖曳 DOM 元素時才對 mousemove
事件做出反應,但在放下後則不反應)。
序列化 Stream
Stream 實現了一個 .toJSON()
方法。當 Stream 作為參數傳遞給 JSON.stringify()
時,Stream 的值會被序列化。
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // 記錄 123
Stream 不會觸發渲染
與 Knockout 等函式庫不同,Mithril.js Stream 不會觸發模板的重新渲染。重新繪製發生在回應 Mithril.js 元件視圖中定義的事件處理程序、路由變更或在 m.request
呼叫解析之後。
如果希望回應其他非同步事件(例如 setTimeout
/setInterval
、websocket 訂閱、第三方函式庫事件處理程序等)進行重新繪製,則應手動呼叫 m.redraw()
。
什麼是 Fantasy Land
Fantasy Land 指定了常見代數結構的互操作性。簡而言之,這意味著符合 Fantasy Land 規範的函式庫可用於編寫通用函數式風格的程式碼,無論這些函式庫如何實現這些結構。
例如,假設我們要建立一個名為 plusOne
的通用函式。基礎的實作看起來像這樣:
function plusOne(a) {
return a + 1;
}
此實作的問題在於它只能與數字一起使用。但是,為 a
產生值的任何邏輯也可能產生錯誤狀態 (包裝在來自 Sanctuary 或 Ramda-Fantasy 等函式庫的 Maybe 或 Either 中),或者它可能是 Mithril.js Stream、Flyd Stream 等。理想情況下,我們不希望為 a
可能具有的每種類型編寫相同函式的類似版本,並且我們不希望重複編寫包裝/解包/錯誤處理程式碼。
這就是 Fantasy Land 可以提供幫助的地方。讓我們根據 Fantasy Land 代數重寫該函式:
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
現在,此方法適用於任何符合 Fantasy Land 的 Functor
,例如 R.Maybe
、S.Either
、stream
等。
此範例可能看起來很複雜,但這是複雜性方面的取捨:如果您有一個簡單的系統並且只會遞增數字,那麼基礎的 plusOne
實作是有意義的,但是如果您有一個具有許多包裝器抽象和重用演算法的大型系統,那麼 Fantasy Land 實作會變得更加強大。
在決定是否應該採用 Fantasy Land 時,您應該考慮您的團隊對函數式程式設計的熟悉程度,並且對您的團隊可以承諾維護程式碼品質的自律程度 (相對於編寫新功能和滿足期限的壓力) 持現實態度。函數式風格的程式設計在很大程度上取決於編譯、策劃和掌握大量小的、精確定義的函式,因此它不適合沒有紮實的文件編寫實務和/或缺乏函數式導向語言經驗的團隊。