全网最全!盘点2023年20大前端状态管理库

前端娱乐圈,造轮子能力说第二,没人敢说第一。今天我们就来盘盘前端领域中状态管理库这个细分领域,看看到底谁才是无冕之王?谁又是绣花枕头?

自从 14 年 Facebook 发布 Flux 开始,状态管理这个概念成为了现代前端开发中不可分割的一个重要组成部分。自此之后,几乎每隔一段时间就会出现一种或几种新的前端管理库。有一些是大厂出品、品质保证(当然也可能是为了 KPI),也有一些是主打一个概念新潮、蹭蹭热度。在琳琅满目、五花八门的状态管理库的英雄池里,你是否有过迷茫、惊愕、进退两难、束手无措的感觉?

也有人会问了,这都 2023 年了,怎么还在卷状态管理?没办法,前端就是这个样子。不卷,就没热度,没有热度,就没有活跃度,没有活跃度,就没有存在感。没有存在感,前端还怎么混?

今天 Noah 就带大家盘点一下,从 2014 年起,到 2023 年止,这 10 年里最具影响力、最具代表性和最流行的 20 个前端状态管理库。

正式开始之前,先废话几句。

没什么特殊的,还是感慨时间过得飞快,一转眼一年又过去了,2022 年立下的 flag 算是只完成了一半,只能说计划没有变化快,人生就是这样,世事无常,大肠包小肠。掘金 2023 年的年终总结征文活动如约而至,看到了当然不能假装看不见,尽管今年的复盘和明年的规划也还没有一个非常清晰的思路。但是文章该写还是要写的。

2022 年的年终总结在这里,感兴趣的同学可以去看一下:24 岁技术人不太平凡的一年

写这篇文章之前,我在掘金看了下,讲前端状态管理库的人不少。但是很多都是抄抄官方文档的介绍和 Demo,深入研究原理的人还是少数,至于一次性盘点 20 个前端状态管理库的人,我估计是第一个。所以我觉得写这篇文章还是蛮有意义的。

这篇盘点文不是浅尝辄止,而是从时代背景、技术理念、API 演示、原理分析和自我实现等多个维度全方面深度剖析每个库。这可能是近年来前端状态管理这方面覆盖面最广、研究最深的一篇文章。(没有自吹自擂哦!)

好了,废话结束,咱们进入正题。

单向数据流系鼻祖 - Flux

flux 对于现在很多前端朋友来说可能比较陌生,对 flux 很熟悉的人估计都是些老前端了。

时间回到 2014 年,前端处于刚刚独立但还没有完全独立的阶段。那时候 Meta 还叫 Facebook,社交软件还处于高速增长期,Facebook 和小扎还处于全世界互联网的中心。

Facebook 正在开发他们的广告应用 Facebook Ads。Ads 主要的场景是 Web,但是在开发时面临着很大的挑战。这些挑战无非就是前端代码的可维护性、可扩展性等问题。当时还没有统一的状态管理概念,所以 Bug 频出。

后来 Facebook 的软件工程师 Jing Chen 在 React.js Conf 上提到了 Flex 架构,她认为当时最流行的 MVC 架构处理复杂数据流时力不从心,得换种更好的玩法。那就是单向数据流的 Flux。当时为什么 MVC 最流行?因为当时前端框架的老大 AngularJS 就是依赖双向数据绑定和 MVC 架构,所以当时大家都用 MVC 来同步数据和视图。另外一点,Flux 还借鉴了函数式编程中的数据不可变性哲学(Immutability),这在数据流管理和状态更新的预测性方面都起到关键作用。

Flux 的技术架构是这样的:它有一个严格的单项数据流动模式,视图(View)触发动作(Action),动作(Action)会被分发器(Dispatcher)接收,分发器会把动作发给多个存储(Store)中的一个或者多个。由存储来处理动作,然后更新状态。视图监听存储的变化,当存储变化时,视图会自动更新。

这个技术架构看似简单,实际上它保证了应用的状态变化始终是可预测的。

其中有很多概念需要去理解,我这里用 Flux 写一个计数器的 Demo 带大家感受一下:

jsx 复制代码
import { Dispatcher } from 'flux'; // 引入 flux 库中的 Dispatcher

// 创建一个新的分发器实例
const appDispatcher = new Dispatcher();

// 定义动作类型常量
const ActionTypes = {
  ADD_ITEM: 'ADD_ITEM'
};

// Action 创建函数
const AppActions = {
  addItem(item) {
    appDispatcher.dispatch({
      actionType: ActionTypes.ADD_ITEM,
      item: item
    });
  }
};

// Store,存放应用状态和逻辑
class AppStoreClass {
  constructor() {
    this.dispatcherIndex = appDispatcher.register(this.dispatcherCallback.bind(this));
    this._items = []; // 私有变量,存储列表项
    this._changeListeners = []; // 存储监听变更事件的回调函数
  }

  // 注册分发器回调函数,处理动作
  dispatcherCallback(action) {
    switch (action.actionType) {
      case ActionTypes.ADD_ITEM:
        this._items.push(action.item); // 处理 ADD_ITEM 动作
        this._emitChange();
        break;
      default:
        // 不处理其他动作
    }
  }

  // 触发变更事件
  _emitChange() {
    this._changeListeners.forEach(listener => listener());
  }

  // 添加变更监听器
  addChangeListener(callback) {
    this._changeListeners.push(callback);
  }

  // 移除变更监听器
  removeChangeListener(callback) {
    this._changeListeners = this._changeListeners.filter(listener => listener !== callback);
  }

  // 获取当前所有项
  getAllItems() {
    return this._items;
  }
}

// 创建 Store 实例
const AppStore = new AppStoreClass();

// 以下是使用 Store 的视图部分,这里我们假设使用了如 React 的某个库
// 实际上,你需要根据所使用的框架来实现视图更新的逻辑
class ViewComponent {
  constructor() {
    // 视图组件初始状态
    this.state = {
      items: AppStore.getAllItems()
    };

    // 当 Store 发生变化时更新视图状态
    AppStore.addChangeListener(() => {
      this.setState({ items: AppStore.getAllItems() });
    });
  }

  setState(newState) {
    this.state = newState;
    this.render(); // 在设置新状态后,重新渲染组件
  }

  // 添加一个新项到列表
  addNewItem() {
    AppActions.addItem('新项目');
  }

  // 渲染组件的方法,在实际的视图库中,这个方法可能包含更复杂的逻辑
  render() {
    console.log('渲染列表', this.state.items.join(', '));
  }
}

// 使用示例
const viewComponent = new ViewComponent();
viewComponent.addNewItem(); // 添加一个新项,会触发 Store 变化并重新渲染视图

可以看到虽然架构中的概念定义清晰,代码结构规整,但是代码量非常冗余,开发时套模板的味道很明显。虽然存在一些瑕疵,但是真真切切解决了当时 Facebook 的问题。

现在我根据我对 flux 架构的理解,用 JS 来模拟实现一下 Flux 的 API,可以帮助大家更好理解 Flux 中的各种概念。

  1. Actions - 用来定义发生了什么,通常是一个简单的对象,包含一个类型(type)和其他数据。
js 复制代码
// Action creators
function createAction(type, payload) {
  return { type, payload };
}
  1. Dispatcher - 用来分发 action 到 store 的中心枢纽。
js 复制代码
class Dispatcher {
  constructor() {
    this.isDispatching = false;
    this.actionHandlers = {};
  }

  register(callback) {
    const id = Date.now().toString(36);
    this.actionHandlers[id] = callback;
    return id;
  }

  dispatch(action) {
    if (this.isDispatching) {
      throw new Error('Cannot dispatch in the middle of a dispatch.');
    }
    this.isDispatching = true;
    try {
      Object.values(this.actionHandlers).forEach(callback => callback(action));
    } finally {
      this.isDispatching = false;
    }
  }
}

const dispatcher = new Dispatcher();
  1. Stores - 保存数据状态和逻辑的容器。响应 actions 并更新状态。
js 复制代码
class Store {
  constructor(dispatcher) {
    this.__state = {};
    this.__listeners = [];
    this.__dispatchToken = dispatcher.register(this.__onDispatch.bind(this));
  }

  __onDispatch(action) {
    // 通常这里会根据不同的 action.type 来更新状态
    console.log(`Action received: ${action.type}`);
  }

  getState() {
    return this.__state;
  }

  addChangeListener(callback) {
    this.__listeners.push(callback);
  }

  removeChangeListener(callback) {
    this.__listeners = this.__listeners.filter(listener => listener !== callback);
  }

  __emitChange() {
    this.__listeners.forEach(callback => callback());
  }
}

// 使用 Dispatcher 实例化 Store
const store = new Store(dispatcher);

基础的 API 实现完成后,我们再来实际运用它完成一个场景:

js 复制代码
// 订阅 store 变更
store.addChangeListener(() => {
  console.log('Store changed:', store.getState());
});

// Action dispatch 示例
const incrementAction = createAction('INCREMENT', { amount: 1 });
const decrementAction = createAction('DECREMENT', { amount: 1 });

// 修改 Store 的 __onDispatch 方法来处理不同的 action
store.__onDispatch = function(action) {
  switch (action.type) {
    case 'INCREMENT':
      this.__state.count = (this.__state.count || 0) + action.payload.amount;
      this.__emitChange();
      break;
    case 'DECREMENT':
      this.__state.count = (this.__state.count || 0) - action.payload.amount;
      this.__emitChange();
      break;
    // 其他 action.type ...
  }
};

// 触发 Action
dispatcher.dispatch(incrementAction);

尽管 Flux 提供了清晰的架构,但是它也存在明显的缺点和局限性:

  1. 学习曲线高,新手玩不明白。
  2. 样板代码多,工作量会很大,老手不想写。
  3. 缺乏具体的规范化,只提供了思想,没有具体约束手段。
  4. 多个 store 管理困难,当应用很复杂的时候,多个 Store 之间的依赖和通信会很麻烦。
  5. 性能一般,每个 Action 都会通知所有 Store,如果 Store 过多会影响性能。

在 Flux 发布后的第二年,也就是 2015 年,Facebook 又野心勃勃地发布了 Flux 的替代品 Relay,主要围绕 GraphQL 设计,但是这次令 Facebook 失望了,Relay 的影响力没有 Flux 那么高,因为 Facebook 赌错了,GraphQL 并没有成为业界主流,而仅仅是昙花一现。所以我认为 Relay 没有什么代表性,在本文中也没有介绍它。世界就是这样的,总是能记住第一,但没人会记住第二。所以有人会认为第二才是最大的失败者。

相关信息梳理归纳:

单向数据流系名门望族 - Redux

受到自家 Flux 的启发,Facebook 研发团队中的 Dan Abramov 和 Andrew Clark 在 2015 年研发了另一个状态管理库 Redux。这里提一句,Dan 是 React 圈,也是前端圈里炙手可热的明星人物。

实际上 Redux 几乎完全把 Flux 中的概念拿了过来,但是经过更深一层次的思考,做出了一些改良和增加了一些功能。

