从 Object 到 Stream 的思维方式转变

发布: (2025年12月20日 GMT+8 08:14)
6 min read
原文: Dev.to

Source: Dev.to

Dario Mannu

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 并排比较

概念OOPSP
状态对象内部的字段值的流
行为会改变状态的方法流的转换
事件回调、监听器事件流
渲染命令式调用响应式订阅
模型对象作为实体随时间流动的数据

示例 2:实时搜索

在面向对象编程(OOP)中,实时搜索通常意味着:

  1. 获取一个输入字段。
  2. 添加 keyup 监听器。
  3. 手动取消挂起的计时器。
  4. 发起 fetch 请求。
  5. 更新结果列表。

在使用 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() 方法的类时,问问自己:如果把它写成流的形式会是什么样子?

了解更多

流式编程 — 入门
流式编程 — 入门

使用简单函数创建 Web 组件
使用简单函数创建 Web 组件

从回调到调用转发:让响应式变得简单?
从回调到调用转发:让响应式变得简单?

Back to Blog

相关文章

阅读更多 »

JavaScript 中的一等函数

介绍 对于学习 JavaScript 的开发者来说,术语 first‑class functions 在讨论和文档中经常出现。在 JavaScript 中,函数 a...