前端状态管理库对比

状态管理

为什么 jQuery 时代,我们并没有谈论状态管理,但是在 ReactVue 的时代,我们日常开发,基本离不开一些状态管理库?

jQuery 是针对 "过程" 的命令式编程,不会去管理数据。而现代前端框架则是把对 "过程" 的各种命令,变为了对 "状态" 的描述。因此前端开发也就从组合各式各样的命令完成用户界面更新和交互,变成了以状态驱动web界面变化的状态机式开发方式

什么是状态?

状态就是 UI 中的动态数据。对于 React 框架而言,就是 stateReact 框架的核心思想是 UI = f(state),即「UIstate 的投影」,state 自上而下流动,整个 React 组件树由 state 驱动。

目前比较常见的状态管理库有 Redux、Mobx、Rematch、Zustand、Recoil、Jotai、Valtio 等。其中 Redux、Mobx、Rematch、Zustand、Valtio 不依赖 UI 框架,在其他框架中也能使用。本文只对不依赖 UI 框架的状态管理库进行介绍,希望能对你在不同的 UI 框架中进行状态管理库选型时提供帮助。首先看下这几个框架近几年的 npm 下载趋势,可以看到 redux 仍遥遥领先~

本文将对 Redux、Mobx、Rematch、Zustand、Valtio 几个库进行介绍,从背景->核心工作流->适用场景->使用->原理进行介绍

对这些部分不感兴趣的同学可以直接跳到最后一节对比(省流篇),以表格的形式清晰、简洁的展示各个框架的区别,方便你的选型~

Redux

简介

Redux是什么?

官方解释:Redux 是一个使用叫做 action 的事件来管理和更新应用状态的模式和工具库
通俗解释:可以把 web 界面当做一个状态机,UI 和状态一一对应,状态以对象的形式存储,redux 就是去管理这个对象以何种方式更新的工具

Redux的工作流程:

  • 首先,用户(通过 View )发出 Action,发出方式就用到了 dispatch 方法。
  • 然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 ActionReducer 会返回新的 State
  • State 一旦有变化,Store 就会调用监听函数,来更新 View

如下图:

为什么要使用 Redux

前端涉及到大量复杂的逻辑判断、交互和异步操作。当应用规模越来越大时,与前端界面相关联的状态也越来越多,界面的改变就会变得难以预测。比如,当视图中的某个标签 A 发生了改变,在传统的开发方式中,我们需要去找到代码里所有修改标签 A 的地方。但是假如我们使用 Redux 去统一管理状态,标签 A 受控于状态 A,而状态 A 的改变只会通过派发一个 action 来改变。这样标签 A 的状态就变得易于控制,且容易预测。

来自官方:Redux 提供的模式和工具使你更容易理解应用程序中的状态何时、何地、为什么、state 如何被更新,以及当这些更改发生时你的应用程序逻辑将如何表现. Redux 指导你编写可预测和可测试的代码,这有助于你确信你的应用程序将按预期工作。

Redux 有哪些使用场景?

  • 应用中有很多 state 在多个组件中需要使用
  • 应用 state 会随着时间的推移而频繁更新
  • 更新 state 的逻辑很复杂
  • 中型和大型代码量的应用,很多人协同开发

Redux 的使用

Redux 是一个小型的独立 JS 库,不依赖于任意框架(库),只要 subscribe 相应框架(库)的内部方法,就可以使用该应用框架保证数据流动的一致性。在 react 中使用时,可以使用 React-Redux 库,它是 React 官方的 Redux UI 绑定库

React 框架中使用

首先看一张 react-reduxredux 的关系图:

总结一下 React-Redux 做了哪些事情(简化版,详细版参见文档 ):

  1. 包装渲染函数
    • 对外只提供了一个 Providerconnect 的方法,隐藏了关于 store 操作的很多细节
    • Provider 接受 store 作为参数,并且通过 contextstore 传给所有的子组件
    • 子组件通过 connect 包裹了一层高阶组件,高阶组件会通过 context 结合 mapStateToPropsstore,把数据传给被包裹的组件
  2. 避免没有必要的渲染
    • 缓存上次的计算的 props,然后用新的 props 和旧的 props 进行对比,如果两者相同,就不调用 render
  3. 更新机制
    • 本质还是使用 reduxstore.subscribe 进行更新订阅,store.dispatch 进行更新派发
    • connect 包裹组件时,会通过 react-redux 内部的 subscription 去调用 reduxstore.subscribe 注册监听
    • 任意一个组件 dispatch 了一次,所有组件的更新函数都要被 batch 执行,所有 connect 包裹的组件都会去判断是否真正需要执行更新

使用示例:

js 复制代码
// UserCard.js
import React from 'react'
import {connect} from 'react-redux'
import { CHANGE_NAME } from '../../store/actions/type'

const mapStateToProps = (state, ownProps) => {
  const { userReducer } = state
  return {
    userInfo: userReducer
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    changeNameAction: (value) => dispatch({
      type: CHANGE_NAME,
      payload: value
    })
  }
}

const UserCard = (props) => {
  const {changeNameAction, userInfo} = props
  const changeName = () => {
    changeNameAction(`${new Date().getTime()}`)
  }
  return (
    <div className="App">
        <div>
          <div>我的名字是{userInfo.nickName || 'xxx'}</div>
          <div>我的性别是{userInfo.gender || ''}</div>
          <div onClick={changeName}>点击改变名字</div>
        </div>
      </div>
  )
}
export default connect(mapStateToProps, mapDispatchToProps)(UserCard)

// App.js
import './App.css';
import UserCard from './Components/UserCard';
import Store from './store'
import { Provider } from 'react-redux'
function App() {
  return (
    <Provider store={Store}>
      <UserCard />
    </Provider>
  );
}

export default App;

// 全局store.js
import {legacy_createStore as createStore, combineReducers} from 'redux'
import userReducer from './reducer/user'
import animationReducer from './reducer/animation'

const rootReducer = combineReducers({
  userReducer,
  animationReducer
})

const Store = createStore(rootReducer)
export default Store

// reducer.js
import { CHANGE_NAME } from "../actions/type"

const initialUserState = {
  avatarUrl: '',
  nickName: '',
  gender: 'female'
}
const userReducer = (state = initialUserState, action) => {
  switch(action.type){
    case CHANGE_NAME:
      return {...state, nickName: action.payload}
    default:
      return state
  }
}
export default userReducer

单独使用Redux

  1. 使用 createStore 创建全局 storecreateStore 返回一个对象,包含dispatch、subscribe、getState、replaceReducer 等方法
  2. store.dispatch(action):派发 action,组合 action 和当前 state,获取到最新 state 数据,遍历收集的订阅函数
  3. store.subscribe(listener):订阅监听函数,存放在数组中,store.dispatch(action) 时遍历执行。
  4. store.getState():获取当前 store 的数据

举个在小程序中使用的例子:

js 复制代码
// 省略全局store.js,同上
import Store from '../../store'
import {changeUserName} from '../../store/actions/user'

Page({
  data: {userInfo: {}},
  onLoad() {
    const fn = () => {
      const {userReducer} = Store.getState()
      this.setData({
        userInfo: userReducer
      })
    }
    fn()
    Store.subscribe(() => {
      fn()
    })
  },
  getUserProfile(){
      wx.getUserInfo({
        success: (res) => {
          Store.dispatch(changeUserName(res.userInfo.nickName))
        },
        fail: (error) => {
          console.error("getUserInfo error", error)
        },
      })

  }
})
// wxml
view class="app">
  <block wx:if="{{!userInfo.nickName}}">
    <view bindtap="getUserProfile">获取头像昵称</view>
  </block>
  <block wx:else>
    <image class="userinfo-avatar" src="{{userInfo.avatarUrl}}"></image>
    <text class="userinfo-nickname">{{userInfo.nickName}}</text>
    <text class="userinfo-gender">{{userInfo.gender ? '女' : '男'}}</text>
  </block>
</view>

// action.js
import { CHANGE_NAME } from "./type"

export const changeUserName = newName => {
  return {
    type: CHANGE_NAME,
    payload: newName
  }
}

// 省略 reducer.js,同上

Redux 实现

主流程

1.createStore

createStore 主要用于 Store 的生成,返回一个 store 对象包含 dispatch、subscribe、getState、replaceReducer 等方法。dispatch 了一个 init Action,为了生成初始的 State

  • getState:返回当前的状态
  • replaceReducer:替换当前的 Reducer 并重新初始化了 State
  • dispatch:分发 action,修改 State 的唯一方式
  • subscribe:入参函数放入监听队列,返回取消订阅函数

getStatereplaceReducer 函数比较简单,不再展开