我认为 Redux 做出的主要改动有以下几点:

  1. 简化状态管理,提倡单一 Store 和单一 Dispatcher。这样可以简化状态管理,同时让状态变化更加可以预测。
  2. 增强纯函数和数据不可变性,增加了 Reducers 的概念,Reducers 监听 action,并且根据旧 state 和 action 返回一个全新的 state 对象。这个过程遵循不可变性原则,所以每次都新对象,而不是直接修改原有的 state。
  3. 增加了中间件的概念,这样开发者可以在 action 发送到 reducers 的之前或者之后注入自定义的功能。
  4. 提供了更多工具和生态,比如提供了 Redux DevTools,增强了开发者体验。

如果说 Flux 是开创了状态管理这条先河,那么 Redux 就是把这条路发扬光大的继承者。

我来写一段 Redux 的计数器示例代码来让大家感受一下。

js 复制代码
// 导入 React 和需要的 Hooks
import React, { useCallback } from 'react';
// 导入 Redux 的 Hooks 和用于创建 reducer 和 store 的辅助函数
import { useDispatch, useSelector } from 'react-redux';
import { configureStore, createSlice } from '@reduxjs/toolkit';

// 创建一个 Redux 状态切片,包含状态、reducer 以及相关的操作
const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  // 定义了相关的 action 以及更新 state 的 reducer
  reducers: {
    increment: state => {
      state.value += 1; // 增加计数
    },
    decrement: state => {
      state.value -= 1; // 减少计数
    },
    reset: state => {
      state.value = 0; // 重置计数
    },
  },
});

// 从切片中导出 action 创建函数
const { increment, decrement, reset } = counterSlice.actions;

// 创建 Redux store,其中包含上面定义的计数器切片
const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
  },
});

// 创建我们的组件,它使用 Redux state 和 dispatch
const Counter = () => {
  // 使用 useSelector Hook 来读取 Redux store 中的状态
  const count = useSelector(state => state.counter.value);
  // 使用 useDispatch Hook 来获取 dispatch 函数
  const dispatch = useDispatch();

  // 使用 useCallback 来避免不必要的重新渲染
  const incrementCount = useCallback(() => dispatch(increment()), [dispatch]);
  const decrementCount = useCallback(() => dispatch(decrement()), [dispatch]);
  const resetCount = useCallback(() => dispatch(reset()), [dispatch]);

  return (
    <div>
      <h1>计数器: {count}</h1> {/* 展示当前计数 */}
      <button onClick={incrementCount}>增加</button> {/* 触发增加操作 */}
      <button onClick={decrementCount}>减少</button> {/* 触发减少操作 */}
      <button onClick={resetCount}>重置</button> {/* 触发重置操作 */}
    </div>
  );
};

// 主应用组件,包括 Redux Provider
const App = () => (
  <Provider store={store}>
    <Counter />
  </Provider>
);

export default App;

可以看到,Redux 是和 React 无关的,可以运行在任何 JavaScript 环境中,换言之,Redux 可以和任何前端框架一起使用,这点是非常了不起的。不过从代码中可以看出,模板化也挺严重,为了更好的和 React 一起使用,同时减少模板代码,Redux 官方还有一个叫做 Redux Tookit 的项目,简称 RTK。RTK 的作用就是简化 Redux 的开发和维护。

接着我们来模拟实现一下 Redux 的基本 API。

js 复制代码
// createStore 创建一个 Redux store 来保存 state,并允许读取 state、派发 actions 和订阅变化
function createStore(reducer, initialState) {
  let state = initialState;
  let listeners = [];

  // getState 用于获取当前的 state
  const getState = () => state;

  // dispatch 用于派发一个 action,这是触发 state 变化的唯一途径
  const dispatch = action => {
    // 使用传入的 reducer 函数来计算新的 state
    state = reducer(state, action);
    // 执行所有的监听函数,通知它们 state 已经变化了
    listeners.forEach(listener => listener());
  };

  // subscribe 用于注册监听函数,每当 dispatch 一个 action 时都会被调用
  const subscribe = listener => {
    listeners.push(listener);
    // 返回一个取消订阅的函数
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  };

  // 初始化 state
  dispatch({ type: '@@redux/INIT' });

  return { getState, dispatch, subscribe };
}

// combineReducers 用于组合多个 reducer 成一个单一的 reducer 函数
function combineReducers(reducers) {
  return (state = {}, action) => {
    return Object.keys(reducers).reduce((nextState, key) => {
      // 调用每个 reducer,并将其结果聚合成一个新的 state 对象
      nextState[key] = reducers[key](state[key], action);
      return nextState;
    }, {});
  };
}

// 使用示例
// 定义 action 类型
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// 创建两个 reducer
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case INCREMENT:
      return { value: state.value + 1 };
    case DECREMENT:
      return { value: state.value - 1 };
    default:
      return state;
  }
}

function infoReducer(state = { name: 'Redux' }, action) {
  switch (action.type) {
    // 可能的信息更新动作
    default:
      return state;
  }
}

// 通过 combineReducers 合并 reducer
const rootReducer = combineReducers({
  counter: counterReducer,
  info: infoReducer
});

// 创建 store
const store = createStore(rootReducer);

// 注册监听器
store.subscribe(() => console.log(store.getState()));

// 派发 actions
store.dispatch({ type: INCREMENT });
store.dispatch({ type: DECREMENT });

总结一下 Redux 的核心原理:

  1. 单一数据源 Single Source of Truth:整个应用状态存储在一棵对象树中,这棵树只存在于单个 store 中。这样做的有点就是状态更加可以预测,方便监控和调试。
  2. 状态只读 State is Read-Only:状态只读意味着唯一改变状态的方法是触发一个 Action,Action 是一个描述发生了什么的普通对象。我们无法直接修改状态对象,而是必须提交一个请求,表明自己在做什么。这样做的好处也是确保状态变更是可以被跟踪和集中处理的,同时还避免了不同部分的代码改变状态时产生冲突。
  3. 纯函数修改状态 Changes are Made with Pure Functions:改变状态的逻辑是 reducer 决定的。reducer 是一个纯函数。这么设计的原因还是让行为可预测,因为每次调用,只要是相同的输入(指前一个状态和 action),总能产生相同的输出(新的状态),这样也是有利于测试和服务端渲染的。

Redux 设计思路清晰,在当时也非常火热。那为什么在 Redux 之后还是出现了那么多同类框架呢?因为 Redux 同时存在一些显而易见的问题:

  1. 学习曲线高。可以看到 Redux 中确实有很多概念需要学习,比如 actions、reducers、store、middleware 等。
  2. 样板代码多。而且一点小的改动,可能需要在多个文件中进行对应的修改。
  3. 性能一般。对于大型项目来说,每次更新都需要通过中央的 reducer,粒度太粗。这可能会成为性能瓶颈。
  4. 对 Hooks 支持一般。Redux 出现的时间很早,比 Hooks 更早。后面是依靠 RTK 来对 Hooks 进行支持的。
  5. 单向数据流。并不是所有人都认为单向数据流是最好的做法,基于代理的方式同样可以做这件事。
  6. 框架生态的多样化。

相关信息梳理归纳:

  • 官网: redux.js.org/
  • GitHub: github.com/reduxjs/red...
  • GitHub Star:60.1k
  • 作者:Facebook 研发团队
  • 主要成就:普及了单向数据流架构思想,在前端社区中影响深远。同时发明了 DevTools、中间件等工具和概念。为后面的 MobX、NgRx、Vuex 等状态管理库提供了思路。

响应式编程系鼻祖 - MobX

在 Redux 大行其道的年代,还有另外一个有力的竞争者,那就是 MobX。

MobX 的作者是 Michel Weststrate。Michel 是一个拥有十多年工作经验,精通多门语言的全栈工程师。他认为 Flux 架构过于繁琐,应该研究一种更加简单的方式来管理数据。于是就有了 MobX。

Mobx 和 Redux、Flux 这类遵循单向数据流的方案不同,MobX 的核心理念是通过类似响应式编程的方式来管理数据流。这种方案会更加自然。MobX 中的概念并不算多,通过使用可观察的值(Observable),自动追踪状态变化,然后通过反应机制(Reaction)自动更新视图,减少了手动维护状态和视图同步需要的样板代码,降低了状态管理的复杂性。

我写一段用 MobX 实现的计数器代码,你感受一下。

jsx 复制代码
import React from 'react';
import { observable, action, makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react';

// 创建一个计数器的 store
class CounterStore {
  // 使用 observable 装饰器标记 count 为可观察的状态
  count = 0;

  constructor() {
    // 使用 makeAutoObservable 包装这个类,让 count 成为响应式状态
    // 并让类中的方法拥有正确的 action 绑定
    makeAutoObservable(this);
  }

  // 使用 action 装饰器标记 increment 为一个动作,用于修改状态
  increment = () => {
    this.count += 1;
  }

  // 使用 action 装饰器标记 decrement 为一个动作,用于修改状态
  decrement = () => {
    this.count -= 1;
  }

  // 使用 action 装饰器标记 reset 为一个动作,用于重置状态
  reset = () => {
    this.count = 0;
  }
}

// 创建 store 实例
const counterStore = new CounterStore();

// 创建一个观察者组件,它被 observer 高阶组件包装以响应状态变化
const Counter = observer(() => {
  return (
    <div>
      <h1>计数器: {counterStore.count}</h1>
      {/* 当按钮被点击时,调用 store 中对应的动作函数修改状态 */}
      <button onClick={counterStore.increment}>增加</button>
      <button onClick={counterStore.decrement}>减少</button>
      <button onClick={counterStore.reset}>重置</button>
    </div>
  );
});

// 渲染计数器组件
const App = () => {
  return (
    <div>
      <Counter />
    </div>
  );
}

export default App;

可以看到代码量确实会比 Redux 少很多。

我再来模拟一下 MobX 的几个核心 API,来感受下 MobX 内部具体是怎么实现的。

js 复制代码
// 一个简单的依赖管理器,用于记录观察者和被观察的值的关系
class Dep {
  constructor() {
    this.observers = new Set();
  }

  // 收集依赖,即将观察者添加到当前的依赖集合中
  depend() {
    if (currentAutorun) {
      this.observers.add(currentAutorun);
    }
  }

  // 通知改变,即执行所有收集的依赖
  notify() {
    this.observers.forEach((observer) => observer());
  }
}

// 当前正在执行的 autorun 函数
let currentAutorun = null;

// observable 函数接收对象,使其属性变得可观察
function observable(obj) {
  Object.keys(obj).forEach((key) => {
    const dep = new Dep();
    let value = obj[key];

    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        dep.depend(); // 当获取值的时候,收集依赖
        return value;
      },
      set(newValue) {
        if (newValue !== value) {
          value = newValue;
          dep.notify(); // 当值改变的时候,触发更新
        }
      },
    });
  });
}

// autorun 函数接收一个函数,当依赖变化时重新执行
function autorun(runner) {
  function wrappedRunner() {
    currentAutorun = wrappedRunner;
    runner();
    currentAutorun = null;
  }

  wrappedRunner();
}

