「State」之我见

原文:what-is-my-statejavascript

阅读前须知前端

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

口味调查

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

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

前情提要

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

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

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

本文的目标

但愿我们能从一个崭新的角度讨论 state ?算法

State

什么是 state

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

状态管理难在哪里?

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

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

语言层面的局限

通常的语言 (好比 js)对 state 这种数据基本都不作原生支持。编程

这体如今,在这些语言中:框架

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

什么叫次类元素?

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

  • 没法经过函数返回
  • 没法做为参数传递

演示局限

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

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 会随之变化为 五、对已经调用完毕的 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 呢?
相关文章
相关标签/搜索