2. store.dispatch
  1. 调用 Reducer,传参(currentState,action
  2. 按顺序执行 listener
  3. 返回 action
3. store.subscribe

createStore 函数中存储 listeners 设计了两个数组:nextListeners、currentListeners

  • 每次 dispatch,都从 currentListeners 中取订阅函数
  • 每次 subscribe,都往 nextListeners 中增加订阅函数

这样设计的目的是为了满足 redux 的设计理念:无论是新增订阅或是取消订阅,都不会在当前 dispatch 阶段生效,只会在下次 dispatch 阶段生效

辅助功能

仅介绍使用的比较多的函数:

  • 与中间件实现相关的 compose,applyMiddleWare
  • combineReducers
combineReducers
  • 作用:合并多个 reducer 为一个函数 combination
  • 使用场景:应用比较大的时候,将 Reducer 按照模块拆分
中间件

先通过一张图看一下,什么是中间件

诞生背景:

Redux reducer 设计理念之一是"绝对不能包含副作用" 副作用:除函数返回值之外的任何变更,包括 state 的更改或者其他行为

  • 一些常见的副作用有:
    • 在控制台打印日志
    • 异步更新 state
    • 修改存在于函数之外的某些 state,或改变函数的参数
    • 生成随机数或唯一随机 ID

为了使 redux 能支持这些副作用,设计了 middleware,用以支持增加一些副作用逻辑代码 中间件的本质 :中间件就是一个函数,对 store.dispatch 方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能

常用的中间件有:

  • redux-thunk
  • redux-logger
  • redux-saga

先看下中间件如何在代码里使用

js 复制代码
// store.js
import {legacy_createStore as createStore, combineReducers, applyMiddleware} from 'redux'
// 新增 redux-thunk中间件
import {thunk} from 'redux-thunk'
import userReducer from './reducer/user'
import animationReducer from './reducer/animation'

const rootReducer = combineReducers({
  userReducer,
  animationReducer
})
// 使用middleware
const Store = createStore(rootReducer, applyMiddleware(thunk))
export default Store
// UserCard 组件
const mapDispatchToProps = (dispatch) => {
  return {
    changeNameActionDelay: (value) => dispatch(changeUserNameAsync(value))
  }
}
 const changeNameDelay = () => {
    changeNameActionDelay(`${new Date().getTime()}`)
  }
<div onClick={changeNameDelay}>点击延迟10s改变名字</div>
// 异步action
export const changeUserNameAsync = (name) => {
  return async(dispath, getState) => {
    await new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, 10000)
    })
    dispath(changeUserName(name))
  }
}

const Store = createStore(rootReducer, applyMiddleware(thunk)) 看,中间件是如何实现的? 从 createStore 里看,上面的代码相当于执行 applyMiddleware(...middlewares)(createStore)(reducer,preloadedState)

先粗略的看下 applyMiddleware,最终的 dispatch 是通过 compose 函数组装的,首先看一下 compose 是怎么组装函数的

compose

compose 这个方法,主要用来组合传入的一系列函数 compose(f,g,h) 等价于 return (...args) => f(g(h(...args)))

js 复制代码
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return (arg) => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args))
  )
}
applyMiddleware

最终 applyMiddleware 的执行结果是返回了一个正常的 Store 和一个被变更过的 dispatch 方法,实现了对 Store 的增强。