// action 函数模拟,用来封装状态修改的逻辑
function action(fn) {
  return function (...args) {
    fn(...args);
  };
}

// 示例使用
const state = observable({
  count: 0,
});

autorun(() => {
  console.log(`Count is: ${state.count}`); // 当 count 改变时,这个函数会被重新调用
});

const incrementCount = action(() => {
  state.count += 1;
});

incrementCount(); // Count is: 1
incrementCount(); // Count is: 2

// 以上代码会在控制台打印:
// Count is: 0
// Count is: 1
// Count is: 2

MobX 的核心技术原理很简单,无非就是围绕着响应式编程和观察者模式进行构建。在 API 层面,它抽象出了可观察数据(Observables)、状态变更的计算值(Computed Values)和响应式副作用(Reactions)。让开发者只需要对数据和计算进行简单的声明性标记,就可以获得自动响应式数据流和 UI 更新的能力,大大减少了样板代码。

MobX 的问题同样是学习曲线高,响应式编程和单向数据流相比同样有一些概念需要理解,不过个人感觉比 Redux 更容易上手一些。另外一点,和单向数据流相比,MobX 数据的流动性并不会那么明确和显式。

相关信息梳理归纳:

  • 官网: mobx.js.org/README.html
  • GitHub: github.com/mobxjs/mobx
  • GitHub Star:26.8k
  • 作者:Michel Weststrate
  • 主要成就:在 Flux 架构大行其道的时代,开创了另外一种做状态管理的思路,让大家看到了:「原来状态管理还可以这么做!」,为后面的诸多同类竞品提供了思路。

单向数据流系国货之光 - Dva

时间来到 2016,前端进入白热化阶段,逐渐被业界认可,演变为一种具体的职业。同时前端三大框架也开始展露头脚。React 一度统治中国前端市场。

当时阿里巴巴是 React 的忠实用户,为了提高研发效率,决定基于 React 自研一套前端框架。那时候 React 的技术栈还没有现在这么五花八门,标准的三件套:Redux、Redux-Saga、React-router。虽然技术栈简单,但是缺少好用的脚手架工具,很多时候都需要自己去动手集成和整合,当时创建一个 React 需要大量的配置,比如 Babel、Webpack、ESLint 等等,这会消耗很多时间和精力。

阿里巴巴的前端开发者云谦(陈成,SorryCC)在这个背景下,得到了公司的支持,开始研发 Dva。Dva 最早就是做集成的框架,它把 Redux、Redux-Saga、React-router 等框架都集成到一起,可以很方便地进行开发。同时也做出了一些创新,比如提出了 Model 的概念,将 state、reducers、effects 和 subscriptions 都集中到一起,这样组织代码会更加模块化和简洁。

我们可以这样认为,Redux 是基于 Flux 的理念设计的状态管理库,而 Dva 则是基于 Redux 设计的更高层次的框架。

下面我写一段简单的 Demo,来看一下 Dva 的数据管理是怎么做的。

js 复制代码
export default {
  namespace: 'counter',  // 模型的命名空间,区分多个模型
  state: {
    count: 0,           // 初始状态,计数器的值
  },
  reducers: {
    // Reducers 处理同步操作,用于修改状态
    add(state, { payload }) {
      return { ...state, count: state.count + payload };  // 增加计数
    },
    minus(state, { payload }) {
      return { ...state, count: state.count - payload };  // 减少计数
    },
  },
  effects: {
    // Effects 处理异步操作和业务逻辑,还可以触发其他 action
    // `put` 用于触发 action,`call` 用于调用异步过程,`select` 用于从 state 里获取数据
    *asyncAdd({ payload }, { put, call }) {
      // 模拟异步操作
      yield call(delay, 1000); // 延迟1s
      yield put({ type: 'add', payload });  // 调用 reducer 来增加计数
    },
    *asyncMinus({ payload }, { put, call }) {
      // 模拟异步操作
      yield call(delay, 1000); // 延迟1s
      yield put({ type: 'minus', payload });  // 调用 reducer 来减少计数
    },
  },
  subscriptions: {
    // Subscriptions 用于订阅数据源,并根据需要分发相应的 action
    // 这里就不包含具体的实现,仅作展示用
    setup({ dispatch, history }) {
      // 监听 history 变化,当进入 `/` 时触发 `asyncAdd` effect
      history.listen(({ pathname }) => {
        if (pathname === '/') {
          dispatch({ type: 'asyncAdd', payload: 1 });
        }
      });
    },
  },
};

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

可以看到 Dva 的模式会使一步流程更加清晰和易于管理。

我们来模拟实现一下基础的 Dva,感受一下它的 API 设计。

js 复制代码
class SimpleDva {
  constructor() {
    this._models = {}; // 存储所有 model 的对象
    this._effects = {}; // 存储所有 effects 的对象
    this._reducers = {}; // 存储所有 reducers 的对象
    this._state = {}; // 维护全局的 state 对象
  }

  model(model) {
    // 注册 model
    const { namespace, state, reducers, effects } = model;
    this._models[namespace] = model;
    this._state[namespace] = state;
    if (reducers) {
      Object.keys(reducers).forEach(reducerKey => {
        const type = `${namespace}/${reducerKey}`;
        this._reducers[type] = reducers[reducerKey];
      });
    }
    if (effects) {
      Object.keys(effects).forEach(effectKey => {
        const type = `${namespace}/${effectKey}`;
        this._effects[type] = effects[effectKey];
      });
    }
  }

  dispatch(action) {
    // 分发 action
    const type = action.type;
    const namespace = type.split('/')[0];
    if (this._reducers[type]) {
      // 执行 reducer 函数,更新对应的 state
      this._state[namespace] = this._reducers[type](this._state[namespace], action);
    } else if (this._effects[type]) {
      // 执行 effect 函数
      const effect = this._effects[type];
      effect(action, {
        put: (actionInEffect) => {
          this.dispatch(actionInEffect); // 在 effect 中可以通过 put 再次分发 action
        },
        call: (fn, ...args) => {
          // call 函数用于调用异步函数,并等待其解决
          return fn.apply(null, args);
        },
        select: (selector) => {
          // select 函数用于选择 state 中的某个部分
          return selector(this._state);
        },
      });
    } else {
      console.warn(`Action type ${type} is not a valid effect or reducer`);
    }
  }

  getState() {
    // 获取当前的 state
    return this._state;
  }
}

// 模拟的使用示例:

// 创建一个 simpleDva 实例
const dvaApp = new SimpleDva();

// 定义 model
const counterModel = {
  namespace: 'counter',
  state: { count: 0 },
  reducers: {
    add(state, { payload }) {
      return { ...state, count: state.count + payload };
    }
  },
  effects: {
    * asyncAdd({ payload }, { put, call }) {
      yield call(delay, 1000);
      yield put({ type: 'counter/add', payload });
    }
  }
};

// 注册 model
dvaApp.model(counterModel);

// 分发 action
dvaApp.dispatch({ type: 'counter/asyncAdd', payload: 1 });

// 延迟函数(用于模拟异步操作)
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 获取当前状态
console.log(dvaApp.getState()); // 应该打印出 { counter: { count: 0 } }

Dva 在数据管理这方面主要的核心原理有以下两点:

  1. 基于现有框架进行集成。数据流管理使用 Redux、副作用管理使用 Redux-saga。
  2. 对已有的数据模型进行抽象,提出了 Model 概念。

Dva 在国内受到了很多前端开发人员的喜爱,但是我认为 Dva 也有两大缺点。

第一是继承了 Redux 的高学习曲线。对新手不太友好,而精通 Redux 的开发者又未必会选择 Dva。为什么不选择呢?这就是它的第二大缺点了。

第二个缺点就是过度封装的问题。React 本身的特性一直在持续演进,比如 Context API、Hooks 等特性的出现,像 Redux 和 Dva 这类独立于 React 之外的数据管理方案就显得没那么完美契合。同时 Dva 的封装也可能让一些 Redux 生态中优秀的工具难以整合到框架中。

相关信息梳理归纳:

  • 官网: dvajs.com/
  • GitHub: github.com/dvajs/dva
  • GitHub Star:16.2k
  • 作者:蚂蚁集团前端团队、云谦
  • 主要成就: 在中国 React 开发者社区中普及了单向数据流架构思想,一定程度上带动了中国本土前端领域的开源文化。

状态机系鼻祖 - XState

在单向数据流派系一领风骚,响应式编程崭露头角的时代,还有一个独树一帜的库,那就是 XState。

XState 的作者是 David Khourshid,一个常年活跃在开源社区的工程师。David 认为,前端应用的状态复杂性,并不是 React、Angular 这些框架独有的问题,而是整个前端整体的问题。重点并不仅仅是视图层的渲染和组件的结构,而是应用程序中的隐含状态和逻辑问题。甚至于说,这种状态转换本质上不区分前后端。所以 XState 还有一个 Python 的版本,但是没多少人用。

回到状态管理上来,程序的本质是什么呢?David 认为,无非就是状态的转换而已。这里的状态你可以理解成变量。程序其实就是把变量转来转去。因为变量根据逻辑的不同,而被分散到程序的各个部分,没有统一进行管理,所以这种转换很难被清晰地把握,最后导致维护状态逻辑变得很困难。

其实在计算机科学里面,有一种非常成熟的概念可以解决这个问题,那就是状态机。David 没有像 Facebook 的工程师那样自研架构,而是直接使用状态机作为基础理论导向,然后设计出 XState 这个库。

下面我来演示一下 XState 的计数器 Demo。

jsx 复制代码
import React from 'react';
import { createMachine, assign } from 'xstate';
import { useMachine } from '@xstate/react';

// 创建一个XState状态机
const counterMachine = createMachine({
  id: 'counter',
  initial: 'active',
  context: {
    count: 0 // 初始计数值
  },
  states: {
    active: {
      on: {
        INCREMENT: {
          // 增加状态动作
          actions: assign({
            count: (context) => context.count + 1
          })
        },
        DECREMENT: {
          // 减少状态动作
          actions: assign({
            count: (context) => context.count - 1
          })
        },
        RESET: {
          // 重置状态动作
          actions: assign({
            count: 0
          })
        }
      }
    }
  }
});

const Counter = () => {
  // 使用XState React Hooks来运行状态机
  const [state, send] = useMachine(counterMachine);

  return (
    <div>
      <h2>计数器: {state.context.count}</h2>
      <button onClick={() => send('INCREMENT')}>增加</button> {/* 触发增加动作 */}
      <button onClick={() => send('DECREMENT')}>减少</button> {/* 触发减少动作 */}
      <button onClick={() => send('RESET')}>重置</button>     {/* 触发重置动作 */}
    </div>
  );
};

export default Counter;

XState 在 npm 上面有很多包,虽然它可以单独运行,但是为了迎合广大前端开发者的实际需求,对 React、Angular 等框架进行了适配,提供了贴合框架的高级 API。所以我们在上面的 Demo 中用到了 @xstate/react 这个包。

