likes
comments
collection
share

对现代主流框架状态管理库的思考

作者站长头像
站长
· 阅读数 3

为什么

这里所指的状态管理库泛指,vue/react/angular 三大框架相关的状态管理库

状态管理在我看来其实是个非常宽泛的概念,我更愿意称它是个跨层级全局上下文共享配置。比如放在 class 中可以是 this,放在服务端单机上叫 全局单例的class,放集群上就是配置中心。这些放置的地方有的并不是特意作为存放共享配置用的,但是并不妨碍都可以做到共同的事情

时到今日聊这个话题,一是想分享自己的看法,二是也想请教下正在看这篇文章的你,是否有和我不同的看法,~欢迎以任何方式联系到我说出你的独道的见解

源头

第一次有想这个东西苗头,是几年前在我学了 react 之后,那时候我只会 vue,也只接触过 vuex, vuex 用起来很顺手,反观当时 react 最火的是 redux,初学我不明白这东西为什么这么复杂,过了许久我自认为精通后,我开始不理解,老外为什么总能能把简简单单做到的事情,要硬套概念搞得这么繁琐

会这么想是因为我想到了基于 react 的另一种实现方式,也做出来了雏形,代码非常的少,所以写的很快

雏形

实现的基本原理就是 useState + 订阅发布者模式,它使用方式大致如下

首先创建使用给定函数创建初始数据

组件内使用时,我就在内部使用 useState 创建状态,然后收集返回的 setState 函数,返回创建函数的状态,以及模拟一个 set 函数返回给用户

这样每次当用户在任意用到的组件内修改状态时,我只需要把当前收集到的 setState 函数全部调用一遍。组件外访问则可以单独提供一个函数用于获取

const map = new Map()
function createStore(initState) {
    const flag = Symbol()
    map.set(flag, { state: initState, sets: new Set() })
    return flag
}
function getStore(flag) {
    return map.has(flag) && map.get(flag).state
}
function useStore(flag) {
    const { state, sets } = map.get(flag)
    const [s, set] = useState(state)
    sets.add(set)
    const _set = (v) => {
        Object.assign(state, v)
        sets.forEach(f => f(state))
    }
    return [s, _set]
}

//定义状态
const store = createStore({ msg: "这是初始状态", fn: ()=>1 })

//组件内使用
function App() {
  const [store, setStore] = useStore(store)
  return
}

//组件外使用
const store = getStore(store)

以上是简化版代码,表达个大概原理

虽然实现上非常的淳朴,但是也算做到了状态共享和响应式更新,其他的功能全都没有

如果有用过 jotai/ valtio/ zustand 的话,会发现原理和这种原子状态管理库是非常像的。之所以没有继续做下去是我觉得这是邪门歪道。本意是想找个简单的方式实现共享状态的响应式更新,因为用 redux 时我总是忘这破玩意应该怎么写,明明并不复杂的一个东西却搞很多模版代码。可如果放出来觉得就是找骂的,所以后来当 zustand 盛行后,我大为震惊

现代框架的状态管理

讲这个往事不是想表达,它们都是一群渣渣,我上我也行的炫耀。而是想说我曾经很早的时候就做过,这只是其中一个,因为做过所以了解些许其中的门道

状态管理本身是个大话题,我没资格讲它。但现代的框架的状态管理还是能唠唠的,这里我们可以尝试对比下,不同风格的状态管理库之间的共同点和差异性

代表框架的特点如下

框架名称代码风格
vue/pinia基于 vue 的响应式 api,更进一步则可以说是基于 proxy
react/redux典型的订阅发布者模式,需要创建,映射,消费,卡修改流程...
react/zustant原子状态管理库,get/set 风格
react/jotai原子状态管理库,proxy 的响应式风格
angular依赖注入

它们的共通点

  1. 全局组件可访问
  2. 响应式状态更新
  3. 有着各自的代码风格/规范(这个还真不是可有可无的东西!!!)
  4. 可扩展
  5. 可提供调试(例如 devtools 集成)

它们的差异点

  1. 实现风格不同
    1. get/set 的函数流派
    2. proxy 的响应式流派
  2. 特色功能不同
    1. 例如有的能严格卡更新环节,例如 redux,而基于 proxy 的在这方面总会出现漏洞
    2. 使用便捷程度,基于 proxy 舒服的很