js 复制代码
function applyMiddleware(
  ...middlewares
) {
  return createStore => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 假设chain是[f,g,h],最终生成的dispatch相当于f(g(h(store.dispatch)))
    // 相当于把原有的dispatch层层过滤,变成新的dispatch
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

结合 applyMiddleware 的实现,我们在写中间件时要注意:

  • 中间件是要多个首尾相连的,需要一层层的"加工",所以要有个 next 方法来独立一层确保串联执行
  • dispatch 增强后也是个 dispatch 方法,也要接收 action 参数,所以最后一层肯定是 action
  • 中间件内部需要用到 Store 的方法,所以 Store 放到顶层

现在再来看看上面的中间件使用的例子是如何工作的 假设我们现在有三个中间件, f,g,h dispatch = f(g(h(store.dispatch))) 中间件一般是【自己实现一个中间件】一节中的格式的函数,这样的话整个执行链路就是这样的:

  1. h(store.dispatch),此时,next = store.dispatch,返回一个 【action => {}】 这样的函数
  2. g(h(store.dispatch))next = h(store.dispatch),返回一个 【action => {}】 这样的函数
  3. f(g(h(store.dispatch)))next = g(h(store.dispatch)),返回一个 【action => {}】 这样的函数
  4. 当业务代码中调用 dispatch 的时候
    • 如果派发的 action 不是一个函数,执行 next(action),会依次执行 g(h(store.dispatch))-> h(store.dispatch)->store.dispatch(action)
    • 如果派发的 action 是一个函数,执行 action(dispacth, getState),全局的 store.getState,和 storedispatch 作为参数传递给业务侧定义的 action 函数,如 changeUserNameAsync,最终执行 dispatch(action),等价于使用 store.dispatch 派发 action
自己实现一个中间件
js 复制代码
// 可以实现异步action
const myMiddleWare = ({
  getState,
  dispatch
}) = next => action => {
  if(typeof action === 'function'){
    console.log('>>>函数Action', action)
    return action(dispacth, getState)
  }
  console.log('>>>object action', action)
  return next(action)
}

Mobx

简介

Mobx 是什么?

Mobx 也是一个状态管理工具,和 Redux 类似,不依赖于前端 UI 框架库。Mobx 的作者觉得 Redux 比较繁琐,采用响应式编程的方式设计了 Mobx

Mobx核心概念:

  • State (状态):驱动应用程序的数据
  • Actions (动作):去改变 state 的代码
  • Derivations (派生):任何源自状态并且不会再有任何进一步的相互作用的东西就是衍生,比如:用户界面,后端集成,衍生数据。派生分为两种:
  • Computed values:使用纯函数从当前 state 中衍生出的值
  • Reactions:当 State 改变时需要自动运行的副作用 (命令式编程和响应式编程之间的桥梁)

Mobx的工作流程

  • 事件触发了 ActionsReactions 可以经过事件调用 Actions
  • Actions 作为唯一修改 State 的方式,修改了 State
  • State 的修改更新了计算值 Computed
  • 计算值的改变引起了 Reactions 的改变

Mobx 原则:

  • 所有的 derivations 将在 state 改变时自动且原子化地更新。因此不可能观察中间值。
  • 所有的 derivations 默认将会同步更新,这意味着 action 可以在 state 改变之后安全的直接获得 computed 值。
  • computed value 的更新是惰性的,任何 computed value 在需要他们的副作用发生之前都是不激活的。
  • 所有的 computed value 都应是纯函数,他们不应该修改 state

Mobx适用于哪些场景?

  • 首先 MobxRedux 一样都是状态管理库,所以 Redux 的使用场景同样适用于 mobx
  • mobx 比较容易上手,适用于需要快速迭代的小项目
  • 如果你之前已经习惯开发 vue,因为 mobxvue 都是响应式编程,所以使用mobx 会更顺手一些

Mobx 的使用

单独使用 mobx

  1. 定义一个可观察的状态

    js 复制代码
    const userStore = observable({
      userInfo: {
        nickName: 'xxx',
        gender: '',
        age: 0
      }
    })
  2. 使用 autorun 定义响应函数。autorun 函数接受一个函数作为参数,每当该函数所观察的值发生变化时,它都会运行。

    js 复制代码
    autorun(() => {
      const nameBox = document.querySelector('#nameBox')
      nameBox.innerHTML = `${userStore.userInfo.nickName || ''}`
    })
  3. action:使用 action 定义函数。通过 action 显式地修改状态,使得状态的变化可预测(状态的变化能定位到是哪个 action 引起的)。此外,action 函数是事务型的,通过 action 修改状态时,响应函数不会立即执行,而是等到 action 结束后才执行,这有助于提升性能。

    js 复制代码
    const userStore = observable({
      changeName: action(function() {
        this.userInfo.nickName = `${new Date().getTime()}`
      })
    })
    const clicBox = document.querySelector('#nameClick')
    clicBox.addEventListener('click', () => {
    userStore.changeName()
    })
  4. computedcomputed 属性有两个特性。使用属性 .get() 获取其值

    • 被缓存:每当读取 computed 值时,如果其依赖的状态或其他 computed 值未发生变化,则使用上次的缓存结果,以减少计算开销,对于复杂的 computed 值,缓存可以大大提高性能
    • 惰性计算:只有 computed 值被使用时才重新计算值。反言之,即使 computed 值依赖的状态发生了变化,但是它暂时没有被使用,那么它不会重新计算。
    js 复制代码
    const userStore = observable({
      age: computed(function() {
        return Math.floor(Math.random() * 200)
      })
    })
    autorun(() => {
      console.log('>>>>', userStore.age.get())
    })
    // 因为age没有依赖状态,所以这里其实每次打印都会是同一个值

React 框架中使用

React框架中使用时,可以借助 mobx-reactmobx-react-lite(轻量级)框架

mobx-react 相对于 mobx,主要增加了以下几点:

  • 提供了 providerinject,方便管理多 store
  • 提供了一个 observer 方法, 它是一个高阶组件,它接收 React 组件并返回一个新的 React 组件,返回的新组件能响应(通过 observable 定义的)状态的变化

使用方式:

  1. 定义 store 通过 provider 注入:多 store 的话,可以聚合成一个 store

    js 复制代码
    import UserStore from "./userStore"
    import CountStore from "./countStore"
    
    export default {
      userStore: new UserStore(),
      countStore: new CountStore()
    }
  2. 在根组件中通过 Provider 注入 store

    js 复制代码
    import React from 'react'
    import {Provider} from 'mobx-react'
    import UserCardMobx from './Components/UserCardMobx';
    import store from './store/mobx/index'
    function App() {
      return (
        <Provider {...store}>
          <UserCardMobx />
        </Provider>
      );
    }
    
    export default App;
  3. React 组件中使用 Store:利用 observer,让 React 组件响应 store 的变化

js 复制代码
import React from 'react'
import {observer, inject} from 'mobx-react'

export default inject('userStore')(observer(({
  userStore
}) => {
    const {userInfo} = userStore
    const changeNameFn = () => {
      userStore.changeName()
    }
    return (
      <div className="App">
          <div>
            <div>我的名字是{userInfo.nickName || 'xxx'}</div>
            <div>我的性别是{userInfo.gender || ''}</div>
            <div onClick={changeNameFn}>点击改变名字</div>
            <div onClick={userStore.changeNameDelay}>点击延迟10s改变名字</div>
          </div>
        </div>
    )
}))

Mobx 实现

MobX 的主要设计思想:「函数响应式编程」+「可变状态模型」

实现:mutable + proxy(为了兼容性,proxy 实际上使用 Object.defineProperty 实现)

简单总结:

  1. Object.defineProperty 或者 Proxy 来拦截 observable 包装的对象属性的 get/set
  2. autorun 或者 reaction 执行的时候,会触发依赖状态的 get,此时将 autorun 里面的函数和依赖的状态关联起来。也就是我们常说的依赖收集。
  3. 当修改状态的时候会触发 set,此时会通知前面关联的函数,重新执行他们

mobx 的源码相比于 redux 真的很晦涩

只介绍依赖收集 observable 和响应更新 autorun,其他不再介绍

observable

mobx 有多种定义观测对象的方式,有 makeAutoObservableobservablemakeObservable,但无论是使用哪个 API,最终都会根据观测的数据类型,如 array, object 等调用不同的拦截方法实现观测

makeAutoObservable,observable,makeObservable的区别:

  • 使用上的区别:
    • makeObservable 捕获已经存在的对象属性并且使得它们可观察
    • makeAutoObservable 默认情况下它将推断所有的属性
    • observable:复制传入的对象,并将传入对象的所有属性都变成可观察的
  • 底层数据基类的区别:
    • make(Auto)Observable 是基于 ComputedValue
    • observable:支持使用 proxy 的环境直接使用 proxy 拦截整个对象。不支持使用 proxy 的环境,使用 ObservableValue
  • 操作对象的区别:
    • make(Auto)Observable 会修改第一个参数传入的对象
    • observable 会创建一个可观察的 副本 对象。
  • 使用建议:
    • 如果你想把一个对象转化为可观察对象,而这个对象具有一个常规结构,其中所有的成员都是事先已知的,建议使用 makeObservable,因为非代理对象的速度稍快一些,而且它们在调试器和 console.log 中更容易检查

ComputedValueObservableValue 的区别:

  • ComputedValue 既是观察者,也是被观察者,它有自己的依赖,同时又是别人的依赖,所以 ComputedValue 中既有 ObservableValue 的特性,又有 Reaction 的特性。
  • computed 装饰器是用来装饰 get 方法的,它将这个方法包装为 ComputedValue,在进行 get 操作的时候,会判断依赖项是否发生变化,进行方法的执行,判断值是否发生变化,通知观察者自身发生了变化。
  • 某个 ObservableValue 值发生改变后,会调用观察者的 onBecomeStale 方法,表明这个观察者的依赖发生变更,如果这个观察者是 ComputedValue,那么这个观察者的值并没有发生改变,而是当调用到 ComputedValue 的时候,才重新计算这个值。
  • computedautorun 进行依赖收集的方式一模一样,都是借助全局变量 globalState.trackingDerivation 完成的。

对于 object 数据类型而言,核心是调用 asObservableObject,该函数的调用流程是:asObservableObject -> ObservableObjectAdministration

  • make(Auto)Observable: ObservableObjectAdministration.make_-> annotation.make_
  • observable: ObservableObjectAdministration.extend_-> defineObservableProperty_

observable 核心:

observable 的目的是将正常的对象变成 ObservableValueadm 是管理 ObservableValue 的,decoratorenhance 用于配置 adm。将每个属性都要转变为 ObservableValue 后,我们对属性的 setget 其实都是对 admwriteread

js 复制代码
 defineObservableProperty_(
        key: PropertyKey,
        value: any,
        enhancer: IEnhancer<any>,
        proxyTrap: boolean = false
    ): boolean | null {
        try {
            startBatch()
            // ...省略一些非核心逻辑
            const cachedDescriptor = getCachedObservablePropDescriptor(key)
            const descriptor = {
                configurable: globalState.safeDescriptors ? this.isPlainObject_ : true,
                enumerable: true,
                get: cachedDescriptor.get,
                set: cachedDescriptor.set
            }
            // Define
            if (proxyTrap) {
                if (!Reflect.defineProperty(this.target_, key, descriptor)) {
                    return false
                }
            } else {
              // target对象的key的descriptor在这里被设置完成了
              // 将该属性的 get 和 set 方法代理到 adm 的 get 和 set 方法上,然后使用 defineProperty 将这个属性赋给之前建立的空对象。
                defineProperty(this.target_, key, descriptor)
            }
      // 生成 ObservableValue
            const observable = new ObservableValue(
                value,
                enhancer,
                **DEV** ? `${this.name_}.${key.toString()}` : "ObservableObject.key",
                false
            )
      // 将对象的属性设置成 ObservableValue,放到 values 中统一管理。统一的发布-订阅
            this.values_.set(key, observable)
            // Notify (value possibly changed by ObservableValue)
            this.notifyPropertyAddition_(key, observable.value_)
        } finally {
            endBatch()
        }
        return true
    }

autorun

autorun 工作流程如图:

autorun 通过在响应式上下文运行 effect 来工作。在给定的函数执行期间,MobX 会持续跟踪被 effect 直接或间接读取过的所有可观察对象和计算值。 一旦函数执行完毕,MobX 将收集并订阅所有被读取过的可观察对象,并等待其中任意一个再次发生改变。 一旦有改变发生,`autorun 将会再次触发,重复整个过程。

autorun 核心流程:

  1. 创建一个 Reaction 实例
  2. 将响应函数 view 先包裹一层 track 函数,并绑定到 Reaction 内部的 onInvalidate_
  3. 通过 reaction.schedule_() 调度执行
    • reaction.schedule_() 会先将这个 Reaction 实例放入 globalState.pendingReactions
    • 执行 runReactions->runReactionsHelper
    • runReactionsHelper 里面会遍历我们的 pendingReactions 数组,执行里面的 reaction 实例的 runReaction_ 方法
    • 执行 runReaction_ 时,就会执行 onInvalidate_,也就是 track(view)
js 复制代码
export function autorun(
    view: (r: IReactionPublic) => any,
    opts: IAutorunOptions = EMPTY_OBJECT
): IReactionDisposer {
    // 省略 环境判断逻辑
    const name: string =
        opts?.name ?? (**DEV** ? (view as any).name || "Autorun@" + getNextId() : "Autorun")
    const runSync = !opts.scheduler && !opts.delay
    let reaction: Reaction
    if (runSync) {
        // normal autorun
        reaction = new Reaction(
            name,
            function (this: Reaction) {
                this.track(reactionRunner)
            },
            opts.onError,
            opts.requiresObservable
        )
    } else {
        const scheduler = createSchedulerFromOptions(opts)
        // debounced autorun
        let isScheduled = false
        reaction = new Reaction(
            name,
            () => {
                if (!isScheduled) {
                    isScheduled = true
                    scheduler(() => {
                        isScheduled = false
                        if (!reaction.isDisposed_) {
                            reaction.track(reactionRunner)
                        }
                    })
                }
            },
            opts.onError,
            opts.requiresObservable
        )
    }
    function reactionRunner() {
        view(reaction)
    }
    if(!opts?.signal?.aborted) {
        reaction.schedule_()
    }
    return reaction.getDisposer_(opts?.signal)
}

下面详细介绍下 track(view) 的执行流程,看下 track 函数

js 复制代码
track(fn: () => void) {
   // ...省略
    startBatch()
    const notify = isSpyEnabled()
    let startTime
    this.isRunning_= true
    const prevReaction = globalState.trackingContext // reactions could create reactions...
    globalState.trackingContext = this
   // fn即为刚刚绑定的view函数
    const result = trackDerivedFunction(this, fn, undefined)
    globalState.trackingContext = prevReaction
    this.isRunning_ = false
    this.isTrackPending_= false
    if (this.isDisposed_) {
        clearObserving(this)
    }
    if (isCaughtException(result)) this.reportExceptionInDerivation_(result.cause)
    if (**DEV** && notify) {
        spyReportEnd({
            time: Date.now() - startTime
        })
    }
    endBatch()
}

可以看到,trackDerivedFunction 会调用 view 函数。在执行 view 函数的时候,如果里面依赖了被 observable 包裹对象的属性,那么就会触发属性的 get 方法

可以看到,当触发属性的 get 方法时,会执行 reportObserved,会将 observable 挂载到 derivation.newObserving_ 上面

再次回到 trackDerivedFunction 函数,函数接着往下执行到 bindDependencies 函数,将 Reaction 实例和 observable 关联起来。bindDependencies 不再详细展开

js 复制代码
function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    // ...省略
    if (globalState.disableErrorBoundaries === true) {
       // 这里触发了 observableValue.get,继而执行了 reportObserved
       // derivation.newObserving_[derivation.unboundDepsCount_++] = observer;
        result = f.call(context)
    } else {
        try {
            result = f.call(context)
        } catch (e) {
            result = new CaughtException(e)
        }
    }
    globalState.inBatch--
    globalState.trackingDerivation = prevTracking
   // 执行 bindDependencies 函数,将 Reaction 实例和 observable 关联起
    bindDependencies(derivation)

    warnAboutDerivationWithoutDependencies(derivation)
    allowStateReadsEnd(prevAllowStateReads)
    return result
}

Zustand

简介

Zustand 是什么?

基于 Flux 模型实现的小型、快速和可扩展的状态管理解决方案,拥有基于 hooks 的舒适的 API,非常地灵活且有趣. 基于发布订阅模式实现的状态管理方案

Zustand 的工作流程:

  • 通过 create 方法去创建一个 Store,并在 Store 里定义我们需要维护的状态和改变状态的方法
  • create 函数实际上返回了一个 hook,通过调用这个 hook,可以在组件中去订阅某个状态,或者获取改变某个状态的方法

Zustand 的特点:

  • 不需要使用 context provider 包裹你的应用程序
  • 可以做到瞬时更新(不引起组件渲染完成更新过程)
  • 不依赖 react 上下文,引用更加灵活
  • 当状态发生变化时 重新渲染的组件更少
  • 集中的、基于操作的状态管理
  • 基于不可变状态进行更新, store 更新操作相对更加可控

Zustand 的使用

单独使用

不在 reactvue 框架中使用的话,要使用 zustand/vanilla

以下使用示例为在小程序中的使用例子

  1. 创建 store
js 复制代码
import { createStore } from 'zustand/vanilla'

const dateStore = createStore((set) => ({
  date: {
    current: new Date().toLocaleDateString()
  },
  changeTime: () => set((state) => ({ date: {
    ...state.date,
    current: new Date().toLocaleString()
  } })),
  changeTimeDelay: async () => {
    await new Promise(resolve => {
      setTimeout(() => {
        resolve()
      }, 10000)
    })
    set((state) => ({ date: {
      ...state.date,
      current: new Date().toLocaleString()
    } }))
  }
}))

const couponStore = createStore((set) => ({
  coupon: {
    name: ''
  },
  changeName: () => set((state) => ({ date: {
    ...state.date,
    name: `${new Date().getTime()}`
  }})),
}))

export {
  dateStore,
  couponStore
}
  1. view 中使用
js 复制代码
import {dateStore, couponStore} from "../../store/zustand"

Page({
  data: {date: {}, coupon: {}},
  onLoad() {
    const {date = {}} = dateStore.getState()
    const {coupon = {}} = couponStore.getState()
    this.setData({
      date,
      coupon
    })
    dateStore.subscribe(state => {
      this.setData({date: state.date})
    })

    couponStore.subscribe(state => {
      this.setData({
        coupon: state.coupon
      })
    })

  },
  changeTime() {
    dateStore.getState().changeTime()
  },
  changeTimeDelay(){
    dateStore.getState().changeTimeDelay()
  },
  changeCouponName(){
    couponStore.getState().changeName()
  }
})

React 框架中使用

  1. 创建 store:创建的 store 是一个 hook,你可以放任何东西到里面(基础变量,对象、函数),状态必须不可改变地更新,set 函数合并状态以实现状态更新
  2. store 绑定组件:可以在任何地方使用钩子,不需要提供 provider,基于 selector 获取业务 state,组件将在状态更改时重新渲染
js 复制代码
import React from 'react'
import { create } from 'zustand'

const useDateStore = create((set) => ({
  date: {
    current: new Date().toLocaleDateString()
  },
  changeTime: () => set((state) => ({ date: {
    ...state.date,
    current: new Date().toLocaleString()
  } })),
  changeTimeDelay: async () => {
    await new Promise(resolve => {
      setTimeout(() => {
        resolve()
      }, 10000)
    })
    set((state) => ({ date: {
      ...state.date,
      current: new Date().toLocaleString()
    } }))
  }
}))

function ZustandIndex() {
  const date = useDateStore(state => state.date)
  const changeTime = useDateStore(state => state.changeTime)
  const changeTimeDelay = useDateStore(state => state.changeTimeDelay)
  return (
    <div>
     <div>当前时间: {date.current}</div>
     <div onClick={changeTime}>点击更新时间</div>
     <div onClick={changeTimeDelay}>点击延迟10s更新时间</div>
    </div>
  );
}

export default ZustandIndex;

使用中间件

zustand 官方提供了六个中间件:

  • immer:给 set 加入 immer 的功能
  • persist:用于持久化存储状态,存储到例如 localStorage、IndexedDB 等,当应用重新加载时可以从存储引擎中恢复状态。
  • redux:利用 reduxdispatch\reducer 方式编写,通过这个中间件可以很方便的把 useReducer 或者 redux 管理的状态迁移到 zustand
  • combine: 合并 state
  • devtools: 利用开发者工具 调试/追踪 Store
  • subscribeWithSelector: 让我们把 selector 用在 subscribe 函数上
js 复制代码
import { immer } from 'zustand/middleware/immer'
const useDateStore = create(immer((...)))

Zustand 的实现

createStore

暴露五个 api

  • setState:修改 state,遍历订阅列表,执行订阅函数
  • getState:获取当前 state
  • getInitialState:获取初始化 state
  • subscribe:添加订阅函数
  • destroy:清空全部订阅函数
js 复制代码
const createStoreImpl = (createState) => {
  let state
  // 创建一个Set结构来维护订阅者。
  const listeners = new Set()
 // 定义更新数据的方法,partial参数支持对象和函数,replace指的是全量替换store还是merge
  // 如果是partial对象时,则直接赋值,否则将上一次的数据作为参数执行该方法。
  // 然后利用Object.is进行新老数据的浅比较,如果前后发生了改变,则进行替换
  // 并且遍历订阅者,逐一进行更新。
  const setState = (partial, replace) => {
    const nextState =
      typeof partial === 'function'
        ? partial(state)
        : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      state =
        replace ?? (typeof nextState !== 'object' || nextState === null)
          ? nextState
          : Object.assign({}, state, nextState)
      listeners.forEach((listener) => listener(state, previousState))
    }
  }
 // getState方法返回当前Store里的最新数据
  const getState = () => state

  const getInitialState = () => initialState
 // 添加订阅方法,并且返回一个取消订阅的方法
  const subscribe = (listener) => {
    listeners.add(listener)
    // Unsubscribe
    return () => listeners.delete(listener)
  }
 // 清空全部订阅函数
  const destroy = () => {
   // ...省略环境判断
    listeners.clear()
  }
  const api = { setState, getState, getInitialState, subscribe, destroy }
  const initialState = (state = createState(setState, getState, api))
  return api
}

create

  1. 先调用上文 createStore 方法生成 store
  2. 再利用 useSyncExternalStoreWithSelector 方法对 react 进行集成
js 复制代码
// 对React进行集成
export function useStore(api, selector, equalityFn) {
  // 利用useSyncExternalStoreWithSelector,对store里的所有数据进行选择性的分片
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector,
    equalityFn
  )
  useDebugValue(slice)
  return slice
}
useSyncExternalStoreWithSelector

useSyncExternalStoreWithSelectoruseSyncExternalStore 指定选择器优化版,useSyncExternalStorereact18 新增的特性,所以 react 团队发布了这个向后兼容的包 use-sync-external-store/shim,以便 18 以前的版本也可以使用(当然要大于 16.8 版本),主要功能是订阅外部 storehook

useSyncExternalStoreWithSelector 接收五个参数:

  • subscribe:外部 store 的订阅方法
  • getSnapshot:相当于 getState
  • getServerSnapshot:返回服务端渲染期间使用的 state
  • selector:返回指定状态的 selector 函数,比如 state 上有个 data 数据,只想得到 data,就可以使用 state => state.data
  • equalFn:对比函数,决定是否更新

工作机制:

  • useSyncExternalStore 是当 store 的状态改变时,订阅函数会执行,此时 react 会调用 getSnapshot 和之前的状态快照比较(通过 Object.is 比较),如果状态发生改变,组件会重新渲染
  • useSyncExternalStoreWithSelectoruseSyncExternalStore 的基础上增加了 selectorisEqual,可以减少 re-render 的次数,也能缓存 state
    • store 不变的情况下,重复调用 getSnapshot 返回同一个值
js 复制代码
function useSyncExternalStoreWithSelector(
  subscribe,
  getSnapshot,
  getServerSnapshot,
  selector,
  isEqual,
) {
  const instRef = useRef(null);
  let inst;
  if (instRef.current === null) {
    inst = {
      hasValue: false,
      value: null,
    };
    instRef.current = inst;
  } else {
    inst = instRef.current;
  }
   /**

- 每次re-render都会获得一个新的selector
- 所以getSelection在re-render后都是新的,但是因为有instRef.current以及isEqual
- 当isEqual的时候返回instRef.current缓存的值,也就是getSelection的返回值不变
- 不会再次re-render,减少了re-render的次数
    */
  const [getSelection, getServerSelection] = useMemo(() => {
    let hasMemo = false;
    let memoizedSnapshot;
    let memoizedSelection;
    const memoizedSelector = (nextSnapshot) => {
      if (!hasMemo) {
        // 第一次调用hook时,没有缓存
        hasMemo = true;
        memoizedSnapshot = nextSnapshot;
        const nextSelection = selector(nextSnapshot);
        // 需要用户自己提供isEqual
        if (isEqual !== undefined) {
          if (inst.hasValue) {
            const currentSelection = inst.value;
            if (isEqual(currentSelection, nextSelection)) {
              memoizedSelection = currentSelection;
              return currentSelection;
            }
          }
        }
        memoizedSelection = nextSelection;
        return nextSelection;
      }

      const prevSnapshot = memoizedSnapshot;
      const prevSelection = memoizedSelection;

      if (is(prevSnapshot, nextSnapshot)) {
        // 快照与上次相同,重复使用之前的结果
        return prevSelection;
      }

      // 快照已更改,需要获取新的快照
      const nextSelection = selector(nextSnapshot);

      // 如果提供了自定义 isEqual 函数,会使用它来检查数据是否,已经改变。
      // 如果未改变,返回之前的结果,React会退出渲染
      if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
        return prevSelection;
      }

      memoizedSnapshot = nextSnapshot;
      memoizedSelection = nextSelection;
      return nextSelection;
    };
    const maybeGetServerSnapshot =
      getServerSnapshot === undefined ? null : getServerSnapshot;
    const getSnapshotWithSelector = () => memoizedSelector(getSnapshot());
    const getServerSnapshotWithSelector =
      maybeGetServerSnapshot === null
        ? undefined
        : () => memoizedSelector(maybeGetServerSnapshot());
    return [getSnapshotWithSelector, getServerSnapshotWithSelector];
  }, [getSnapshot, getServerSnapshot, selector, isEqual]);

  const value = useSyncExternalStore(
    subscribe,
    getSelection,
    getServerSelection,
  );

  useEffect(() => {
    inst.hasValue = true;
    inst.value = value;
  }, [value]);

  useDebugValue(value);
  return value;
}

中间件

特点:

  • zustand 的中间件实际上是一个高阶函数,它的入参和 create 函数相同,本质上是对 create 时传入的初始化 config 做了一层包裹,注入特定的逻辑
  • zustand 核心源码中并没有发现任何和中间件有关的代码
  • redux 一样,本质还是利用函数组合

来看下 redux 中间件源码

js 复制代码
export const redux = ( reducer, initial ) => ( set, get, api ) => {
  api.dispatch = action => {
    set(state => reducer(state, action), false, action)
    return action
  }
  api.dispatchFromDevtools = true
  return { dispatch: (...a) => api.dispatch(...a), ...initial }
}

// 使用:create(redux(reducer, initialState))

// 自定义一个中间件,记录状态更新
const middleware1 = (config) => ( set, get, api ) => config((args) => {
  console.log("  applying", args);
  set(args);
  console.log("  new state", get());
}, get, api)

Valtio

简介

Valtio 是什么?

  • Valtio 是一个基于 Proxy 实现,更容易上手的状态管理工具
  • 双向数据绑定
  • 发布订阅模式

Valtio 的工作流程:

  • 使用 proxy 拦截对象,得到 state
  • 使用 useSnapshot 获取 state,返回一个不可变的 snapshot
  • 操作 proxy state 获取新的 snapshot,触发组件 rerender

Valtio 适用于哪些场景?

  • 上手简单:使用体验和 mobx 基本一致
  • 适用于需要简单的自动更新的场景

Valtio 的特点

  • 容易使用和理解:没有复杂的概念,只有两个核心方法 proxyuseSnapshot
    • proxy 函数创建状态代理对象
    • useSnapshot 获取状态快照
  • 细粒度渲染:使用 useSnapshot 可以只在状态变化的部分触发组件的重新渲染

Valtio 的使用

单独使用 Valtio

要点:

  1. 使用 proxy 拦截 state
  2. 使用 snapshot 获取一个不可变对象
  3. 使用 subscribe 订阅 state 改变

举个在小程序中使用的例子

js 复制代码
import {proxy, snapshot, subscribe} from 'valtio'

const dateState = proxy({
  current: new Date().toLocaleString()
})

const changeTime = () => {
  dateState.current = new Date().toLocaleString()
}

const changeTimeDelay = async () => {
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, 10000)
  })
  changeTime()
}

Page({
  data: {date: {}},
  onLoad() {
    const fn = () => {
      const date = snapshot(dateState)
      this.setData({date})
    }
    fn()
    subscribe(dateState, () => {
      fn()
    })
  },
  changeTime,
  changeTimeDelay
})

React 框架中使用 Valtio

js 复制代码
import React from "react";
import {proxy, useSnapshot} from 'valtio'

const dateState = proxy({
  time: new Date().toLocaleString()
})

const changeTime = () => {
  dateState.time = new Date().toLocaleString()
}

const changeTimeDelay = async () => {
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, 10000)
  })
  changeTime()
}

const ValtioIndex = () => {
  const date = useSnapshot(dateState)
  return (
    <div>
      <div>当前时间: {date.time}</div>
      <div onClick={changeTime}>点击更新时间</div>
      <div onClick={changeTimeDelay}>点击延迟10s更新时间</div>
    </div>
  )
  
}

export default ValtioIndex

Valtio 的实现

proxy

要点:

  • 最终会调用函数 proxyFunction
  • 内部会用到两个关键的数据结构 proxyStateMapproxyCache
    • proxyStateMap: 跟踪和管理代理对象与原始对象之间的映射关系。在代理对象创建时建立映射,并在需要时提供从代理对象到原始对象或从原始对象到代理对象的查询功能
    • proxyCache:缓存已经创建的代理对象,以避免同一个原始对象被多次创建代理对象,目的是提高性能和避免不必要的内存消耗
js 复制代码
proxyFunction = (initialObject) => {
    // ...省略校验
   // proxyCache用来存储已经创建的代理对象
    const found = proxyCache.get(initialObject)
    // 如果cache中有直接返回
    if (found) {
      return found
    }
    let version = versionHolder[0]
    const listeners = new Set()
    // 通知更新
    const notifyUpdate = (op, nextVersion = ++versionHolder[0]) => {
      if (version !== nextVersion) {
        version = nextVersion
        listeners.forEach((listener) => listener(op, nextVersion))
      }
    }
    let checkVersion = versionHolder[1]
    // 确保代理对象和其versionHolder的版本匹配,确保依赖关系在需要时得到正确的通知
    const ensureVersion = (nextCheckVersion = ++versionHolder[1]) => {
      if (checkVersion !== nextCheckVersion && !listeners.size) {
        checkVersion = nextCheckVersion
        propProxyStates.forEach(([propProxyState]) => {
          const propVersion = propProxyState[1](nextCheckVersion)
          if (propVersion > version) {
            version = propVersion
          }
        })
      }
      return version
    }
    // 创建属性监听
    const createPropListener =
      (prop) =>
      (op, nextVersion) => {
        const newOp = [...op]
        newOp[1] = [prop, ...(newOp[1])]
        notifyUpdate(newOp, nextVersion)
      }
    const propProxyStates = new Map()
    // 向代理对象添加属性监听器,用于监听某个属性的变化,并在变化时触发指定的回调函数
    const addPropListener = (prop, propProxyState) => {
      // ... 省略环境判断代码
      if (listeners.size) {
        // 有listener,设置remove
        const remove = propProxyState[3](createPropListener(prop))
        propProxyStates.set(prop, [propProxyState, remove])
      } else {
        propProxyStates.set(prop, [propProxyState])
      }
    }
    // 移除属性监听
    const removePropListener = (prop) => {
      const entry = propProxyStates.get(prop)
      if (entry) {
        propProxyStates.delete(prop)
        entry[1]?.()
      }
    }
    // 返回移除监听函数
    const addListener = (listener) => {
      listeners.add(listener)
      //当有listener时,遍历propProxyStates,加上remove
      if (listeners.size === 1) {
        propProxyStates.forEach(([propProxyState, prevRemove], prop) => {
          // ... 省略环境判断代码
          const remove = propProxyState[3](createPropListener(prop))
          propProxyStates.set(prop, [propProxyState, remove])
        })
      }
      // 移除监听
      const removeListener = () => {
        listeners.delete(listener)
        if (listeners.size === 0) {
          propProxyStates.forEach(([propProxyState, remove], prop) => {
            if (remove) {
              remove()
              propProxyStates.set(prop, [propProxyState])
            }
          })
        }
      }
      return removeListener
    }
    // 一个新对象
    const baseObject = Array.isArray(initialObject)
      ? []
      : Object.create(Object.getPrototypeOf(initialObject))
    const handler = {
      // 删除属性
      deleteProperty(target, prop) {
        const prevValue = Reflect.get(target, prop)
        removePropListener(prop)
        const deleted = Reflect.deleteProperty(target, prop)
        if (deleted) {
          notifyUpdate(['delete', [prop], prevValue])
        }
        return deleted
      },
      // 修改属性
      set(target, prop, value, receiver) {
        const hasPrevValue = Reflect.has(target, prop)
        const prevValue = Reflect.get(target, prop, receiver)
        if (
          hasPrevValue &&
          (objectIs(prevValue, value) ||
            (proxyCache.has(value) &&
              objectIs(prevValue, proxyCache.get(value))))
        ) {
          return true
        }
        removePropListener(prop)
        if (isObject(value)) {
          value = getUntracked(value) || value
        }
        let nextValue = value
        // 处理异步
        if (value instanceof Promise) {
          value
            .then((v) => {
              value.status = 'fulfilled'
              value.value = v
              notifyUpdate(['resolve', [prop], v])
            })
            .catch((e) => {
              value.status = 'rejected'
              value.reason = e
              notifyUpdate(['reject', [prop], e])
            })
        } else {
          // 新值是个可代理对象
          if (!proxyStateMap.has(value) && canProxy(value)) {
            nextValue = proxyFunction(value)
          }
          const childProxyState =
            !refSet.has(nextValue) && proxyStateMap.get(nextValue)
          if (childProxyState) {
            addPropListener(prop, childProxyState)
          }
        }
        Reflect.set(target, prop, nextValue, receiver)
        notifyUpdate(['set', [prop], value, prevValue])
        return true
      },
    }
    // newProxy -> new Proxy(target, handler)
    const proxyObject = newProxy(baseObject, handler)
    proxyCache.set(initialObject, proxyObject)
    const proxyState = [
      baseObject,
      ensureVersion,
      createSnapshot,
      addListener,
    ]
    proxyStateMap.set(proxyObject, proxyState)
    Reflect.ownKeys(initialObject).forEach((key) => {
      const desc = Object.getOwnPropertyDescriptor(
        initialObject,
        key
      )
      if ('value' in desc) {
        proxyObject[key as keyof T] = initialObject[key as keyof T]
        delete desc.value
        delete desc.writable
      }
      Object.defineProperty(baseObject, key, desc)
    })
    return proxyObject
  }

snapshot

要点:

  • 返回一个只读不可变的对象,本质是调用 createSnapshot 函数
  • 如何实现对象不可变的?
    • preventExtensions 阻止对象修改
    • defineProperty writable 属性默认为 false
js 复制代码
  createSnapshot = (target, version, handlePromise = defaultHandlePromise) => {
    const cache = snapCache.get(target)
    if (cache?.[0] === version) {
      return cache[1]
    }
    const snap = Array.isArray(target)
      ? []
      : Object.create(Object.getPrototypeOf(target))
    markToTrack(snap, true) // mark to track
    snapCache.set(target, [version, snap])
    Reflect.ownKeys(target).forEach((key) => {
      if (Object.getOwnPropertyDescriptor(snap, key)) {
        // Only the known case is Array.length so far.
        return
      }
      const value = Reflect.get(target, key)
      const { enumerable } = Reflect.getOwnPropertyDescriptor(
        target,
        key,
      )
      const desc = {
        value,
        enumerable: enumerable,
        configurable: true,
      }
      if (refSet.has(value)) {
        markToTrack(value, false) // mark not to track
      } else if (value instanceof Promise) {
        delete desc.value
        desc.get = () => handlePromise(value)
      } else if (proxyStateMap.has(value)) {
        const [target, ensureVersion] = proxyStateMap.get(value)
        desc.value = createSnapshot(
          target,
          ensureVersion(),
          handlePromise,
        )
      }
      Object.defineProperty(snap, key, desc)
    })
    return Object.preventExtensions(snap)
  },

useSnapshot

只能在 react 框架中使用,因为它使用了 reactuseSyncExternalStore,关于 useSyncExternalStore 参见 zustand 一节

作用:

  • snapshot 一样,都是用来获取不可变 state
  • 区别是,snapshot 是只要代理对象或者子代理对象改变都会创建,而 useSnapshot 是在组件里调用的,只有当组件重新渲染时才会重新创建
js 复制代码
export function useSnapshot(proxyObject, options) {
  const notifyInSync = options?.sync
  const lastSnapshot = useRef()
  const lastAffected = useRef()
  let inRender = true
  const currSnapshot = useSyncExternalStore(
    useCallback(
      (callback) => {
        const unsub = subscribe(proxyObject, callback, notifyInSync)
        callback()
        return unsub
      },
      [proxyObject, notifyInSync],
    ),
    () => {
      const nextSnapshot = snapshot(proxyObject, use)
      try {
        if (
          !inRender &&
          lastSnapshot.current &&
          lastAffected.current &&
          !isChanged(
            lastSnapshot.current,
            nextSnapshot,
            lastAffected.current,
            new WeakMap(),
          )
        ) {
          // not changed
          return lastSnapshot.current
        }
      } catch (e) {
        // ignore if a promise or something is thrown
      }
      return nextSnapshot
    },
    () => snapshot(proxyObject, use),
  )
  inRender = false
  const currAffected = new WeakMap()
  useEffect(() => {
    lastSnapshot.current = currSnapshot
    lastAffected.current = currAffected
  })
  if (import.meta.env?.MODE !== 'production') {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useAffectedDebugValue(currSnapshot, currAffected)
  }
  const proxyCache = useMemo(() => new WeakMap(), []) // per-hook proxyCache
  // 创建proxy,先查 cache, 没有降级到 new Proxy
  return createProxyToCompare(
    currSnapshot,
    currAffected,
    proxyCache,
    targetCache,
  )
}

订阅 subscribe

js 复制代码
export function subscribe(proxyObject, callback, notifyInSync) {
  const proxyState = proxyStateMap.get(proxyObject)
  // ...省略环境判断
  let promise
  const ops = []
  //定义一个addListener,用来在代理状态中添加监听器,以便在代理对象发生更改时调用listener
  const addListener = proxyState[3]
  let isListenerActive = false
  const listener = (op) => {
    ops.push(op)
    if (notifyInSync) {
      callback(ops.splice(0))
      return
    }
    if (!promise) {
      promise = Promise.resolve().then(() => {
        promise = undefined
        if (isListenerActive) {
          callback(ops.splice(0))
        }
      })
    }
  }
  const removeListener = addListener(listener)
  isListenerActive = true
  return () => {
    isListenerActive = false
    removeListener()
  }
}

Rematch

简介

是什么?

来自官方:Rematch 是没有样板文件的 Redux 最佳实践,没有多余的 action types,action creators,switch 语句或者 thunks

个人理解:对 Redux 框架的重封装,使得 Redux 更易用

RematchRedux 的对比:

  • Rematch 移除了 Redux 中的这些东西:
    • 声明 action 类型
    • action 创建函数
    • thunks
    • store 配置
    • mapDispatchToProps
    • sagas

Rematch的工作流程:

  • 首先,用户通过 view 组件调用 dispatch 触发 reducer action
  • reducer action 会去修改 state,并返回新的 state
  • state 改变触发组件重新渲染

Rematch 的特点:

  1. 合理 的数据结构设计,rematch 使用 model 的概念,整合了 state, reducer 以及 effect
  2. 移除了 redux 中大量的 action.type 常量以及分支判断
  3. 更简洁的 API 设计,rematch 使用的是基于对象的配置项,更加易于上手
  4. 更少的代码
  5. 原生语法支持异步,无需使用中间件。
  6. 提供插件机制,可以进行定制开发

Rematch的使用

单独使用 Rematch

举个在小程序中使用的例子:

  1. 定义 model
js 复制代码
const gift = {
  name: 'gift',
  state: {
    name: '',
    time: 3000,
    price: 99,
  },
  reducers: {
    changeName(state) {
      return {
        ...state,
        name: `${new Date().getTime()}`
      }
    }
  },
  // 定义异步action
  effects: {
    async changeNameDelay() {
      await new Promise(resolve => setTimeout(() => {
        resolve()
      }, 10000))
      this.changeName()
    }
  }
}

export default gift
  1. 初始化 store
js 复制代码
import { init } from '@rematch/core';
import count from './models/count';
import gift from './models/gift';

const store = init({
  models: {
    count,
    gift
  }
})

export default store
  1. dispatch action & 更新 view
js 复制代码
import store from '../../store/rematch'

Page({
  data: {gift: {}},
  onLoad() {
    const fn = () => {
      const {gift} = store.getState()
      console.log('>>>reducer', gift)
      this.setData({
        gift
      })
    }
    fn()
    store.subscribe(() => {
      console.log('>>change')
      fn()
    })
  },
  changeName() {
    store.dispatch.gift.changeName()
  },
  changeNameDelay() {
    store.dispatch.gift.changeNameDelay()
  }
})

React 框架中使用 Rematch

是否在 react 框架中使用只有视图层的区别,在 react 框架中使用时,仍旧可以使用现成的 react-redux 库包裹组件

js 复制代码
import React, { useEffect, useState } from 'react'
import { Provider, connect } from 'react-redux'
import store from './store/rematch'

const mapStateToProps = (state, ownProps) => {
  return {
    gift: state.gift
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    changeName: dispatch.gift.changeName,
    changeNameDelay: dispatch.gift.changeNameDelay
  }
}

const UserCard = (props) => {
  const {gift, changeName, changeNameDelay} = props
  return (
    <div className="App">
        <div>
          <div>礼物名字是{gift.name || 'xxx'}</div>
          <div>展示时长{gift.time || ''}</div>
          <div onClick={changeName}>点击改变名字</div>
          <div onClick={changeNameDelay}>点击延迟10s改变名字</div>
        </div>
      </div>
  )
}
const UserCardRematch = connect(mapStateToProps, mapDispatchToProps)(UserCard)

function App() {
  return (
    <Provider store={store}>
      <UserCardRematch />
    </Provider>
  );
}

Rematch 中使用插件

插件可以扩展 Rematch 功能,重写配置,添加新的 model,甚至替换整个 store。内置的的 dispatcheffects 也都是插件,分别用来增强 dispatch 和处理异步操作。除此之外,Rematch 还开发了不少第三方插件,如:

  • @rematch/immer: 对于一个复杂的对象,immer 会复用没有改变的部分,仅仅替换修改了的部分,相比于深拷贝,可以大大的减少开销
  • @rematch/select:给 Rematch 使用的 selectors 插件
    • Model 中增加了一个 selectors 属性,同时导出了一个 select 函数,挂在 RematchStore
    • Selector 主要用于封装从 state 中查找特定值的逻辑、派生数据的逻辑以及通过避免不必要的重新计算来提高性能
  • @rematch/loading:对每个 model 中的 effects 自动添加 loading 状态
  • @rematch/updated:用于在触发 effects 时维护时间戳,主要用来 throttle effects
  • @rematch/persist:使用 local storage 选项提供简单的 redux 状态持久化。
    • React,PersistGate 一起使用,在等待数据从 storage 中异步加载的同时显示 loading 指示器
  • @rematch/typed-state:在运行时进行类型检查
    • 使用 prop-types 描述类型

举个例子,如何使用 @rematch/immer 插件

js 复制代码
import { init } from '@rematch/core';
import immerPlugin from '@rematch/immer'
import count from './models/count';
import gift from './models/gift';

const immer = immerPlugin()
const store = init({
  models: {
    count,
    gift
  },
  plugins: [immer]
})

export default store

// model.js
  reducers: {
    changeName(state) {
      state.name = `${new Date().getTime()}`
      return state
    }
  }

Rematch 的实现

从上面的使用方式可以看出,核心是使用 init 生成 store,在调用 store.dispatch.{modelName}.{reducerName}

你是否在学习 rematch 的时候,有这几个问题:

  1. Rematch 是如何减少模板代码的,即如何自动生成 actionType 的?
  2. 既然没有改写Redux,那么 Rematch 如何和 Redux 结合的?
  3. Rematch 如何处理异步 action
  4. 插件机制如何实现?

带着问题,让我们一起看下 rematch 源码

init

主要有两步:

  1. createConfig 生成配置对象

    • 生成一个自增的 name,如果没有传入 name,就使用这个自增的 name
  2. createRematchStore 生成最后的 rematchStore, rematchStore 既包含了 redux.store 原本的方法,又在它的基础上做了扩展

    • model 中的 reducer、effects 绑定到 dispatch
    • 调用 dispatch 时会自动拼装 action.name
    • 对于传入 Rematch 的插件,会在 Rematch store 的初始化的各个阶段去调用生命周期钩子。forEachPlugin 是插件机制的核心,通过这个方法,执行插件钩子函数
js 复制代码
export const init = (
 initConfig
) => {
  // 根据传入的参数,生成最后的配置对象
 const config = createConfig(initConfig || {})
 return createRematchStore(config)
}
export default function createConfig(initConfig) {
 const storeName = initConfig.name ?? `Rematch Store ${count}`
 count += 1
 const config = {
  name: storeName,
  models: initConfig.models || {},
  plugins: initConfig.plugins || [],
  redux: {
   reducers: {},
   rootReducers: {},
   enhancers: [],
   middlewares: [],
   ...initConfig.redux,
   devtoolOptions: {
    name: storeName,
    ...(initConfig.redux?.devtoolOptions ?? {}),
   },
  },
 }
  // 验证config参数是否合法
 validateConfig(config)
 // Apply changes to the config required by plugins
  // ...省略配置插件部分代码
 config.plugins.forEach((plugin) => {
  // ...
 })
 return config
}

function createRematchStore(config) {
 // setup rematch 'bag' for storing useful values and functions
 const bag = createRematchBag(config)
 // add middleware for handling effects
 bag.reduxConfig.middlewares.push(createEffectsMiddleware(bag))
 // collect middlewares from plugins
 bag.forEachPlugin('createMiddleware', (createMiddleware) => {
  bag.reduxConfig.middlewares.push(createMiddleware(bag))
 })
 const reduxStore = createReduxStore(bag)
 let rematchStore = {
  ...reduxStore,
  name: config.name,
  addModel(model) {
   validateModel(model)
   createModelReducer(bag, model)
   prepareModel(rematchStore, model)
   enhanceModel(rematchStore, bag, model)
   reduxStore.replaceReducer(createRootReducer(bag))
   reduxStore.dispatch({ type: '@@redux/REPLACE' })
  },
 }

 addExposed(rematchStore, config.plugins)
 bag.models.forEach((model) => prepareModel(rematchStore, model))
 bag.models.forEach((model) => enhanceModel(rematchStore, bag, model))
 bag.forEachPlugin('onStoreCreated', (onStoreCreated) => {
  rematchStore = onStoreCreated(rematchStore, bag) || rematchStore
 })
 return rematchStore
}
createRematchStore

主流程:

  1. 调用 createRematchBag,createRematchBag 会返回一个包含 models、reduxConfig、forEachPlugin 的对象。
    • models: [{name, reducers, ...}]
    • reduxConfigredux 配置相关
    • forEachPlugin: 传入两个参数,methodfn,如果 config.plugins 能找到 method,则执行 fn(config.plugins[method])
  2. createEffectsMiddleware:添加处理 effects 的中间件,下面会详细介绍
  3. forEachPlugin('createMiddleware', cb)createMiddleware 来自 Rematch 提供的插件 plugins/typed-state,如没有配置则不会执行
    • createMiddleware 函数的返回值又是一个 Redux 的中间件
  4. createReduxStore:核心,根据传入的配置,创建 redux store
  5. prepareModel:往最终返回的 store 对象暴露 dispatch 钩子
  6. onStoreCreated:调用插件的 onStoreCreated 钩子,@rematch/persist 插件的
createEffectsMiddleware

其实就是一个 effectsRedux 中间件

js 复制代码
function createEffectsMiddleware(bag) {
  // store,next,action分别对应redux的store,dispatch,action
    return store => next => action => {
        if (action.type in bag.effects) {
            next(action)
            return bag.effects[action.type](
                action.payload,
                store.getState(),
                action.meta,
            )
        }
        return next(action)
    }
}
createReduxStore

流程:

  1. 遍历 models 执行 createModelReducer
  2. 创建 rootReducer
  3. 应用中间件
  4. 生成 enhancers
  5. 生成初始化 state,initialState
  6. 调用 reduxcreateStore,传入 2,4,5 步生成的参数创建最终的 store

createModelReducer

通过这个函数,我们可以看到,rematch 是怎么无需定义 action type

js 复制代码
function createModelReducer(bag, model) {
   const modelReducers = {}
    const modelReducerKeys = Object.keys(model.reducers)
    modelReducerKeys.forEach((reducerKey) => {
      // 如果reducerkey含'/',直接使用reducerkey作为actionName,否则使用`${model.name}/${reducerKey}`作为action name
        const actionName = isAlreadyActionName(reducerKey)
            ? reducerKey
            : `${model.name}/${reducerKey}`

        modelReducers[actionName] = model.reducers[reducerKey]
    })

   // 如果当前的 action 存在于 model 的reducer 中,则直接执行这个 reducer,否则则返回 state
    const combinedReducer = (state, action) => {
        if (action.type in modelReducers) {
            return modelReducers[action.type](state, action.payload, action.meta)
        }
        return state
    }
    const modelBaseReducer = model.baseReducer
  // Rematch 可以和原有的 redux 的reducer 同时存在,且 Rematch.reducer > Redux.reducer
    let reducer = !modelBaseReducer
        ? combinedReducer
        : (state = model.state, action) =>
            combinedReducer(modelBaseReducer(state, action), action)
  // 如果插件中配置了 onReducer 事件,则依次执行
    bag.forEachPlugin('onReducer', (onReducer) => {
        reducer = onReducer(reducer, model.name, bag) || reducer
    })

    bag.reduxConfig.reducers[model.name] = reducer
}

生成 enhancers

  • 如果 init 时传入了 reduxdevtoolComposer,则使用传入的
  • 未传入的话,会先判断以下三个条件是否满足:
    • 处于浏览器环境
    • 安装了 Redux Devtools
    • 未禁用 Redux Devtools 满足之后,如果初始时传入 devtoolOptions,则会开启 Redux Devtools
js 复制代码
 const enhancers = bag.reduxConfig.devtoolComposer
  ? bag.reduxConfig.devtoolComposer(...bag.reduxConfig.enhancers, middlewares)
  : composeEnhancersWithDevtools(bag.reduxConfig.devtoolOptions)(
    ...bag.reduxConfig.enhancers,
    middlewares
    )
  
  function composeEnhancersWithDevtools(
    devtoolOptions = {}
) {
    return !devtoolOptions.disabled &&
        typeof window === 'object' &&
        window.**REDUX_DEVTOOLS_EXTENSION_COMPOSE**
        ? window.**REDUX_DEVTOOLS_EXTENSION_COMPOSE**(devtoolOptions)
        : Redux.compose
}
prepareModel

rematch 派发 action 的时候是使用 store.dispatch.{modelName}.{reducerName or effectsName}prepareModel 就是实现这一步的

具体流程:

  1. 创建一个空对象
  2. model.name 作为 key,在 dispatch 上绑定这个空对象
  3. 遍历 model 上的所有 reducer,通过 createActionDispatcheractionDispatcher 作为 value 绑定上去
js 复制代码
function prepareModel(rematchStore, bag, model) {
    const modelDispatcher = {}
    rematchStore.dispatch[`${model.name}`] = modelDispatcher
    createDispatcher(rematchStore, bag, model)
    bag.forEachPlugin('onModel', (onModel) => {
        onModel(model, rematchStore)
    })
}

function createDispatcher(rematchStore, bag, model) {
    const modelDispatcher = rematch.dispatch[model.name]
    const modelReducersKeys = Object.keys(model.reducers)
    modelReducersKeys.forEach((reducerName) => {
        validateModelReducer(model.name, model.reducers, reducerName)
        modelDispatcher[reducerName] = createActionDispatcher(
            rematch,
            model.name,
            reducerName,
            false
        )
    })

    if (model.effects) {
        effects =
            typeof model.effects === 'function'
                ? model.effects(rematch.dispatch)
                : model.effects
    }
    const effectKeys = Object.keys(effects)
    effectKeys.forEach((effectName) => {
        validateModelEffect(model.name, effects, effectName)
        bag.effects[`${model.name}/${effectName}`] = effects[effectName].bind(
            modelDispatcher
        )
        modelDispatcher[effectName] = createActionDispatcher(
            rematch,
            model.name,
            effectName,
            true
        )
    })
}

const createActionDispatcher = (
 rematch,
 modelName,
 actionName,
 isEffect
)=> {
 return Object.assign(
  (payload, meta)  => {
   const action = { type: `${modelName}/${actionName}` }
   if (typeof payload !== 'undefined') {
    action.payload = payload
   }
   if (typeof meta !== 'undefined') {
    action.meta = meta
   }
   return rematch.dispatch(action)
  },
  {
   isEffect,
  }
 )
}

结论

  • 问题一:Rematch 是如何减少模板代码的,即如何自动生成 actionType 的?

    createModelReducer 时,根据 reducer key,如果 reducer key 包含 '/',则将 reducer key 作为 action type,否则将{modelname}/{reducer key} 作为 action type

  • 问题二:既然没有改写 Redux,那么 Rematch 如何和 Redux 结合的?

    根据传入的 config 参数重组,最后生成 redux createStore 所需的参数,本质上还是创建 redux store,只不过经过一层封装

  • 问题三:Rematch 如何处理异步 action

    createEffectsMiddleware 用来处理异步 action 也就是 effects,本质上还是创建 redux 的异步中间件

  • 问题四:插件机制如何实现?

    init 时根据传入的 plugin 配置生成 config 对象,创建 rematch store 时,通过 forEachPlugin 处理 plugin 的钩子函数

对比(省流篇)

redux rematch mobx zustand valtio
star 60.3k 8.5k 27.1k 41.2k 8.3k
大小(压缩前) 290k 312k 4.19M 327k 312k
诞生时间 2011 2018 2018 2019 2021
最近更新时间 3月前 2年前 4月前 17天前 17天前
原理 Flux思想 发布订阅模式 对redux的二次封装 观察者模式 基于数据代理 Flux思想 观察者模式 基于数据代理 数据双向绑定 发布订阅模式
优点 1. 通用的状态解决方案 2. 单一数据源,使得数据发生改变时更容易追踪 3. 生态系统完善 4. 函数式编程,在 reducer 中,接受输入,然后输出,不会有副作用发生,幂等性 1. 更合理的数据结构设计,rematch 使用 model 的概念,整合了 state, reducer 以及 effect 2. 移除了 redux 中大量的 action.type 常量以及分支判断 3. 更简洁的 API 设计,rematch 使用的是基于对象的配置项,更加易于上手 4. 更少的代码 5. 原生语法支持异步,无需使用中间件。 6. 提供插件机制,可以进行定制开发 1. 使用简单,上手门槛低 2. 通用的状态解决方案 3. 支持计算属性 1. 不需要使用 context provider 包裹你的应用程序 2. 可以做到瞬时更新(不引起组件渲染完成更新过程) 3. 不依赖 react 上下文,引用更加灵活 4. 当状态发生变化时 重新渲染的组件更少 5. 集中的、基于操作的状态管理 6. 基于不可变状态进行更新, store 更新操作相对更加可控 1. 容易使用和理解:没有复杂的概念,只有两个核心方法 proxy 和 useSnapshot 2. 细粒度渲染:使用 useSnapshot 可以只在状态变化的部分触发组件的重新渲染
缺点 1. 学习成本高,需要学习dispatch,action,reducer的概念 2. 使用起来比较复杂,需要定义大量的action 3. 在非react框架中使用时,默认只要store的任一属性发生改变,都会执行全部的订阅函数(通过store.subscription订阅的函数),业务在使用时,需要自己实现diff更新 1. 在非react框架中使用时,需要使用subscribe去订阅store的更新,但是只要store任一属性发生改变,都会引起更新,造成不必要的性能损耗。可以参照react-redux实现二次封装组件 1.可变状态模型,某些情况下可能影响调试 2. 体积较大 3. 在非react框架中使用时,默认只要观测值的任一属性发生改变,都会执行autorun,在使用时,需要二次封装,进行优化,减少性能消耗 4. 太过灵活,更容易导致 bug 1. 框架本身不支持 computed 属性,但可基于 middleware 机制通过少量代码间接实现 computed 1.可变状态模型,某些情况下可能影响调试
学习成本
使用成本
异步支持 借助中间件 友好 友好 友好 友好
易于调试
性能 中等 中等 中等
Typescript 友好 支持 支持 支持 支持 支持

参考文档

  1. tech.meituan.com/2017/07/14/...
  2. juejin.cn/post/684490...
  3. cn.redux.js.org/tutorials/f...
  4. zhuanlan.zhihu.com/p/465917281
  5. yingchenit.github.io/react/redux...
  6. github.com/Aaaaash/blo...
  7. zhuanlan.zhihu.com/p/80655889
  8. zhenhua-lee.github.io/react/redux...
  9. juejin.cn/post/718760...
  10. zhuanlan.zhihu.com/p/157176365
  11. juejin.cn/post/703747...
  12. mp.weixin.qq.com/s/d5Cuo9skg...
  13. www.mobxjs.com/reactions
  14. github.com/yinguangyao...
  15. juejin.cn/post/719551...
  16. juejin.cn/post/722324...
  17. github.com/yinguangyao...
  18. rematchjs.org/docs/
  19. awesomedevin.github.io/zustand-vue...
  20. github.com/ascoders/we...
相关推荐
太阳花ˉ3 分钟前
html+css+js实现step进度条效果
javascript·css·html
小白学习日记1 小时前
【复习】HTML常用标签<table>
前端·html
john_hjy1 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd1 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo2 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
DOKE2 小时前
VSCode终端:提升命令行使用体验
前端
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试