接下来我来模拟实现 XState 中的几个基本 API:Machine、interpret 和 assign。目标是创建一个简单的状态机,具备处理状态更新和转换的能力。

js 复制代码
// Machine 状态机构造器
class Machine {
  constructor(config) {
    this.initialState = { value: config.initial, context: config.context };
    this.states = config.states;
    this.context = config.context;
    this.state = this.initialState;
  }

  transition(currentState, event) {
    const currentStateConfig = this.states[currentState.value];
    const eventConfig = currentStateConfig.on[event];
    
    if (!eventConfig) {
      // 如果没有对应的事件处理,返回当前状态
      return currentState;
    }
    
    const nextStateValue = eventConfig.target;
    const nextState = {
      value: nextStateValue,
      context: this.context // 保留上下文
    };

    // 如果有 action,我们将执行它来更新上下文
    if (eventConfig.actions) {
      this.context = eventConfig.actions(this.context);
      nextState.context = this.context;
    }

    return nextState; // 返回新状态
  }
}

// interpret 解释器,启动和执行状态机
function interpret(machine) {
  let currentState = machine.initialState;

  return {
    send(event) {
      currentState = machine.transition(currentState, event);
      console.log(`状态转换到: ${currentState.value}`);
      // 输出上下文变化
      console.log('当前上下文:', currentState.context);
    },
    getState() {
      return currentState;
    }
  };
}

// assign 更新上下文的助手函数
function assign(updater) {
  return (context) => updater(context);
}

// 使用例子
const toggleMachine = new Machine({
  initial: 'inactive',
  context: { count: 0 },
  states: {
    inactive: {
      on: {
        TOGGLE: {
          target: 'active',
          actions: assign(context => ({ count: context.count + 1 }))
        }
      }
    },
    active: {
      on: {
        TOGGLE: {
          target: 'inactive',
          actions: assign(context => ({ count: context.count + 1 }))
        }
      }
    }
  }
});

const toggleService = interpret(toggleMachine);

console.log('Initial state:', toggleService.getState());

toggleService.send('TOGGLE');
toggleService.send('TOGGLE');

上面的代码中包含了 XState 核心概念 transition、interpret 和 assign,你可以仔细阅读代码来理解 XState 是如何进行状态配置、事件处理和通过状态转换更新上下文的。我个人感觉 XState 会比 Redux 和 MobX 更加复杂。其中并行状态、守卫条件、延迟事件等概念也非常重要,篇幅所限我没有模拟出完整的实现,如果感兴趣你可以自己尝试实现剩下的几个重要概念。

XState 的核心原理是建立在有限状态机(FSM)和状态图(Statecharts)之上的,我再对这两个概念做一个简短的介绍。

  • 有限状态机 FSM:就是一个计算的数学模型。系统中有很多状态,但是状态的总量是有限的。在任一时刻,系统都处于其中一个状态。它可以根据某个特定的事件跳转到另一个状态。这些状态和状态的转换定义了系统的全部行为。FSM 的关键点在于系统在任一一个时刻都清晰地知道自己处于什么状态。
  • 状态图 Statecharts:状态图是 FSM 的一种扩展,它在 FSM 的基础上引入了层级、并行状态和历史状态等概念,可以允许更复杂的行为表达。状态图的作用是增加了 FSM 的表达力,可以更容易地应对实际程序开发中遇到的复杂逻辑。

XState 的缺点,第一点是学习曲线更高,我个人感受是精通 XState 的难度比 Redux 和 MobX 都要高出不少。第二点是调试复杂度也很高,虽然 XState 有提供可视化工具,但是很多情况下不如 Redux-Devtool 这类工具那么容易调试。第三点就是应用场景很少,XState 是为了应对复杂应用而生的,大部分普通项目用它,就是杀鸡用牛刀,大炮打麻雀,完全是过度设计。所以要结合实际的项目情况来选择,简单的项目用 XState 是一种折磨,但超级复杂的项目用 XState 会非常舒服,个人建议是慎用。

相关信息梳理归纳:

Redux 简化版 - Rematch

Redux 的冗余和复杂是有目共睹的。阿里巴巴研发了 Dva,国外同样也有一些开发者在做类似的事情。Rematch 就是其中一个。

和 Dva 的思路不同,Rematch 聚焦于一件事,那就是「简化 Redux」。它几乎没有提供新的 API,也没有阉割 Redux 的核心功能。它做的事主要有以下几点:

  • 不再需要编写 action types 和 action creators 了。直接定义修改状态的函数,Rematch 会自动地生成相应的 actions。
  • 改良了 reducers 的声明方式。用一个对象来定义 reducers,而不是繁琐的 switch 语句。
  • 内置了异步处理。Rematch 中增加了 effects 的概念,不需要额外的中间件就可以处理异步逻辑和副作用。

空口无凭,我直接用 Rematch 写一段计数器的 Demo,你来感受一下和 Redux 的区别。

jsx 复制代码
import React from 'react';
import { init } from '@rematch/core';
import { Provider, useSelector, useDispatch } from 'react-redux';

// 定义计数器模型
const countModel = {
  state: 0, // 初始状态
  reducers: {
    // 定义处理函数用于修改状态
    increment: (state, payload) => state + payload,
    decrement: (state, payload) => state - payload,
    reset: () => 0,
  },
};

// 初始化 Rematch store 并注册模型
const store = init({
  models: {
    count: countModel,
  },
});

// 计数器组件
const Counter = () => {
  const count = useSelector((state) => state.count); // 使用 useSelector Hook 获取状态
  const dispatch = useDispatch(); // 使用 useDispatch Hook 获取 dispatch 方法

  return (
    <div>
      <h2>计数器: {count}</h2>
      <button onClick={() => dispatch.count.increment(1)}>+</button> {/* 调用增加动作 */}
      <button onClick={() => dispatch.count.decrement(1)}>-</button> {/* 调用减少动作 */}
      <button onClick={() => dispatch.count.reset()}>重置</button> {/* 调用重置动作 */}
    </div>
  );
};

// 根组件,使用 Provider 包裹 App 组件以共享 Redux store
const App = () => (
  <Provider store={store}>
    <Counter />
  </Provider>
);

export default App;

可以看到代码的繁琐程度确实大大减轻了。

总的来说,Rematch 没什么明显缺点。在学习曲线上比 Redux 更低,所以如果你认可 Flux 架构,喜欢用 Redux,那么直接换到 Rematch 上面可能是一个更好的选择。

相关信息梳理归纳:

返璞归真的先行者 - Unstated

时间来到 2018 年,虽然有了 RTK 这种官方的解决方案,也有了像 Dva、Rematch 这类社区推出的简化方案。不满 Redux 的人依然大有人在。Jamie Kyle 就是其中一位。

他认为状态管理复杂,主要就是由于跨组件通信时 props 的钻孔问题导致的。如果我们有一种方式能够解决跨组件共享状态,就可以在保持简洁 API 的同时拥有足够的能力管理复杂应用的状态。它的目的是:在 React 中共享状态和逻辑的最小 API。

Unstated 仅仅提供了三个 API。

  • Container,一个容器,用来存储状态和更新逻辑。
  • Subscribe,一个组件,可以传递容器到组件树中,这样组件树就可以使用容器中的状态和变更状态。
  • Provider,一个组件,在最外层进行注入容器。

我直接把它官方计数器的 Demo 搬过来了。

jsx 复制代码
// @flow
import React from 'react';
import { render } from 'react-dom';
import { Provider, Subscribe, Container } from 'unstated';

type CounterState = {
  count: number
};

class CounterContainer extends Container<CounterState> {
  state = {
    count: 0
  };

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
}

function Counter() {
  return (
    <Subscribe to={[CounterContainer]}>
      {counter => (
        <div>
          <button onClick={() => counter.decrement()}>-</button>
          <span>{counter.state.count}</span>
          <button onClick={() => counter.increment()}>+</button>
        </div>
      )}
    </Subscribe>
  );
}

render(
  <Provider>
    <Counter />
  </Provider>,
  document.getElementById('root')
);

看这个 API,真的超级简洁。在代码结构上,几乎和如今 Context API 一摸一样。你可能会问了?既然和 Context API 这么像?为什么不直接用 Context 呢?要知道 Unstated 流行的时候,Context 还在 React 的 RFC 里放着呢。Unstated 已经早 React 一步推出了这种模式。

很遗憾,Unstated 这个项目仅仅存活了一年左右,随着 React 官方推出了 useState、useContext 和 useReducer 这些 Hooks API 后,Unstated 就没有继续更新了,它在 GitHub 上的最后一次提交停留在了 2019 年 5 月 7 日。不过 Unstated 的作者在 2020 年又开发了一个新的库:unstated-next。主打"更小"的解决方案。它的代码体积只有 200b。Jamie 认为,Unstated 的竞争对手不是 React,而是 Redux。他希望只需要 React 自身的 API 就能做到这一点,鼓励大家放弃 Redux。后来 React 真的做到了,Jamie 也停止项目的更新,鼓励大家投入 React 的怀抱。

相关信息梳理归纳:

Angular 生态下的 Redux - Akita

虽然在 2018 年,前端最鼎盛的时期,状态管理库百花齐放、争奇斗艳。但论谁最有影响力,毫无疑问仍然是 Redux。

尽管 Redux 不受框架的限制,但归根结底 Redux 是 React 社区的产物。更适合 React 这个库。如果你在 Angular 或者 Vue 这些框架中使用 Redux,总觉得有些怪怪的。

当你觉得怪怪的时候,就表示该造一个新的轮子了。

所以 Angular 生态中出现了 Akita。Akita 就是秋田犬的意思。Akita 的作者是 Netanel Basal,一位活跃的开源软件贡献者,在前端,特别是 Angular 社区有非常多的贡献。他认为 Flux 架构非常适合管理前端状态。如果能够和 Angular 生态中的响应式编程库 Rxjs 进行结合,就能打造一个适合 Angular 框架的状态管理库。于是有了 Akita。这里额外提一句,虽然刚开始 Akita 的目标是打造 Angular 中的 Redux,但是随着项目的发展,Akita 现在也支持各种框架。

Akita 有以下几个主要的概念:

  • 存储 Stores:类似于 Redux 的单一状态树的概念。
  • 实体 Entities:特殊的实体 store,每个实体管理一个实体类型的集合,比如用户、商品等。
  • 服务 Services:服务通常负责与后端通信并把结果分派到对应的 store。
  • RxJS 集成:RxJS 是 Akita 的核心部分,Stores 和 Queries 都利用了 RxJS 的 Observable。
  • 数据不变性 Immutability:和 Redux 的概念保持一致。

现在我同样用 Akita 来写一个计数器的 Demo,因为 Akita 并不是为了 React 单独设计的,所以这里的代码是使用 Angular 框架。

首先是 Store 部分。

ts 复制代码
// counter.store.ts
// Store 是 Akita 中用来存储状态的主要单元。这里创建了一个简单的 CounterStore 来保存我们的计数器状态。
import { Injectable } from '@angular/core';
import { Store, StoreConfig } from '@datorama/akita';

