不够“坦诚”的 Zustand:我们是否为了函数式而函数式? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
jaydenWang
V2EX    程序员

不够“坦诚”的 Zustand:我们是否为了函数式而函数式?

  •  
  •   jaydenWang 18 小时 13 分钟前 1197 次点击

    引言

    Zustand 是目前 React 生态中最流行的状态管理库之一。它以极简著称,也是我个人非常喜欢的库。 但在长期的使用中,我常常产生一种违和感:我们在一个名为“函数式”的库里,费力地模拟着面向对象。

    那个无处不在的 get()

    来看看经典的 Zustand 写法:

    const useStore = create((set, get) => ({ count: 0, inc: () => set({ count: get().count + 1 }), actionB: () => { // 调用另一个 Action get().inc(); // 获取当前状态 const val = get().count; } })) 

    仔细审视这个 get()

    1. **它就是 this**:它的作用就是访问当前实例的上下文。
    2. 它是“二等公民” :你必须显式地调用它 get(),而且它打破了 JS 引擎对 this 的自然优化。
    3. 心智负担:在写复杂逻辑时,你满屏都是 get().xxx,这并不比 this.xxx 优雅,反而增加了一层函数调用括号的视觉噪音。

    那个黑盒般的 set

    set 函数的设计初衷是好的(提供类似 setState 的原子更新),但在复杂场景下,它显得不够“坦诚”:

    1. 语义模糊set 隐藏了更新的细节。是合并?是替换?是深拷贝?你必须去查文档或看源码才能确定它是 "Auto Merging" 的。
    2. 逻辑断层:当你想复用一段逻辑(比如 Private Method )时,你发现你很难在 create 的闭包里优雅地定义私有辅助函数,往往只能写在外面,破坏了 Store 的内聚性。

    为了函数式而函数式?

    我们推崇函数式编程( FP ),是因为它有 纯函数无副作用引用透明 等数学上的美感。

    但 Zustand 的 Store 定义是纯函数吗?显然不是。它是一个包含了状态( State )和行为( Action )的容器。 在计算机科学中,状态 + 行为 = 对象( Object )

    既然我们本质上是在构建一个对象,为什么要回避 JS 语言原生提供的、经过几十年打磨的构建对象的最佳工具Class

    我们为了避嫌 "OOP",发明了一套 (set, get) => ({...}) 的 DSL 。这不仅牺牲了 Class 的继承、属性访问器( Getter/Setter )等高级能力,还增加了一层理解成本。

    这是否是一种形式上的函数式正确,而非工程上的务实选择

    另一种可能性

    如果在 React 状态管理中,我们不再视 class 为洪水猛兽,而是承认它作为 "Model" 载体的合理性,会发生什么? 这或许值得我们深思。

    14 条回复    2025-12-24 03:37:10 +08:00
    tangdw
        1
    tangdw  
       16 小时 46 分钟前   1
    set 支持函数式更新 set(state => result) 不必在 set 里面调 get
    action 里面最好在顶部声明 const { count } = get() ,要是异步里面的话就只能 get().count 这也是为了避免闭包拿到旧的状态
    jaydenWang
        2
    jaydenWang  
    OP
       15 小时 41 分钟前
    确实可以,但需要这么多"约定"和"注意事项"来规避闭包陷阱时,是否说明这套 DSL 本身在表达"有状态对象"时不够直观
    Al0rid4l
        3
    Al0rid4l  
       15 小时 26 分钟前   2
    就像这个回答说的:
    「前端如今不提倡使用 class 的观点,最终都可以归结于一点:我们讨厌 this 」
    https://www.zhihu.com/question/516551830/answer/1904509977818825306
    https://www.zhihu.com/question/1951689499047334694/answer/1955619666190922360
    wakarimasen
        4
    wakarimasen  
       15 小时 5 分钟前 via Android
    因为我们前端程序员都是共运,阶级( class )是要坚决消除的
    LaTero
        5
    LaTero  
       14 小时 42 分钟前 via Android
    因为 this 不好用,隐式 this 尤甚,比如 C++现在就在搞显式 this 。this 和继承,几乎在每个 OOP 语言都是深坑。我不是专业写 JS 的,所以至今都搞不明白 JS 的 this 和继承……比如箭头函数好像 this 有什么特殊行为,继承又是什么原型链,总之特别复杂,毫无必要的复杂,最好的办法就是不碰它。
    codehz
        6
    codehz  
       13 小时 59 分钟前
    我翻遍了资料也没看 zustand 说它是函数式啊?能不能先别立一个稻草人呢
    jaydenWang
        7
    jaydenWang  
    OP
       13 小时 43 分钟前
    @codehz Zustand 的设计和使用方式,被默认理解和实践为一种‘函数式姿态’,但它在工程语义上其实是在模拟对象模型
    codehz
        8
    codehz  
       7 小时 23 分钟前
    zustand 里唯一看上去是函数式相关的,大概只有不可变数据和纯函数更新这两点了,但完全不能和真正意义上的函数式打等号,一些资料里这样写单纯是因为他们没搞清楚概念,真正重要的是那个 flux 模型,它看似与函数式紧密联系,但实际上是完全不同层次的抽象,只能说有一定的相似性

    Flux 的核心原则包括:
    单向数据流:数据流动是单向的,避免双向绑定带来的复杂性和不可预测性。典型流程是:View 触发 Action → Dispatcher 分发 → Store 更新状态 → View 重新渲染。
    单一真相来源:应用状态集中在 Store 中,而不是散布在各个组件。
    动作驱动更新:状态变化通过明确的 Action 来驱动,便于追踪和调试。

    zustand 只是在这个基础上把开发体验做了一定的提升,简化了使用:
    不需要 Dispatcher 或严格的 Action/Reducer 分离。
    直接在 store 中定义状态和更新函数(这些函数类似于 Action Creators ),通过 set 函数不可变地更新状态。

    至于为啥不用 class ,还不是因为 js 本身局限性,无法高效跟踪深度嵌套类型的变动,只能采取创建新对象的方式来触发更新例如 zustand 同一家出的那个 valtio ,就是使用 proxy 做的状态跟踪,为了解决 js 的局限性,其实不仅带来了很多损耗,也需要遵循很多规则( this 使用规则,还有 proxyMap proxySet 等原生对象包装)写才不会出事
    jaydenWang
        9
    jaydenWang  
    OP
       5 小时 46 分钟前 via iPhone
    @codehz 我很认同你说的 flux 核心原则,但是 zustand 做得并不好,派生状态目前只能通过 usememo 散落在各个组件里,并没有内聚到 store 中。另外一个就是组件可以直接 set zustand 的 state ,并没有限制到只能调用 store 的 action ,在实际开发中就会出现状态和 action 可能都大量散落在组件中的情况
    jaydenWang
        10
    jaydenWang  
    OP
       5 小时 40 分钟前 via iPhone
    @codehz zustand 的使用体验,api 设计的非常棒。我觉得它缺失的是:set 不是私有方法,没能限制在 store 中调用;派生状态没有内聚在 store 中
    codehz
        11
    codehz  
       5 小时 11 分钟前 via Android
    @jaydenWang zustand 的派生不是直接在 selector 里写的吗,我倒好奇你是怎么用的
    语法上确实没阻止你直接导出 set 但一般人也不会这么写啊
    codehz
        12
    codehz  
       5 小时 7 分钟前 via Android
    主要是就算你想在外面直接调用 set 也可以使用那个 selector 函数的 setState 方法,所以我完全不知道为啥你有导出 set 的想法()
    codehz
        13
    codehz  
       4 小时 58 分钟前 via Android
    zustand 的核心就是那个 selector 机制,避免了 context 的牵一发动全身问题
    其他的一切都只是为了一个简单好用的 api ,包括那个 set get 的设计,做成这样是为了能方便被中间件扩展,某种意义上说,相比于函数式或者面向对象,zustand 的 middleware 设计更偏向于 aop 也就是面向切面编程
    netabare
        14
    netabare  
       1 小时 15 分钟前 via iPhone
    AI ?怪不得那么喜欢 OOP 。

    有没有一种可能,你说的「 class 继承」这种「高级」功能恰恰就是 OOP 为什么坏的原因。

    那个 inc 确实丑,但我不认为这个模式有什么问题,这就是最平凡的 lambda 。「 JS 十几年打磨的 this 和 class 」,我愿意称这是今天看到的最好笑的笑话。

    至于「 class 作为 model 载体」,这句话对我来说就是一个危险信号:不再在乎「 model 如何参与渲染循环,而开始把它像业务、intelligence 一样当一个大垃圾堆,像后端人那样把一切看不懂的或者不想理解的东西塞进去」,而这就是 what class supposed to do 。

    反过来看这个「没有 this 的坏代码」:

    命名明确:count 、inc 一目了然
    显示调用:我调用 set/get 的时候我会被迫知道我在干嘛

    以及 OOP 不是只有 Jaba ,actor model 也是 OOP 。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     892 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 21ms UTC 20:53 PVG 04:53 LAX 12:53 JFK 15:53
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86