细节上的东西会有很多,扣起来没完没了了,我这里只是列出来了宏观上的

把这些东西列出来后,就意味着,如果我们只要做到了共同点的部分,尤其是前两条,它就可算是一个最低限度的,生产可用的版本了

对管理库进行抽象

核心抽象

如果实现一个状态管理库本身在我看来并不难,甚至精简成 2 个词来总结

  1. 共享
  2. 管理

第一点很好理解,我们用管理库的本身可不就是为了跨组件共享状态么

第二点管理需要绕个弯子想,因为不好好想想就会觉得这东西是个可有可无的东西了

可能有人疑惑,全局响应式更新难道就不算是必备条件吗?状态管理库别管是不是官方出的,要实现响应式更新就得依赖框架的响应式系统,框架即便不提供,只要能共享状态,用户自己也能做到。之所以每个框架都提供相关的能力,不如想想如果不提供还有谁用啊!!!

如何管理

共享和一些默认的东西 (响应式更新)都是必备功能,而如何进行状态管理则是各家状态管理库真正的卖点所在

管理主要包含了两方面内容

  1. 如何对状态,从创建到消费,这之间各个环节进行卡点。比如提供生命周期,让用户的插件可以在修改状态前干点什么,修改状态后干点什么,典型的应用是持久化保存数据到 localstoreage
  2. 代码风格/规范。不同的风格规范直接决定了用户的使用形式和对外提供什么功能。比如 vue 的用户可曾想过,使用本身的响应式系统和使用 pinia 之间的差异是什么吗?再比如 redux 是没法做出来像 jotai/pinia/vuex 一样的简单的 api 用么,那肯定是能做到的

概念套套套

弄清楚了管理库核心抽象后,我们在看看看 vue/react 生态中的相关库

Vue

vue 中应该会有很多人有疑问 pinia 到底该不该用,它和直接用 vue 的响应式系统,直接 reactive 一个对象用到底差异在了哪里

这个答案很简单,pinia 提供了默认的开发规范,如怎么定义状态、修改状态的位置和方式、使用状态的方式,反之直接用 reactive 则没有,我们需要自己定义,应用规模不大都用不到定义,只要不放飞自我影响都不大

功能上则是一模一样,所以对规范不看重的想要自定义团队规则的,完全可以自己定几条团队规则,这样反而比直接用 pinia 能剩下很多的打包体积,至于最大的损失,我觉得就是 devtools 了,但现在有多少人喜欢用这东西来调试的?

注:这个做法是尤雨溪亲口承认的,b 站有本人的访谈视频可以自行查找

React

react 的生态真的是百花齐放,相较于 vue 总是上来不管三七二十一的上 pinia,我还是更喜欢 react 的这种模式。因为我不喜欢 vue/angular 官方提供了推荐,然后用户就无脑用,我不喜欢把简单的东西搞复杂,它们会产生诸多模板式的用法和无聊的八股文,我尤其恨死这方面的八股文,看着面试官一本正经问管理库的原理和使用心得,说的抽象点还不认,无语死

对于 react 生态的众多的管理库来说,用哪个学哪个无非是自己团队的更喜欢用哪种风格的代码,只要核心功能没 bug,代码写的轻松就够了,应该没什么人喜欢搞这方面的插件吧!!!像 redux 那样,一个小小的库,硬生生靠着各种扩展搞得功能五花八门的,新手直接哭晕在了厕所

注:越是复杂的项目,个人更推荐 get/set 风格的,反之则是基于 proxy

其他场景

这里我想着重来聊聊微前端,实现微前端的框架并不重要。在微前端下,一个页面会有若干个不同框架的项目共同工作,基于之前的抽象我们就可以套进来,做个非常简单的,生产也够用的版本,即我们只需要满足,共享+响应式更新就够了

比如我们可以在全局(尽量放在基座应用会更好,防止被攻击)搞个对象实例,然后提供 get/set 方法,提供它们是为了可以调试哪些应用操作了状态,想做的简单直接一个 new Map 也行。不建议用 proxy 代理,因为以外修改状态的漏铜太多,性能也差,想要做高质量 bug 少的在这里就不能用这玩意

在各自的子应用中,可以提供一个转换函数,该函数需要把状态塞进各自应用的状态管理库内部。然后双向监听管理库内部的变化以及全局库的变化,做到状态同步。虽然是实现起来不是很优雅,但确实最简单无脑的做法之一