// 定义计数器状态的接口
export interface CounterState {
  count: number;
}

// 设置初始状态
export function createInitialState(): CounterState {
  return {
    count: 0
  };
}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'counter' })
export class CounterStore extends Store<CounterState> {
  constructor() {
    super(createInitialState());
  }
}

然后是 UI 部分。

ts 复制代码
// counter.component.ts
// 这个 Angular 组件提供了用户界面,允许用户通过点击按钮来增加、减少或重置计数器。
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { CounterQuery } from './counter.query';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      Count: {{ count$ | async }}
      <button (click)="increment()">Increment</button>
      <button (click)="decrement()">Decrement</button>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class CounterComponent {
  count$: Observable<number>;

  constructor(
    private counterQuery: CounterQuery,
    private counterService: CounterService
  ) {
    this.count$ = this.counterQuery.count$; // 订阅 count 的变化
  }

  increment() {
    this.counterService.increment();
  }

  decrement() {
    this.counterService.decrement();
  }

  reset() {
    this.counterService.reset();
  }
}

虽然 Akita 成功把 RxJS 和 Redux 进行了有效结合。但是在 Angular 生态里面并没有得到广泛的认可。在生态上反而被 Akita 的有力竞争对手 NgRx 压制。

Akita 的缺点是生态一般,虽然 Redux 有的东西 Akita 基本上也都有,像 DevTools。但 Angular 社区的人就是不怎么买单。所以有些情况下,我们不能很快找到 Akita 的插件和中间件,需要自己去写很多代码。

相关信息梳理归纳:

响应式编程与函数式编程的结合 - Effector

Effector 出现的时候,前端已经有一大批优秀的状态管理解决方案了。比如 Redux、MobX 和 Vuex 等。但是这些方案都有一些问题被大家反复诟病。主要的问题如下:

  1. 学习曲线高,一大堆概念。
  2. 代码模板多,维护困难。
  3. 性能一般般,在大型项目中很多状态管理库都成为了性能瓶颈。

同时响应式编程和函数式编程两大编程范式在前端领域迅速崛起,开发者开始追求更加声明式和函数式的方式来处理状态和副作用。

在这种大趋势下,俄罗斯程序员 ZeroBias 创建了 Effector 项目。他的愿景是设计一个灵活、强大的状态管理库,同时要保持简洁的 API,让前端开发者可以轻松地管理状态和副作用。简单点说,就是想保持高效的开发效率,同时保持高度的可维护性。

Effector 中主要的概念有如下几点:

  • 存储 Stores:存储状态的核心单元。它是可观察和响应式的,在状态变化时,可以自动更新 UI。一个应用中可以存在多个 Store。
  • 事件 Events:用来表示动作和数据变更的核心概念。通过触发 event 表示发生了某个行为,然后执行和它相关的状态更新和副作用。
  • 副作用 Effects:用来处理异步操作和副作用。一个 effect 表示一个异步函数。effect 提供了一个统一的接口来处理数据请求等异步任务,简化了异步的复杂性。
  • 命名空间 Domain:用来组织相关的 stores、events 和 effects。在大型项目中非常有用,可以在逻辑上进行隔离和复用。
  • 计算值 Computed Values:计算值是依赖一个或者多个 stores 的值,它们是动态计算的,会随着依赖的状态的变化而自动更新。
  • 订阅 Subscriptions:一套订阅机制,可以让组件或者响应式的逻辑监听 stoer 的变更事件,在状态变化时自动执行回调。
  • 响应式 Reactivity:Effector 是基于流的响应式系统,这样能保证状态的更新是自动并且连贯的。在 Effector 中,我们可以把状态视为一个随着时间变化的数据流。这和 RxJS 有异曲同工之妙。

我用 Effector 写一个计数器的 Demo 大家感受下。

jsx 复制代码
// 导入 React 和 Effector 相关的函数和钩子
import React from 'react';
import { createEvent, createStore } from 'effector';
import { useStore } from 'effector-react';

// 创建事件,事件用于触发状态更新
// 增加事件
const increment = createEvent();
// 减少事件
const decrement = createEvent();
// 重置事件
const reset = createEvent();

// 创建状态存储(store),初始值为 0
const $counter = createStore(0)
  // 在状态上绑定事件,响应相应的状态变化
  .on(increment, (state) => state + 1) // 增加时,状态加 1
  .on(decrement, (state) => state - 1) // 减少时,状态减 1
  .reset(reset); // 重置时,恢复到初始值

// 计数器组件
const Counter = () => {
  // 使用 useStore 钩子从 Effector store 中获取状态
  const count = useStore($counter);

  // 渲染组件
  return (
    <div>
      {/* 显示当前计数 */}
      <h1>计数器: {count}</h1>
      {/* 按钮用于触发增加事件 */}
      <button onClick={() => increment()}>增加</button>
      {/* 按钮用于触发减少事件 */}
      <button onClick={() => decrement()}>减少</button>
      {/* 按钮用于触发重置事件 */}
      <button onClick={() => reset()}>重置</button>
    </div>
  );
};

export default Counter;

可以看到 Effector 的代码确实比 Redux 更加简洁直观,状态变化的逻辑也清晰集中,这样即使程序变得复杂了,也能很好地进行维护。

前端状态管理库发展到 Effector 这个阶段,其实已经趋近于完美了。同类的竟品在 API 的设计上同质化非常严重。以至于接下来介绍的几个库都有彼此的影子。而不同的是各自的设计理念。

不少 Effector 同时期的项目都死掉了,而 Effector 至今仍然积极维护,并且适配了 React、Vue 等多个框架,说明还是能够很好地迎合市场的。

如果你喜欢通过流的方式管理状态,那么 Effector 是一个不错的选择。

相关信息梳理归纳:

Redux 抽象版 - Easy Peasy

时间来到 2019 年,状态管理库的战争已经进入到白热化阶段,不过主题仍然没有什么变化,还是在对 Redux 这位老大哥进行口诛笔伐。

印度程序员 Sean Matheson 开源了 Easy Peasy 项目。它和 Rematch 有些类似,在保留 Redux 的核心优势的前提下,仅可能降低开发门槛。Easy Peasy 的主要特性有:

  • 简化 Action 和 Reducer 的定义。只需要定义好模型和相关逻辑,不需要创建 Action Type 和 Action Creators 了。是不是和 Rematch 很像?
  • 内置了异步逻辑的支持,通过 async/await 处理异步操作,不再依赖中间件。是不是和 Rematch 很像?
  • 提供了更加直观和自然的声明式 API,描述业务逻辑更轻松。是不是和 Rematch 很像?
  • 不需要担心不可变性。在 Easy Peasy 里面,我们可以直接修改状态,不需要关心不可变性。Easy Peasy 为了实现这一点,底层用了类似 immer 的库。

我们来看下 Easy Peasy 编写计数器应用的代码。

jsx 复制代码
import React from 'react';
import { StoreProvider, createStore, action, useStoreState, useStoreActions } from 'easy-peasy';

// 计数器模型,定义了状态和更新状态的 actions
const counterModel = {
  // 定义了状态变量 count,并给它初始值 0
  count: 0,
  // increment action,用于增加 count 的值
  increment: action((state) => {
    state.count += 1;
  }),
  // decrement action,用于减少 count 的值
  decrement: action((state) => {
    state.count -= 1;
  }),
  // reset action,用于重置 count 为初始值 0
  reset: action((state) => {
    state.count = 0;
  }),
};

// 创建一个 easy peasy 的 store,包含 counterModel
const store = createStore(counterModel);

// 计数器组件
const Counter = () => {
  // 使用 useStoreState 钩子函数从 store 中获取 count 状态
  const count = useStoreState((state) => state.count);
  
  // 使用 useStoreActions 钩子函数获取操作 count 的 actions
  const { increment, decrement, reset } = useStoreActions((actions) => ({
    increment: actions.increment,
    decrement: actions.decrement,
    reset: actions.reset,
  }));

  return (
    <div>
      <h1>计数器 Demo</h1>
      <p>当前计数: {count}</p>
      <button onClick={increment}>增加</button>
      <button onClick={decrement}>减少</button>
      <button onClick={reset}>重置</button>
    </div>
  );
};

// 应用组件,它将 Counter 组件包裹在 StoreProvider 组件中
const App = () => (
  <StoreProvider store={store}>
    <Counter />
  </StoreProvider>
);

export default App;

连代码结构都和 Rematch 很相似。其实这也不怪 Easy Peasy,毕竟大家都是建立在 Redux 之上的,又为了相同的目标,长得像是必然的事情。

相关信息梳理归纳:

Hooks 时代降临 - Zustand

2019 年的 React 已经有了 Hooks API,通过 Hooks 去做状态管理和处理副作成为首选。这是一个大趋势,聪明人都看得出来,继续去卷封装 Redux 那条路已经没有什么前途了。

著名的开源组织 Poimandres 看准了机会,也加入到造轮子的圈子里。你可能没听说过 Poimandres,但是你可能听说过它们的一些开源项目,比如动画库 React-Spring、Three.js 的 React 渲染器 React-Three-Fiber 等等。由于 Poimanders 有着丰富的开源项目经验,所以成功地创建了 Zustand 这个项目。

Zustand 借鉴了一些 Redux 中的思路,但在具体的设计上截然不同。它主要的几个设计理念如下:

  • 中心化的状态存储,这一点似乎已经成了状态管理库的标配。
  • 围绕 Hooks API 进行设计。从这一点上,已经区分开以 Redux 为代表的上一代产物了。
  • 状态可变性,Zustand 摒弃了状态不可变,可以直接修改状态对象。这也简化了状态更新的操作,同时不再需要频繁地创建新对象,减少性能损耗。
  • 状态更新更容易。只需要一个简单的函数就能更新状态,类似于组件内部的 setState,但是这种变更会作用在全局上。同时也支持异步更新。
  • 可选择的监听器和中间件。虽然 Zustand 非常简单,但是它仍然提供了一些高阶功能,比如状态监听器和中间件。这意味着我们仍然轻松地实现日志、持久化、撤销/重做等高级功能。

我们来看一个用 Zustand 实现的计数器例子。

jsx 复制代码
import React from 'react';
import create from 'zustand';

