Hypocrite Nush
  1. 1 Hypocrite Nush
  2. 2 One Last You Jen Bird
  3. 3 Warcry mpi
  4. 4 Life Will Change Lyn
  5. 5 Flower Of Life 发热巫女
  6. 6 The Night We Stood Lyn
  7. 7 Libertus Chen-U
  8. 8 かかってこいよ NakamuraEmi
  9. 9 Last Surprise Lyn
  10. 10 Time Bomb Veela
  11. 11 Quiet Storm Lyn
2022-10-23 22:05:52

状态管理方案发展概览

自前端进入现代化时期以来,状态管理就是个经久不衰的话题。这篇文章就以react这个经典的前端框架为切入点从头理一理状态管理方案的发展历程。

为什么需要状态管理

现代化的前端框架都强调一件事:开发者只需要集中精力在状态数据到视图的映射处理,其中的渲染细节由框架接管。但状态数据本身其实就是一种很杂的数据源,且特征随着来源的不同而不同。比如

  • 来自服务端的应用数据
  • 页面间需要共享的局部前端数据
  • 用户信息等全局数据
  • 等等

虽然react本身提供了一些管理状态的方案,但都不够完善,各有各的问题。

  • state提升:嵌套组件传参繁琐,路径组件的无意义重渲染。
  • context: 只能存储单一值,无法存储多个各自拥有消费者的值的集合,且Provider也容易嵌套。

react迭代至现在的18版本,虽然架构上出现了很多变化,但上述问题依然存在,所以不断地有三方状态管理方案产生,适应react本身的架构变化的同时,来解决状态数据管理的问题。

Flux

严格来说,Flux并不是一套具体的状态管理方案,而是一种架构思想。它的诞生非常重要,主要在于在当时视图状态无规则更新的年代,提出了一个简单的约束规则:单向数据流。以此来保证视图层对状态的操作和渲染是可预测的。

简单介绍就是在Flux架构中,状态的流转需要以Action为媒介经过3个模块:

  1. View:视图层。在这里交互触发一个Action。
  2. Dispatcher: 分发层。收到触发的Action,找到并通知对应Store进行状态数据更新。
  3. Store: 对应Store收到更新请求,更新数据后将最新数据流转到View。

以上就是一次可预测的单向数据流转。以Flux为起始,后续的状态管理方案或多或少都借鉴了思想。

Redux

redux就是基于Flux思想实现的一套最有名的方案。在Flux基础上进一步提出了3大原则:

  • 单一数据源store
  • state只读
  • 纯函数修改state

并提供了一套具体的flux架构实践方案。虽然redux的这套方案在当时非常流行,但回过头看其实并不是一套很通用的方案,当想用其解决通用项目的状态管理问题时,就会遇到非常多的问题。包括但不限于:

  • 繁琐的模板代码
  • 全触发再拦截的订阅更新效率较低
  • 基于字符串实现的action调用无法开发时跟踪定义代码,开发体验较差

前文也说了,项目中的状态来源是很杂的,也各有特征,当想用统一的全局单一store去管理时,实际上是一种偷懒。

mobx

和redux同期风靡的另一套方案就是mobx了。严格来说mobx只是一套引擎实现,对标的状态管理方案是Mobx State Tree。其核心在于实现了一套和Vue相同的数据响应系统强制结合react视图更新。在当时的class时代用着问题确实不大,但不可否认的是这套基于mutable的响应式更新实现和react的immutable理念是相违背的,这个问题在hooks时代react大改为并发架构之后显得更加明显,很难和hooks的新特性结合,反而会带来一系列问题。

就我看来,这套状态依赖收集并触发视图精确更新的理念很好,但需要和视图层的渲染做深度结合才算是比较完美,而这点vue就做的很好。如果喜欢这套方案,为什么不出门左转vue呢。

新时代的状态管理

从上文也可以看到,我对redux和mobx的评价并不高,但这也是基于时代特征的,就像jquery一样,一代工具解决一代问题。react的架构变化,带来了hooks,随之而来的也是基于此的一系列新生代状态管理方案。从这一步开始,我们终于能够得到贴合react渲染体系的简洁好用的状态管理方案了。

Recoil

这个状态管理库应该是hooks时代第一个进入大众视野的框架,因为开发团队背靠facehook,所以也受到了不少的关注。

相比于class时代的单一store分发到各个组件的自顶向下的数据组织方式,recoil反其道而行之,Recoil采用了自底向上的原子化状态数据组织形式。同时由于react的hooks的出现,新时代状态管理方案包括Recoil更倾向于结合其特性,而不是独立于react外部靠连接工具结合。

