从 Object 到 Stream 的思维方式转变
Source: Dev.to
Introduction
如果你上过编程课,很可能学到了类、继承、封装——面向对象编程(Object‑Oriented Programming (OOP))的常规内容。也许这还有一定道理:把数据和行为封装在一起,在实例之间发送消息,构建一个能够清晰映射世界的系统。
但是,当我们从 things 的世界转向 workflows 的世界时会怎样?在现实中,一切都在不断变化,对吧?
这就是 Stream‑Oriented Programming (SP) 的用武之地。我们不再思考“objects holding state”,而是把注意力放在“information / data / values flowing through time”上。
如果你之前使用过 RxJS,你已经知道流可以以非常自然的方式对异步数据建模。下一步就是不再把它们强行套在 OOP 框架上,而是将 UI 本身构建为一个流的图。
为什么对象让人感觉舒适
让我们来看一个非常熟悉的面向对象编程(OOP)模式。想象一个简单的计数器:
class Counter {
private count = 0;
increment() {
this.count++;
this.render();
}
render() {
return `Click me (${this.count})`;
}
}
const counter = new Counter();
document.body.innerHTML = counter.render();
document.body.addEventListener('click', () => counter.increment());
Counter 对象持有状态,拥有方法,并在每次变化时更新 DOM。
这里的思路是:
- 我有一个 对象。
- 对象拥有 状态。
- 对象拥有 行为。
- 当行为被触发时,对象会 自行变更(在最坏的情况下,甚至会影响整个世界)。
流有什么不同?
现在让我们来看 SP,使用 Rimmel.js:
import { BehaviorSubject, scan } from 'rxjs';
import { rml } from 'rimmel';
const count = new BehaviorSubject(0).pipe(
scan(x => x + 1)
);
const App = () => rml`
<button>
Click me (${count})
</button>
`;
document.body.innerHTML = App();
思维转变
- 我没有对象。
- 我拥有一个 流。
- 该流表示 随时间变化的状态。
- 事件也是流,合并到同一个流中。
- 渲染不是方法调用 — 它只是描述流之间的连接方式。
没有显式的 render() 调用,也没有可变的 this.count。按钮“订阅”了 count 流,DOM 会在 count 变化时自动更新。
OOP 与 SP 并排比较
| 概念 | OOP | SP |
|---|---|---|
| 状态 | 对象内部的字段 | 值的流 |
| 行为 | 会改变状态的方法 | 流的转换 |
| 事件 | 回调、监听器 | 事件流 |
| 渲染 | 命令式调用 | 响应式订阅 |
| 模型 | 对象作为实体 | 随时间流动的数据 |
示例 2:实时搜索
在面向对象编程(OOP)中,实时搜索通常意味着:
- 获取一个输入字段。
- 添加
keyup监听器。 - 手动取消挂起的计时器。
- 发起 fetch 请求。
- 更新结果列表。
在使用 Rimmel.js 的函数式响应式编程(SP)中,同样的逻辑以声明式方式表达:
// Helper that renders a single list item
const item = (str: string) => rml`- ${str}`;
// Observable that debounces the input, fetches data, and formats the result
const typeahead = new Subject<string>().pipe(
debounceTime(300),
switchMap(q => ajax.getJSON(`/countries?q=${q}`)),
map(list => list.map(item).join(''))
);
// Component that wires everything together
const App = () => rml`
<input
type="text"
placeholder="Search…"
on:input="${typeahead}"
/>
<ul>${typeahead}</ul>
`;
基础自动补全示例
${typeahead}
在 StackBlitz 上尝试。按键是一个流。防抖仅仅是一个操作符。获取数据是一次转换。渲染结果是另一个订阅。无需手动清理,也没有“可变查询字段”。
示例 3:表单验证
面向对象(OOP)方式
- 每个输入字段都绑定到一个属性。
- 每个属性都有类似
isValid的标志。 - 验证在每次更改时以命令式方式运行。
- 一个表单对象聚合这些结果。
面向函数式(SP)方式,使用 Rimmel.js
const App = () => rml`
const submit = new Subject();
const valid = submit.pipe(
map(({ email, password }) =>
ALL(
email.includes('@'),
password.length >= 6
)
)
);
const invalid = valid.pipe(
map(v => !v)
);
return rml`
<form on:submit=${submit}>
<input type="email" name="email" placeholder="Email" />
<input type="password" name="password" placeholder="Password" />
<button disabled=${invalid}>Submit</button>
</form>
`;
`;
document.body.innerHTML = App();
验证逻辑以及启用/禁用状态完全通过流的组合来表达。
接下来该怎么做
如果你感兴趣,Rimmel.js 是一个很好的 playground(实验场),可以进一步探索这种思维方式。它轻量、兼容 RxJS,并且从头到尾都是围绕流(streams)构建的。
👉 试试看: GitHub 上的 Rimmel.js
下次当你准备创建一个带有 render() 方法的类时,问问自己:如果把它写成流的形式会是什么样子?