// 使用zustand创建一个store,它会保存状态并提供修改这些状态的方法
const useCounterStore = create((set) => ({
  count: 0, // 初始状态
  // 定义修改状态的方法
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

const Counter = () => {
  // 使用自定义的useCounterStore钩子来订阅状态变化
  // select参数是一个函数,用来选择性订阅state中的特定部分
  const { count, increment, decrement, reset } = useCounterStore();

  return (
    <div>
      <h1>计数器: {count}</h1>
      <button onClick={increment}>增加</button> {/* 触发增加计数 */}
      <button onClick={decrement}>减少</button> {/* 触发减少计数 */}
      <button onClick={reset}>重置</button>     {/* 触发重置计数 */}
    </div>
  );
};

export default Counter;

可以看出它有很明显的优势。代码极度简洁,没有注入和提供者,只需要一个 Hooks。直接修改状态,不需要返回新对象。状态和逻辑实现了高内聚。状态管理库从 Flux 开始,到 Zustand 止,发展了整整 5 年,终于达到了一个不再需要吐槽的程度了。

Zustand 可谓是非常成功,作为后起之秀,如今却占据了前端状态管理这个领域的半壁江山,更是如今 React 生态中状态管理库的事实标准。这也是我非常推荐大家用的状态管理库。

相关信息梳理归纳:

旧时代的绝唱 - Overmind

按道理说,Zustand 已经趋近于完美了,那么百家争鸣的状态管理库卷了这么多年也该歇歇了吧?然而事情并没有这么简单。一个优秀的程序员要么就是在造轮子,要么就是在造轮子的路上。

Zustand 从诞生,到被认可,最后普及。是需要一个过程的,而在这个过程之中,还有很多优秀的程序员造了很多优秀的轮子。现在我们来介绍一个和 Zustand 同一时期的库:Overmind。

Overmind 这个名字取得霸气,主宰。Overmind 主要有以下几个优点:

  • 简化了模板代码,常规操作。
  • 内置了异步操作,常规操作。
  • TypeScript 高度支持。虽然 Redux 也支持 TypeScript,但是 Overmind 是从头就用 TypeScript 进行开发的。在类型推断和类型安全上都有更多优势,这也算是 Overmind 的一大优势。
  • 派生状态,常规操作。其实就是计算值。
  • 模块化,常规操作。
  • DevTools,常规操作。
  • FRP 特性,FRP 就是函数响应式编程,一种把函数式编程和响应式编程结合的编程范式。

我们用 Overmid 来创建一个计数器 Demo,看一下代码结构怎么样。

首先定义状态和操作:

jsx 复制代码
import { createHook } from 'overmind-react';

export const config = {
  state: {
    count: 0 // 设置初始状态 count
  },
  actions: {
    increment({ state }) {
      state.count += 1; // 定义 increment 操作,每次调用 count 加一
    },
    decrement({ state }) {
      state.count -= 1; // 定义 decrement 操作,每次调用 count 减一
    },
    reset({ state }) {
      state.count = 0; // 定义 reset 操作,将 count 重置为 0
    }
  }
};

export const useOvermind = createHook();

然后在 UI 层使用状态和操作:

jsx 复制代码
import React from 'react';
import { createOvermind } from 'overmind';
import { Provider } from 'overmind-react';
import { config, useOvermind } from './overmind';

// 创建 Overmind 实例
const overmind = createOvermind(config);

// 计数器组件
const Counter = () => {
  // 使用 Overmind Hook 来访问状态和操作
  const { state, actions } = useOvermind();

  return (
    <div>
      <h2>计数器: {state.count}</h2>
      <button onClick={actions.increment}>增加</button>
      <button onClick={actions.decrement}>减少</button>
      <button onClick={actions.reset}>重置</button>
    </div>
  );
};

// 将 Overmind Provider 组件包裹在 App 组件外部,以便在整个应用中使用状态
const App = () => (
  <Provider value={overmind}>
    <div className="App">
      <Counter />
    </div>
  </Provider>
);

export default App;

可以看到它的 API 设计是在从 Redux 时代向 Hooks 进行过渡:用了 Hooks,但还没有完全脱离 Context。而配置对象的写法又像极了 Vue2 的代码。

在我收集的 20 个库中,Overmind 似乎是最后一个还保留着浓厚的 Redux 影子的库,所以我称它为旧时代的绝唱。

相关信息梳理归纳:

Hooks 系先行者 - Hookstate

在 2019 年之后诞生的状态管理库,基本上都会和 Hooks 挂钩。而那个时候能够把 Hooks 视作第一公民,完全围绕 Hooks 设计的库还并不多。Hookstate 从名字上就能看出来,它就是围绕 Hooks 设计 API 的库之一。

Hookstate 的作者是 Andrei,一位拥有 20 多年编程经验的老程序员。

Hookstate 的设计哲学是:

  • 简洁和易用性。
  • Hooks 的全面支持。
  • 强大的性能。
  • 强大的插件系统。
  • 对 TypeScript 友好。

Hookstate 在不牺牲性能的前提下,在轻量级和简洁性之间取得了平衡。我们来看一下它的 API。

jsx 复制代码
import React from 'react';
import { createState, useState } from '@hookstate/core';

// 使用 createState 创建一个全局状态,初始为 0
const counterState = createState(0);

function Counter() {
  // 使用 Hookstate 的 useState 连接计数器的状态
  const counter = useState(counterState);

  // 定义 increment 函数用于增加计数
  const increment = () => counter.set(p => p + 1);
  
  // 定义 decrement 函数用于减少计数
  const decrement = () => counter.set(p => p - 1);
  
  // 定义 reset 函数用于重置计数
  const reset = () => counter.set(0);

  // 渲染计数器 UI
  return (
    <div>
      <h1>计数器 Counter</h1>
      {/* 显示当前计数值 */}
      <p>当前计数 Count: {counter.get()}</p>
      {/* 按钮用于调用增加、减少和重置功能 */}
      <button onClick={increment}>增加 Increment</button>
      <button onClick={decrement}>减少 Decrement</button>
      <button onClick={reset}>重置 Reset</button>
    </div>
  );
}

export default Counter;

可以看到,在代码的简洁性上,基本和 Zustand 不相上下。但是社区活跃度不如 Zustand,所以我还是推荐你选择 Zustand。

相关信息梳理归纳:

Hooks 与 immer 的结合 - Pullstate

2019 年到 2023 年,这 4 年的时间里,前端状态管理库的 API 似乎已经没有太大的变化了,似乎已经演变到了瓶颈。这也导致很多库高度相似。

Pullstate 的设计思路和 Hookstate、Zustand 极为相似,我就不赘述了。它同样是围绕 Hooks 进行设计,独特之处是底层依赖了 immer,可以直接修改对象,而不用创建一个新对象。

我们直接看代码好了。

jsx 复制代码
import React from 'react';
import { Store, useStoreState, useStoreActions } from 'pullstate';

// Step 1: 创建一个 Pullstate Store 来存储我们的状态
const CounterStore = new Store({
  count: 0,
});

// Step 2: 创建操作状态的一些 action
const increment = () =>
  CounterStore.update(s=> {
    s.count += 1; //直接修改状态(Pullstate 会负责不可变性和更新)
  });

const decrement = () =>
  CounterStore.update(s => {
    s.count -= 1;
  });

const reset = () =>
  CounterStore.update(s => {
    s.count = 0;
  });

// Step 3: 创建一个使用拉取状态的React组件
const Counter = () => {
  // 使用 useStoreState,订阅所需的状态部分并响应变化
  const count = useStoreState(CounterStore, s => s.count);

  // Step 4: 使用 useStoreActions 获取更新函数
  const actions = useStoreActions(() => ({
    increment,
    decrement,
    reset,
  }));
  
  return (
    <div>
      <h1>计数器</h1>
      <p>当前计数: {count}</p>
      <button onClick={actions.increment}>增加</button>
      <button onClick={actions.decrement}>减少</button>
      <button onClick={actions.reset}>重置</button>
    </div>
  );
};

export default Counter;

从代码中可以看到,Pullstate 的 API 并不如 Zustand 或者 Hookstate 简洁优雅。初始化状态还需要 new 一个对象,而状态和操作分成了两个 Hooks 也显得没有必要。

总的来说,Pullstate 在设计上不如 Zustand,个人建议使用 Zustand。

相关信息梳理归纳:

中规中矩的大厂之作 - react-sweet-state

前面介绍的几个库都是个人开发者创建的项目,其实这个时间也有一些大公司在这个领域卷。Atlassian 就是其中一个。Atlassian 是澳大利亚一家上市软件公司,以开发工具和写作软件而闻名全球。如果你对 Atlassian 不熟悉,那么他家的产品你应该多少听过或用过。比如 Jira、Confluence 和 Trello 等。

Atlassian 的开发团队在使用 React 的时候同样遇到了状态管理方面的问题。于是其中两位工程师 Luca Bolognese 和 Alberto Gasparin 决定设计一个既能够支持支持功能级和全局状态管理、又能够保持性能和开发简易性的状态管理库。这个库应该具有足够现代化的 API,同时充分利用 React 的新特性 Hooks。于是就有了 react-sweet-state。

react-sweet-state 的主要概念如下:

  • Store:和 Redux 类似,每个 Store 是一个包含了 state、actions 和 reducers 的独立实体。我们可以定义多个 Store,而不像 Redux 那样只能定义一个 Store。这样可以按照功能模块或者逻辑来区分不同的状态存储,更加模块化。
  • Hooks:基于 useState 和 useEffect 进行封装,提供了自定义的 useStore 和 useSweetState 等 Hooks。
  • Context:为了避免 Props 穿孔问题,使用 Context API 为组件提供共享状态的访问。
  • Reducer:类似于 Redux。
  • 中间件:类似于 Redux。
  • 订阅机制:通过订阅机制,可以提升性能。
  • 初始化和持久化:可以在应用启动时加载初始状态或者持久化状态到本地存储。这样在页面刷新后能够恢复状态。

我们用 react-sweet-state 来编写一个计数器的 Demo 看一下代码结构。

jsx 复制代码
import React from 'react';
import { createStore, createHook, Action } from 'react-sweet-state';

// 定义初始状态
const initialState = {
  count: 0,
};

// 定义 actions,包括增加、减少和重置操作
const actions = {
  increment: () => ({ getState, setState }) => {
    const { count } = getState();
    setState({ count: count + 1 });
  },
  decrement: () => ({ getState, setState }) => {
    const { count } = getState();
    setState({ count: count - 1 });
  },
  reset: () => ({ setState }) => {
    setState({ count: 0 });
  },
};

// 创建 store,包含状态和 action
const Store = createStore({
  initialState,
  actions,
});

// 创建自定义 hook 以访问 store 和 action
const useCounter = createHook(Store);

// 计数器组件,使用我们的自定义 hook
const Counter = () => {
  // 使用 useCounter 获取状态和 action
  const [{ count }, { increment, decrement, reset }] = useCounter();

  return (
    <div>
      <h2>计数器</h2>
      <p>当前计数: {count}</p>
      <button onClick={increment}>增加</button>
      <button onClick={decrement}>减少</button>
      <button onClick={reset}>重置</button>
    </div>
  );
};

export default Counter;

在状态变更上和 Zustand 很像,都是提供了 set 和 get 函数。结构上多了两个概念,state 和 action,略显冗余。为什么这么说?如果对新手,或者状态管理库刚开始发展的阶段,区分 state 和 action 是有必要的。但是在状态管理库深入人心的时代,每个人都知道我们写的代码是在做什么,再从 API 层面去区分它们,就显得没有必要,反而要多写很多代码,这也是为什么 Zustand 非常受欢迎的原因。

react-sweet-state 的 API 设计非常经典,我认为在设计层面没有任何问题,很优秀。但是没有做到更进一步,所以略逊于 Zustand。

到这里我们可以反思一个问题。开发者到底在要什么?其实很简单,就两个字:简单。

Zustand 为什么这么受欢迎?就是因为它太简单了,把一些不是绝对必须的东西都删除掉了。API 层面没有 state、没有 action、没有 reducer、没有 Context、没有 namespace。很多时候我们只需要一个 create 函数就够了。甚至连 create 函数的名字都是最短的,不像其他一些库把 API 定义为 createStore 或者 createXXX。因为所有人都知道这是在创建一个 Store,那 store 的概念也可以从 API 层面抹杀了。这也是 Zustand 了不起的地方,它看透了一个点:深入人心、司空见惯的事情,就不需要再从代码里体现出来。

相关信息梳理归纳:

Dva 的继任者 - Hox

在 Dva 出现 3 年后,其中很多设计理念和 API 其实已经和最新版本的 React 有些脱钩了。Dva 的作者云谦决定再创建一个新的项目:umi。umi 是一个现代 React 企业级框架。其中状态管理方案就是 Hox。

Hox 的作者不是云谦,而是阿里巴巴团队的其他工程师。Hox 希望解决的唯一痛点就是:「在多个组件间共享状态」。所以在一定程度上和 Redux、Zustand 它们不同。

那我们通过计数器的案例来看下 Hox 是怎么做的。

首先创建一个模型。

jsx 复制代码
import { useState } from 'react';
import { createModel } from 'hox';

// 创建一个自定义 Hook,用于实现计数逻辑
function useCounter() {
  // 使用 useState 初始化计数器的 state
  const [count, setCount] = useState(0);

  // 创建增加计数的方法
  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  // 创建减少计数的方法
  const decrement = () => {
    setCount((prevCount) => prevCount - 1);
  };

  // 创建重置计数器的方法
  const reset = () => {
    setCount(0);
  };

  // 返回 count 状态以及修改状态的方法
  return {
    count,
    increment,
    decrement,
    reset,
  };
}

// 使用 createModel 将上面的自定义 Hook 转换成一个可以在组件间共享状态的 Model
export default createModel(useCounter);

然后创建 UI 组件:

jsx 复制代码
import React from 'react';
// 引入我们建立的 Counter Model
import useCounterModel from './useCounterModel';

function Counter() {
  // 从我们的 Model 中获得状态和操作方法
  const { count, increment, decrement, reset } = useCounterModel();

  return (
    <div>
      <h2>计数器: {count}</h2>
      <button onClick={increment}>增加</button> {/* 点击按钮增加计数 */}
      <button onClick={decrement}>减少</button> {/* 点击按钮减少计数 */}
      <button onClick={reset}>重置</button>     {/* 点击按钮重置计数 */}
    </div>
  );
}

export default Counter;

可以看到 Hox 是以一种非常接近于原生 React 的方式来组织代码的。定义 Model 的 Hook,实际上就是创建一个函数,然后在函数中使用原生 API useState,可谓是极其精简和干净。在一众同类库中,我认为 Hox 的目的最单纯、纯度也最高。

相关信息梳理归纳:

阿里飞冰的另一作品 - icestore

除了 Dva 和 Hox,阿里还有一个开源项目,就是 ice。

如果你研究过低代码,应该对飞冰这个平台不陌生,ice 就是飞冰。是国内最早一批低代码平台。icestore 就是 ice 的状态管理库。

icestore 的灵感来源于文中介绍过的 Rematch 和另外一个状态管理库 constate,由于 constate 在去年就停止维护了,所以文中没有介绍 constate。

可以看出阿里的工程师精力都非常旺盛,热衷于造轮子。

我把它官方的计数器 Demo 拿过来看下代码结构:

jsx 复制代码
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, createModel } from '@ice/store';

const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time));

