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 值的展开 |
changed | Array<Stream> | 是 | 受更新影响的 stream 列表 |
返回 | any | 返回一个计算值。 |
Stream.merge
创建一个 stream,其值是由 stream 数组中每个 stream 的值组成的数组。
stream = Stream.merge(streams)
参数 | 类型 | 必需 | 描述 |
---|---|---|---|
streams | Array<Stream> | 是 | stream 列表 |
返回 | Stream | 返回一个 stream,其值是输入 stream 值的数组。 |
Stream.scan
创建一个新的 stream,该 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 和 scan 函数组成的 pair 数组,并使用给定的函数将所有这些 stream 合并到一个 stream 中。
stream = Stream.scanMerge(pairs, accumulator)
参数 | 类型 | 必需 | 描述 |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | 是 | stream 和 scan 函数的元组数组。 |
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 的零个或多个值的展开 |
返回 | 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。请参阅 ended state。
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,其值是回调函数的返回值。请参阅 chaining streams
此方法的存在是为了符合 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 的工作方式类似于变量或具有 getter 和 setter 的属性:它可以保存状态,并且可以修改。
var username = stream('John');
console.log(username()); // logs "John"
username('John Doe');
console.log(username()); // logs "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()); // logs "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()); // logs "John Doe"
firstName('Mary');
console.log(fullName()); // logs "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()); // logs 2
依赖 stream 是反应式的:它们的 value 会在父 stream 的 value 更新时更新。无论依赖 stream 是在父 stream 的 value 设置之前还是之后创建的,都会发生这种情况。
你可以通过返回特殊值 stream.SKIP
来阻止依赖 stream 被更新。
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// 永远不会运行
});
组合 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()); // logs "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()); // logs "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()); // logs 12
一个 stream 可以依赖于任意数量的 stream,并且保证以原子方式更新。例如,如果 stream A 有两个依赖 stream B 和 C,并且第四个 stream D 依赖于 B 和 C,则如果 A 的 value 发生变化,stream D 只会更新一次。这保证了 stream D 的回调永远不会使用不稳定值调用,例如当 B 具有新 value 但 C 具有旧 value 时。原子性还带来了不必要地重新计算下游 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 也处于挂起状态,并且不会更新其 value。
var a = stream(5);
var b = stream(); // 挂起的 stream
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // logs undefined
在上面的示例中,added
是一个挂起的 stream,因为它的父 stream b
也是挂起的。
这也适用于通过 stream.map
创建的依赖 stream:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // logs undefined because `doubled` is pending
活动状态
当 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
也会变为活动的,并更新为具有 value "hello world"
。
结束状态
可以通过调用 stream.end(true)
来阻止 stream 影响其依赖 stream。这有效地删除了 stream 与其依赖 stream 之间的连接。
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // 设置为结束状态
value(5);
console.log(doubled());
// logs undefined because `doubled` no longer depends on `value`
已结束的 stream 仍然具有状态容器的特性,也就是说,即使它们已经结束,你仍然可以将它们用作 getter 和 setter。
var value = stream(1);
value.end(true); // 设置为结束状态
console.log(value(1)); // logs 1
value(2);
console.log(value()); // logs 2
在 stream 具有有限生命周期的情况下,结束 stream 可能很有用(例如,仅在拖动 DOM 元素时才对 mousemove
事件做出反应,但在放下 DOM 元素后则不做出反应)。
序列化 stream
Stream 实现了一个 .toJSON()
方法。当 stream 作为参数传递给 JSON.stringify()
函数时,stream 的值会被序列化。
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // logs 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 时,你应该考虑团队对函数式编程的掌握程度,并对团队在代码质量维护方面的投入程度 (与开发新功能和按时交付的压力相比) 保持客观。函数式编程风格在很大程度上依赖于对大量小型、精确定义的函数进行组织、管理和熟练运用,因此它不适合缺乏完善的文档实践和/或缺乏函数式编程经验的团队。