Recoil核心设计在于定义了一个有向图 (directed graph),正交同时又天然连结于你的 React 树上。状态的变化从该图的顶点(我们称之为 atom)开始,流经纯函数 (我们称之为 selector) 再传入各个组件。

状态的定义从最底层的atom开始。

const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

使用方式很简单,和hooks的语法一致。

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}

自底向下的组织状态生成由selector完成。selector代表一个派生状态,类似于Vue的计算属性,由纯函数生成。这使得我们避免了冗余atom,通常无需使用 reduce 来保持状态同步性和有效性。所有状态数据根据最小粒度的状态进行有效计算。

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => {
    const fontSize = get(fontSizeState);
    const unit = 'px';

    return `${fontSize}${unit}`;
  },
});

综上可知,Recoil的核心思想就在于

  • 利用hooks的内部特性操作状态流转
  • 自底向下的组织状态
  • 保持尽可能简洁并且和react一致的API,降低心智负担和样板代码的存在

以上也是hooks时代状态管理方案的一个大方向。

zustand

当然如果你更喜欢类flux的自顶向下的状态流转,依然有合适的选择。zustand和redux很像,不过相比redux更加适合hooks的写法,也没有繁琐的样板代码。同时也不强制单一store,相对回归原始flux的理念。

import create from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

jotai

jotai和Recoil类似,是一个受Recoil启发的自底向上的原子化模型状态管理方案。Recoil中的atom通过hooks和selector纯函数来组合、创建、更新。只有使用到该atom的组件才会在atom更新时触发re-render。因此在原子式中,无需定义模版代码和大幅改动组件设计,直接沿用类似于useState的API就能实现高性能的状态共享和代码分割。

虽然Recoil宣称的高性能原子式状态管理非常诱人,但不能忽视的是Reocil本身设计相当复杂,为了适用于更复杂的大型场景,Recoil拥有高达数十个APIs,上手成本不低。而且为了规避Context API的问题,Recoil使用了useRef API来存放状态并在内部管理状态订阅和更新,严格意义上状态也并不算在React Tree中,同样面临着外部状态的Concurrent Mode兼容性问题。

jotai就是在Recoil的基础上解决以上问题的一个新方案。相比于Recoil,jotai有以下两个特点:

  • 更原生:jotai的基础接口更接近于原生语法,更简洁。
  • 更灵活:派生原子状态能和其他原子状态结合,并且以atom对象本身为唯一标识,不用声明额外的字符串。

声明atom很简单。

import { atom } from 'jotai'

const priceAtom = atom(10)
const messageAtom = atom('hello')
const productAtom = atom({ id: 12, name: 'good stuff' })

声明派生atom也不需要引入额外的概念。

const readOnlyAtom = atom((get) => get(priceAtom) * 2)
const writeOnlyAtom = atom(
  null, // it's a convention to pass `null` for the first argument
  (get, set, update) => {
    // `update` is any single value we receive for updating this atom
    set(priceAtom, get(priceAtom) - update.discount)
  }
)
const readWriteAtom = atom(
  (get) => get(priceAtom) * 2,
  (get, set, newPrice) => {
    set(priceAtom, newPrice / 2)
    // you can set as many atoms as you want at the same time
  }
)

消费atom时也不用区分原生atom和派生atom,统一使用useAtom即可。

const [value, updateValue] = useAtom(anAtom)

对比和分类

最后对hooks时代的3种方案进行一个简单的对比和分类吧。

jotai vs zustand

  • 名称:日文和德文
  • 类比:recoil和redux
  • 状态保存的地点:react组件树内部 react外部
  • 结构化数据的方式:自底向上(atoms) 自顶向下(object)
  • 技术区别:主要区别在于状态模型。zustand是单一状态。jotai是可以合成的原子状态。

jotai vs Recoil

  • 开发团队背景:jotai是少量成员组成的开发团队。recoil是背靠facebook。
  • 语法基础:jotai聚焦于原始简单的API。recoil有更复杂的面向不同场景的API。
  • 技术区别:jotai依赖于atom对象作为唯一标识。recoil依赖字符串。

分类

中心化:Redux,Zustand

原子化:Recoil, jotai

store存放在外部:redux

store存放在内部:Recoil, jotai

小结

目前我比较倾向于将项目中的范围局部状态交给jotai,全局状态交给zustand,IO状态由更专业的工具(useSWR,react-query)处理。当然这也不是唯一方案,只是我自己的习惯。redux依然是项目中使用最广泛的方案,有人喜欢也完全没问题。这篇文章就是提供更多的选择项。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「React」「工程化」 标签。