// 1️⃣ 使用模型定义你的状态
const counter = createModel({
  state: 0,
  reducers: {
    increment:(prevState) => prevState + 1,
    decrement:(prevState) => prevState - 1,
  },
  effects: () => ({
    async asyncDecrement() {
      await delay(1000);
      this.decrement();
    },
  })
});

const models = {
  counter,
};

// 2️⃣ 创建 Store
const store = createStore(models);


// 3️⃣ 消费模型
const { useModel } = store;
function Counter() {
  const [ count, dispatchers ] = useModel('counter');
  const { increment, asyncDecrement } = dispatchers;
  return (
    <div>
      <span>{count}</span>
      <button type="button" onClick={increment}>+</button>
      <button type="button" onClick={asyncDecrement}>-</button>
    </div>
  );
}

// 4️⃣ 绑定视图
const { Provider } = store;
function App() {
  return (
    <Provider>
      <Counter />
    </Provider>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

可以看出来 Rematch 的味道极重,从官方文档和代码中也并未看出有什么创新性。这里我就想吐槽一句,为什么不直接用 Rematch 呢?(没有贬低 icestore 的意思)

相关信息梳理归纳:

新时代的代理库 - Valtio

时间来到 2020 年,这一年可谓是前端最巅峰的一年,也是开源最后的一波热潮。由于新冠导致的全球经济恶化,互联网行业和一些周边行业也难以避免的走向了下坡路。

国内的裁员潮其实是在 2019 年就开始了。只是刚开始动作不大,没有引起大家广泛的关注。但是裁员确实存在,我统计了 19 年的一些裁员数据:百度无人驾驶部门、腾讯 PCG、字节跳动某些部门、滴滴、哈罗、携程上海总部、苏宁北京研发中心、新浪阅读、vipkid、马蜂窝、唯品会、去哪儿、宜信、水滴筹、蔚来汽车、快手、360 等公司都有大规模的裁员。

真正被大家关注的,是 2019 年年底的爱奇艺,裁了大概整个公司的 20%-40% 员工。由此彻底开启了国内互联网公司大规模裁员之路。

前面我谈到,互联网的裁员影响的不仅仅是互联网自身。我再举两个例子。

一个是地产行业。在 2020 年阿里还没有进行大规模裁员的时候,杭州阿里巴巴总部所在地的未来科技城的二手房房价一度高达 8 万/平方,一些新房项目甚至出现了数万人摇号的情况。那时候一些炒房客一度认为未来科技城的二手房炒到 10万/平方只是时间问题。然而随着阿里的裁员,房价也开始走下坡路了,二手房的均价也降到 4 万多一平,和巅峰期 6 万多的均价相比,降幅近 3 成。

另一个是知识付费行业。这可能是很多人意想不到的。你可能会认为,大家都找不到工作,一定会焦虑,然后去买知识付费产品提升自己。知识付费行业应该会大赚。实际上不是,知识付费行业反而在走下坡路,很多公司的营收都在下滑。因为大家发现花几千块钱买的课,学完之后依然找不到工作。其中有大家熟悉的行业龙头开课吧,在 22 年倒闭了。当然也有自己太作的原因。拉勾教育在 20 年一度负债高达 2 亿,濒临倒闭,不过后来挺过来了。慕课网、三节课等机构也是在艰难中度日。而我自己创业的前端培训公司也在 22 年轰然倒闭,对我自身的打击也很大。

随着大厂的各种裁员,失业率大幅上升。焦虑中夹杂着压抑的情绪一直在蔓延、升温。脉脉、Boss 等招聘平台上也都是些炫耀几千+沟通、几百+简历投递的帖子,今年又爆出"前端已死"的言论,接着又有"后端已亡"的言论。让人真真切切感受到这股可怕的危机感。可以说这是互联网有史以来最大的危机。

好了,我们言归正传,回到状态管理库上面来。造轮子的步伐似乎停留在了 2020 年,自此之后几乎没有新的、具有开创性的状态管理库出现在大众视野。一个原因可能是大家都在忙着找工作,或者忙着创业,没时间也没有热情继续搞开源了,毕竟开源赚钱实在是太难了,不少开源作者一个月收入只有可怜的几千块。另一个原因可能是卷到头了,Zustand 这种库的出现,已经解决了大多数人的痛点。

Valtio 是日本程序员 Daishi Kato(加藤大志)开发的。加藤的精力非常旺盛,从他的 GitHub 上可以看出,他几乎每天都在写代码。最厉害的是,他同时参与了 Zustand、Valtio 和 Jotai 三个著名状态管理库的开发工作,由此可以看出加藤对状态管理库的理解非常透彻。加藤同时也是 Poimandres 组织的成员之一。

Valtio 是围绕 ES6 的 Proxy 特性来进行设计的,它有以下几点核心的特性:

  • 代理状态,基于 Proxy。
  • 响应式更新,对状态的操作都会通过代理进行,Valtio 内部会跟踪并自动渲染。
  • 细粒度渲染,Valtio 的代理可以实现细粒度的依赖跟踪,这样只有组件实际使用的状态变化时才会重新渲染,避免了不必要的渲染,性能方面很不错。
  • 简洁的 API,Valtio 尽可能地让开发者操作状态就像操作普通对象一样自然。
  • 订阅/通知模式
  • 状态适用于组件之外,除了组件外,也支持用在某些逻辑上。

这样看起来,似乎和 MobX 有些像。不过我认为 Valtio 相比于 MobX 有一些优势,以下是我理解的几点优势:

  • Valtio 的学习难度更低,概念更少,对初学者友好。MobX 是按照标准的响应式编程设计的,有 observable、computed、action 等概念,有一定学习门槛。
  • Valtio 的 API 更加简洁,不需要把状态进行包装成特殊结构或者特殊方法,状态就是一个普通对象。这也是比 MobX 更容易上手的一点。
  • 不需要特殊函数或者装饰器。对比 MobX 的 autorun 或者 @observable 等 API,Valtio 统统没有。
  • 更少的魔法,MobX 内部有不少不太透明的魔法,而 Valtio 几乎没有魔法。
  • 更容易追踪状态的变化。

以下是 Valtio 的官方 Demo:

jsx 复制代码
import { proxy, useSnapshot } from 'valtio'

function Counter() {
  const state = proxy({
    dur: 1,
    count: 0
  });
  const incDur = () => {++state.dur};
  const decDur = () => {--state.dur};
  const incCount = () => {
    ++state.count;
    setTimeout(incCount, 100 * state.dur);
  };

  incCount();

  const snap = useSnapshot(state)

  return (
    <div>
      <h3>{snap.dur}</h3>
      <button
        disabled={snap.dur <= 1}
        onClick={decDur}>
        -
      </button>
      <button
        disabled={snap.dur >= 10}
        onClick={incDur}>
        +
      </button>
    </div>
  );
}

这 API 确实是极其简洁,对 state 的操作也是足够简单。

现在我用 Proxy API 来模拟一下 Valtio 的 API,其实核心原理确实很简单。

js 复制代码
const subscribers = new Set();  // 存储订阅者的集合

function subscribe(subscriber) {
  // 添加订阅者
  subscribers.add(subscriber);
}

function unsubscribe(subscriber) {
  // 移除订阅者
  subscribers.delete(subscriber);
}

function notify() {
  // 通知所有订阅者
  subscribers.forEach((subscriber) => subscriber());
}

function proxy(target) {
  // 创建 Proxy 对象用于状态管理
  return new Proxy(target, {
    get(target, prop, receiver) {
      // 属性读取操作
      return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
      // 属性设置操作,当状态改变时通知订阅者
      const result = Reflect.set(target, prop, value, receiver);
      notify(); // 状态更改时触发更新
      return result;
    },
  });
}

// 使用示例
const state = proxy({ count: 0, text: 'hello' }); // 创建一个响应式状态对象

// 订阅一个函数来模拟组件的渲染
const render = () => {
  console.log(`The count is now: ${state.count}`);
};

// 订阅渲染函数,以便在状态更改时进行更新
subscribe(render);

// 更改状态,触发订阅的渲染函数
state.count += 1; // 输出: The count is now: 1
state.count += 1; // 输出: The count is now: 2

// 如果不再需要更新,可以取消订阅
unsubscribe(render);

// 更改状态,但由于取消了订阅,不再触发渲染函数
state.count += 1; // (不输出任何内容)

我个人很喜欢 Valtio,如果你很喜欢响应式编程,也很喜欢 MobX,那么我推荐你试试 Valtio,它或许是一个比 MobX 更优秀的库(没有贬低 MobX 的意思)。

相关信息梳理归纳:

  • 官网: valtio.pmnd.rs/
  • GitHub: github.com/pmndrs/valt...
  • GitHub Star:7.8k
  • 作者:Daishi Kato
  • 主要成就:基于 Proxy 设计的、新时代的状态管理库代表,无论是从设计理念还是从 API 的设计上,都超越了这个赛道的老大哥 MobX。毫无疑问的优秀之作。

Atoms 系开创者 - Recoil

在 Facebook 创造了 Flux、Relay 和 Redux 等一众状态管理库后,又推出了 Recoil。Facebook 在设计 Flux、Redux 时,React 并没有 Hooks。在 React 推出 Hooks 后,Facebook 的工程师自知它们都过时了,也知道它们一直被开发者所诟病。

所以在 2020 年的 React Europe 大会上,Facebook 的工程师 Dave McCabe 介绍了他们团队新推出的状态管理库 Recoil。按照官方的说法,Recoil 目前还处于实验性阶段。它主要是建立在 React 的原生概念之上的,比如 Hooks。然后提供一些单独使用 React 难以实现的功能,同时与最新版本的 React 保持兼容。从它的目的看就知道 Recoil 不会太稳定。

Recoil 的核心设计理念包括以下几点:

  • 最小的可共享状态 Minimal Shared State。Recoil 首次将 React 提出了原子 atoms 的概念融入到状态管理库中,原子就是指程序中最小的可分享状态单元。通过对原子的订阅和更新,可以消除 props 钻孔的问题。其实这个概念存在很多年了,只是从来没有人想过要用一个词对这个概念进行定义。要说简单概念复杂化、造词的能力,还得是大厂。
  • 派生状态 Derived State。通过 selector 把 atoms 进行处理,从而派生出新的状态。也是一个老概念。
  • 基于组件的状态管理。提供了一系列 Hooks,比如 useRecoilValue、useRecoilState 和 useSetRecoilState,主要就是让组件能够订阅 atoms 和 selectors,从而具备根据状态响应 UI 的能力。
  • 性能优化。因为有订阅机制,所以 Recoil 可以确保只有组件所依赖的 atoms 或者 selectors 发生变化才会重新渲染。原理也是为了避免不必要的计算和重渲染。
  • 并发模式的兼容。支持 React 的并发模式,在异步渲染期间能够保持状态的一致。
  • 异步状态处理。通过异步 Selectors 支持异步状态的管理,解决了数据获取怎么整合到状态管理流程的问题。还可以配合 Suspense 功能,让异步交互更加顺滑。
  • 开发者体验。Recoil 的接口设计和 React 保持一致性,同时提供了开发者工具 Recoilize,可以帮助开发者监控和调试状态变化。

我们来看一下用 Recoil 写的计数器是怎么样的。

jsx 复制代码
import React from 'react';
import { atom, useRecoilState, useResetRecoilState, selector, useRecoilValue } from 'recoil';

// 定义一个Recoil的atom,这个atom代表了可共享的状态 - 计数器的数值
const counterState = atom({
  key: 'counterState', // 唯一标识符
  default: 0, // 初始值设置为0
});

// 定义一个Recoil的selector,这个selector不改变值,只是用来展示如何使用
// 可以用于将来的计算派生状态。
const counterDisplayState = selector({
  key: 'counterDisplayState',
  get: ({get}) => {
    const count = get(counterState);
    return `计数器的值:${count}`; // 返回展示用的字符串
  },
});

const Counter = () => {
  // 使用useRecoilState Hook来订阅和更新counterState
  const [count, setCount] = useRecoilState(counterState);
  
  // 使用useResetRecoilState Hook来获得重置counterState的函数
  const resetCounter = useResetRecoilState(counterState);
  
  // 使用useRecoilValue Hook来获取counterDisplayState的值,
  // 这里只是用来展示selector的使用,通常用于派生和计算的状态
  const displayValue = useRecoilValue(counterDisplayState);
  
  // 处理增加操作
  const handleIncrement = () => {
    setCount(count + 1);
  };
  
  // 处理减少操作
  const handleDecrement = () => {
    setCount(count - 1);
  };
  
  return (
    <div>
      <div>{displayValue}</div>
      <button onClick={handleIncrement}>+</button>
      <button onClick={handleDecrement}>-</button>
      <button onClick={resetCounter}>重置</button>
    </div>
  );
}

// 包装了 Counter 组件的 App 组件
const App = () => (
  <div>
    <h2>Recoil 计数器 Demo</h2>
    <Counter />
  </div>
);

export default App;

不愧是 Facebook,无论是概念还是接口,在设计上都显得非常规整。但实话实说,接口不够简洁。概念还是很多。起码和 Zustand 还是有不小差距的。API 确实非常符合 React 的一贯风格。如果你对 React 原生风格情有独钟,那么可以去尝试 Recoil。毕竟作为官方团队,肯定是最懂 React 的。

但是按照前端社区惯例,除了 React 本身,React 的官方出品往往都不是最佳选择,所以接下来我会介绍 Recoil 的模仿者和竞争者 Jotai。

相关信息梳理归纳:

  • 官网: recoiljs.org/
  • GitHub: github.com/facebookexp...
  • GitHub Star:19.2k
  • 作者:Facebook 开发团队
  • 主要成就:作为 Hooks 时代的 React 官方出品,同时也是顺应官方 Atoms 等概念,等于是开辟了一条状态管理库的新赛道。

Atoms 系的改良者 - Jotai

在 Recoil 发布不久后,社区出现了另外一个项目 Jotai。Jotai 的意思就是日语的单词 "状態"(joutai),也就是状态的意思。

Jotai 主要作者是 Poimandres 组织的 Pedro Nauck 和 Daishi Kato,之前文中介绍 Zustand 时提到过 Poimandres,同时也是 Zustand 的作者。很感谢 Poimanders,为大家贡献了多个优秀的状态管理库。而 Daishi Kato 也是 Valtio 的作者。

Pedro 的灵感来自于 Recoil。它最大的优势就是足够简单。这里不得不吐槽一句,官方在定义概念上永远都是最强大的,但是真正落地的时候,永远都是复杂的。而社区的作品几乎都是在官方作品的基础之上进行轻量化、简单化。

Jotai 的优势主要有三点,其他的优势和 Jotai 几乎一致,就不赘述了:

  • 体积小,2kb。
  • 提供了很多功能扩展。
  • 和 Recoil 相比,消除了 key。

我们看下用 Jotai 来编写计数器是怎么样的。

jsx 复制代码
import React from 'react';
import { atom, useAtom } from 'jotai';

// 创建一个 Jotai 原子代表计数器的状态
// 这个原子可以在组件间共享
const counterAtom = atom(0); // 参数 0 是原子的初始值

// 创建对应的 setter 原子,用于修改 counterAtom 的值
// 这种方式可以实现更复杂的状态逻辑和更好的模块化
const incrementAtom = atom(
  (get) => get(counterAtom), // 获取当前的计数器值
  (get, set) => set(counterAtom, get(counterAtom) + 1) // 实现增加操作
);
const decrementAtom = atom(
  (get) => get(counterAtom),
  (get, set) => set(counterAtom, get(counterAtom) - 1) // 实现减少操作
);
const resetAtom = atom(
  (get) => get(counterAtom),
  (get, set) => set(counterAtom, 0) // 实现重置操作
);

// 计数器组件
const Counter = () => {
  // 使用 useAtom Hook 绑定到特定的原子状态和操作
  const [count, ] = useAtom(counterAtom);
  const [, increment] = useAtom(incrementAtom);
  const [, decrement] = useAtom(decrementAtom);
  const [, reset] = useAtom(resetAtom);

  return (
    <div>
      <h1>计数器: {count}</h1>
      <button onClick={increment}>增加</button> {/* 调用增加操作 */}
      <button onClick={decrement}>减少</button> {/* 调用减少操作 */}
      <button onClick={reset}>重置</button> {/* 调用重置操作 */}
    </div>
  );
};

export default Counter;

有点 Recoil 的影子,又有点 Zustand 的模样。

我认为在 API 层面相比 Recoil 的优势就是收敛了很多,其实我们认真思考,确实只需要一个 atom 和一个 useAtom 就足够应付大多数情况了。至于 selector 的概念,完全可以放到 atom 的参数中,而不需要单独一个 API,这样降低了理解门槛和使用门槛。

而 atom 函数的两个参数,又和 Zustand 中设置状态和获取状态的样子很相似。

可以说 Jotai 融百家之长于一身,设计理念是目前所有状态管理库中最先进的。

那么 Jotai 和 Valtio 之间该怎么选择呢?作为两个库的作者,加藤写过一篇博客:When I Use Valtio and When I Use Jotai,结论就是一句话:以数据为中心的应用,用 Valtio。以组件为中心的应用,用 Jotai。

Jotai 也是目前我最推荐使用的状态管理库之一,另一个是 Zustand。

相关信息梳理归纳:

至此已经把 20 大状态管理库全部盘点完毕。

本文从时间线的角度出发,详细介绍了状态管理库的起源、发展、成熟壮大的完整过程。希望能够让你理解这个领域整体的情况和现状。能够根据自身需求正确选择适合自己的库。如果你对文中的一些观点有不同见解,欢迎评论区留言。

2023 年马上过去了,另外希望大家在新的一年里能够找到称心如意的工作。同时还希望全球经济能够快点复苏,再次重现互联网的荣光。

本人在 Web3、AI 等热门领域进行创业,如果有对这些领域感兴趣的朋友,可以添加我的微信:LZQ20130415,拉你进交流群,大家一起讨论最新的技术趋势,一起吃技术带来的红利。

相关推荐
一颗松鼠几秒前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds20 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
阿伟来咯~1 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨1 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓3 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 小时前
无网络安装ionic和运行
前端·npm