zustand/redux漫谈

746 阅读5分钟

简介

本文主要涉及zustand核心代码简要介绍、部分和redux之间的一点有趣的问题、github homepage 简介中提到的问题的解释和部分简介的注释以及其他一些大佬们的观点链接

先看设计原则

与mutable阵营的mobx, valtio对比,zustand和redux一样属于immutable阵营

Flux架构图(Flux 是一种架构思想,本质上跟MVC架构是同一类东西,但是更加简单和清晰---阮一峰)

Flux 是 Facebook 官方构建 Web 前端应用体系架构,通过数据的单向流动有效补足了 React 组件间通信的短板,Flux 架构思想主要由如下 4 个部份组成:

  1. Action:视图层发出的动作信息,可以来自于用户交互,也可能来自于服务器响应。
  2. Dispatcher:派发器,用来接收 Actions 并执行相应回调函数。
  3. Store:用来存放应用的状态,一旦发生变化就会通知视图进行重绘。
  4. View:React 组件视图。

单向数据流

所谓的单向数据流(unidirectional data flow)是指用户访问View,View发出用户交互的Action,Dispatcher收到Action之后,要求Store进行相应更新。Store更新后会发出一个change事件,View收到change事件后更新页面的过程。这样数据总是清晰的单向进行流动,便于维护并且可以预测。

简单看下Flux Redux对比:

Redux受到了Flux架构的启发,但在实现上有一些不同:

  • Redux并没有 dispatcher。它依赖纯函数来替代事件处理器,也不需要额外的实体来管理它们。Flux尝尝被表述为:(state, action) => state,而纯函数也是实现了这一思想。
  • Redux为不可变数据集。在每次Action请求触发以后,Redux都会生成一个新的对象来更新State,而不是在当前状态上进行更改。
  • Redux有且只有一个Store对象。它的Store储存了整个应用程序的State。

redux设计灵感来源(来自github主页)

看下zustand的设计: designed for single store

zustand既可以单store又可以多store使用,但设计原则上还是单store (多store下面会提到)

再看下核心代码

核心API代码(简单抽象版本)

import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';

function createStore(createState: Function) {
  let state: any;
  const listeners = new Set<Function>();
  const setState = (partial: any, replace: any) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial;
    if (nextState !== state) {
      const previousState = state;
      state = replace ? nextState : Object.assign({}, state, nextState);
      listeners.forEach((listener) => listener(state, previousState));
    }
  };
  const getState = () => state;
  const subscribe = (listener: any) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  const destroy = () => listeners.clear();
  const api = { setState, getState, subscribe, destroy };
  state = createState(setState, getState, api);
  return api;
}

function useStore(api: any, selector: any, equalityFn: any) {
  return useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getState,
    selector || api.getState,
    equalityFn,
  );
}

export function create(createState: Function) {
  const api = createStore(createState);
  const useBoundStore = (selector?: any, equalityFn?: any) => {
    return useStore(api, selector, equalityFn);
  };
  Object.assign(useBoundStore, api);
  return useBoundStore;
}

默认不需要 Provider:直接声明一个 hooks 式的 useStore 后就可以在不同组件中进行调用,它们的状态会直接共享

状态管理最必要的一点就是状态共享。这也是 context 出来以后,大部分文章说不需要 redux 的根本原因。因为context 可以实现最最基础的状态共享。但这种方法(包括 redux 在内),都需要在最外层包一个 Provider。 Context 中的值都在 Provider 的作用域下有效。

再来看下 useSyncExternalStoreWithSelector做了什么

核心代码抽象如下

interface ExternalStore<T> {
  getState(): T;
  subscribe(listener: () => void): () => void; // return unmount
  dispatch(updater: T | ((prevState: T) => T)): void;
}

export const useCustomStore = <T>(store: ExternalStore<T>) => {
  const [state, setState] = useState(store.getState());

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setState(store.getState());
    });

    return () => { unsubscribe(); };
  }, []);

  return state;
};

useSyncExternalStoreWithSelector来源简介: 

迭代过程可参考这个discussion:github.com/reactwg/rea… (useMutableSource → useSyncExternalStore -> useSyncExternalStoreWithSelector)

useSyncExternalStoreuseSyncExternalStoreWithSelector

react-redux useSelector API,为了保证库的状态更新逻辑跟react一致都也换为了useSyncExternalStoreWithSelector

下面是react-redux useSelector的 selector逻辑 v8和v7版本的对比

中间件

和redux中间件有点类似,但没有applyMiddleware进行中间件的统一柯力化管理

immer

改写了store.setState state加produce处理一遍

自定义中间件(log

向redux习惯兼容

关于简介里提到的 zombie child problem

先看一个栗子react-redux的一个问题

cn.react-redux.js.org/api/hooks/#…

以及上面官方文档附带的链接,大家可以重点看下这篇文章,对react-redux的迭代版本如何解这个问题一一做了拆解,极力推荐

react-redux最近的两个版本解决办法:v7版本解决办法,核心代码如下,v8已经替换为了useSyncExternalStoreWithSelector, 这个上面已经讲过了

再看下zustand

readme描述解决了这个问题, 但是还是存在另一个类似的问题:

github.com/pmndrs/zust…

解决办法:batchupdate: codesandbox.io/s/fast-shad…

github.com/pmndrs/zust…

关于react concurrency, and context loss

我在zustand github中向作者的提问的回答:discussion

关于github主页提到的几个点

why zustand over redux

  • Simple and un-opinionated
  • Makes hooks the primary means of consuming state
  • Doesn't wrap your app in context providers
  • 由于没有 Provider 的存在,所以声明的 useStore 默认都是单实例,如果需要多实例的话,zustand 也提供了对应的 Provider 的写法(这块参见文末的第一篇文章)

常规书写方式:

Provider的书写方式:

对比react-redux, 不需要进行re-render

Why zustand over context?

  • Less boilerplate

    • Renders components only on changes
  • Centralized, action-based state management

看到context这块,就顺藤摸瓜查了下面这些关于大佬们对context的一些观点:

顺便看下关于context的理解Sebastian大佬的一些观点

github.com/facebook/re…

twitter.com/sebmarkbage…

@da-shi 对这段话的解释

那什么时候用context? 取决于你的实际场景,比如下面这个例子

最佳实践

参考文章:

  1. mp.weixin.qq.com/s?__biz=Mzg…
  2. You Might Not Need Redux
  3. github.com/facebook/re…
  4. github.com/facebook/re…
  5. formidable.com/blog/2021/s…