「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 呢?
相關文章
相關標籤/搜索