2

原文:what-is-my-state

阅读前须知

  • 本文献给对前端状态管理 state management 有思考的同学。
  • 文章有涉及 函数式编程响应式编程 概念
  • 原文是 slide,所以是言不成章的。本文为了通顺,加了一些过渡。还有,由于 slide 常用于演讲,所以文字说明不是很多。我补上了一些个人的理解(以引用块的样式),但也不是很多,有时候会再出文章解释一些术语,如 lensatom 等。
  • 文中的 state 和「状态」是同义的,有时为了强调和更易于理解,保留了这一术语未翻译,读者请自行脑内替换。
  • 本文中的「我」指原作者

口味调查

在我给出我的口味前,下面几个矛盾,你会怎么选择?

  • 无状态 vs 状态化
    程序是基于状态的,所以它不可能被完全地清除,但它必须被管理起来。
  • 可变状态 vs 不可变状态
    状态随着时间而变化,所以不可变状态这个说法是自相矛盾的。人们可以在状态的一个点上捕捉到不可变的值,但状态本身并不全部不可变。
  • 全局状态 vs 局部状态
    来自外部的、共享的、全局状态实际上优于被封装在内部的本地状态。这也是本篇文章要讨论的要点之一。

前情提要

  • 这篇文章不会提出新发明。

Most papers in computer science describe how their author learned what someone else already knew. — Peter Landin

  • 我们的讨论基于我在 Calmm 中的实践
    Calmm 是一个用于编写响应式 UI 的框架。鼓励使用外部共享的状态,和持续可观察的属性(continuous observable properties)。
  • 在赞美 Calmm 之前,我们需要达成一些共识

本文的目标

希望咱们能从一个崭新的角度讨论 state ?

State

什么是 state

  • has value,有值
  • has identity,有索引
  • can change over time, 随着时间会变化

状态管理难在哪里?

值、索引和时间相互交织,索引和时间尤其复杂。

  • 追踪索引常常导致算法复杂化,比如 React 中的 key
  • 随着时间变化,依赖于状态的一些计算会无效化

语言层面的局限

一般的语言 (比如 js)对 state 这种数据基本都不做原生支持。

这体现在,在这些语言中:

  • 变量是可变的、对象上的字段也是可变的
  • 根本上来说,是次类元素
  • 无法组合
  • 无法分形(decompose)
  • 无法(随着时间)响应变化

什么叫次类元素?

这个说法对应于首类元素 first-class,它

  • 无法通过函数返回
  • 无法作为参数传递

演示局限

无法(随着时间)响应变化

let x = 1       // 创建了一个可变的 state
let y = 2
let sum = x + y // 获取了 state 的快照 snapshot,值为 3

x = 3

sum             // 值还是 3,sum 无法观察 x 赋值后的值,随之变化值为 5

state 不是语言中的 first-class 元素

function foo() {
  let x = 1 // 创建可变的 state
  bar(x)    // 无法将 state 作为参数传递,只能传递值,即 1

  x = 2     // 修改了 state ,但这对于函数 bar 来说是不可知的

  return x  // 也无法将 state 作为返回,只能返回值,即 2
}

如果你了解 js ,知道变量区分值类型和引用类型、形参实参的分别,那么就不会觉得上面的代码有任何奇怪的地方。甚至你会觉得如果 x 重新赋值后, sum 会随之变化为 5、对已经调用完毕的 bar 还能产生影响,那才太可怕了。

但其实上面的代码,并不是在挑战这些东西。而是假设我们创建的 x 都是一种名为 state 的首类元素,它应当可以

  • 作为函数的参数或返回值进行传递,而不仅仅只是传递其计算值,即满足其身为 first-class 的特性
  • 可以被其它引用它的函数或对象观察到它的变化

当然,目前 js 中并不存在这样的首类元素。

Make State Fun Again

neta Make American Great Again, 哈哈

我们试试在 js 中模拟出 State
下文代码都是 typescript

State Interface

interface State<T> {
  get(): T;
  set(value: T): void;
}

构造首类元素 state

我们已经说过首类元素的特性了,可以作为函数的参数和返回值传递。

class Atom {
  constructor(value) {
    this.value = value
  }
  get() {
    return this.value
  }
  set(value) {
    this.value = value
  }
}

现在在组件中,我们就可以声明一个 state 来作为参数了。

Observable state

class Atom {
  constructor(value) {
    this.value = value
    this.observers = []
  }
  get() { return this.value }
  set(value) {
    this.value = value
    this.observers.forEach(observer => observer(value))
  }
  subscribe(observer) {
    observer(this.get())
    this.observers.push(observer)
  }
}

state 能独立于时间变化了(Independence from time)

可分形的 state

decomposable

class LensedAtom {
  constructor({getter, setter}, source) {
    this.getter = getter
    this.setter = setter
    this.source = source
  }
  get() {
    return this.getter(this.source.get())
  }
  set(value) {
    this.source.set(this.setter(value, this.source.get()))
  }
}

把 store state 作为一个整体,而其分片的元素作为组件的 state

可组合的 state

class PairAtom {
  constructor([lhs, rhs]) {
    this.lhs = lhs
    this.rhs = rhs
  }
  get() {
    return [this.lhs.get(), this.rhs.get()]
  }
  set([lhs, rhs]) {
    this.lhs.set(lhs)
    this.rhs.set(rhs)
  }
}
  • 事务性
  • 独立于存储

全局状态的场景

为什么说全局状态更好?

  • 组件因此可以无状态、可以方便地组合
  • 全局状态更容易检查
  • 一切对全局状态的操作测试起来都很简单
  • 全局状态是稳健的单一数据源

为什么不用局部状态

  • 局部状态无法从外部访问
  • 很难组合
  • 只能间接地去测试局部状态
  • 很容易变得散乱

常见的误解

流(streams)是无状态的

一般我们认为 stream 是无状态的,但是请看:

  • 是无状态的吗?
  • merge + scan 引入了局部状态
  • 组织很容易变得散乱
  • 时间变得很重要

不过,从好的方便来说:

  • 它可观察
  • 可以使得依赖更精确:可以方便地观察「是什么触发了这个 stream ?」。

    • 但是没必要。

任何人都可以修改状态将会是一团糟

是的,在我们的方案里,任何人得到了一个 state 的分片,都可以修改它。
但是在 calmm 中,我们已经

  • (限定了)作用域
    我们通过参数赋予组件一部分 state,组件只能修改这部分 state,而不是全部
  • (宣告了)意图
    如果你把可变 state 传递给了组件,这相当于就宣告说,你允许在这个组件中修改 state
  • 观察(了变化)
    即使有人修改了 state,组件也能观察 state 的变化并随之应变。

课后思考

  • 思考下,你到底想把 state 存储在哪里?
  • 同时,你的组件如何持久化 state 呢?

tinkgu
503 声望20 粉丝

{{user.signature}}