zustand状态管理器与观察者模式

6,032 阅读8分钟

zustand状态管理器 | 小帅の技术博客 (ssscode.com)

特性

  1. 不需要像redux那样在最外层包裹一层高阶组件,只绑定对应关联组件即可(当在其他组件/方法修改状态后,该组件会自动更新)
  2. 异步处理也较为简单,与普通函数用法相同
  3. 支持hook组件使用、组件外使用
  4. 提供middleware拓展能力(reduxdevtoolscombinepersist
  5. 可通过 github.com/mweststrate… 拓展能力(实现嵌套更新、日志打印)

先来看看用法

创建 store

// store
import create from 'zustand'

// 通过 create 方法创建一个具有响应式的 store
const useStore = create(set => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })), // 函数写法
  removeAllBears: () => set({ bears: 0 }) // 对象写法
}))

组件引用

// UI 组件,展示 bears 状态,当状态变更时可实现组件同步更新
function BearCounter() {
  const bears = useStore(state => state.bears)
  return <h1>{bears} around here ...</h1>
}

// 控制组件,通过 store 内部创建的 increasePopulation 方法执行点击事件,可触发数据和UI组件更新
function Controls() {
  const increasePopulation = useStore(state => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

组件外使用

import useStore from './index';

// const { getState, setState, subscribe, destroy } = store

export const sleep = (timeout: number) => {
  // 1. 获取方法 执行逻辑
  const { setLoading } = useStore.getState();
  // 2. 直接通过 setState 修改状态
  // useStore.setState({ loading: false });

  return new Promise((resolve) => {
    setLoading(true);
    setTimeout(() => {
      setLoading(false);
      resolve(true);
    }, timeout);
  });
};

结合官方示例,可以确定 zustand 内部对通过 state 绑定的组件,默认添加注册到了订阅者队列,此时该 bears 属性相当于一个被观察者,当 bears 状态变更后,通知所有订阅了该数属性的组件进行更新。(我们可以大致推测一下这个 set 方法)

废话不多说,看代码,我们先按照创建 store 的逻辑分析:

create 接受一个函数(非函数情况暂时不研究),返回我们定义的状态和方法,且该函数是提供 set 方法供我们使用的,而这个 set 方法必定是可以触发更新通知的。

原理分析

通过窥探源码可知实现原理为:观察者模式

想深入研究观察者模式的可以看我的这篇文章:发布订阅模式vs观察者模式

直接上代码

  • create 方法
function create(createState) {
  // 初始化处理 createState
  const api = typeof createState === "function" ? createImpl(createState) : createState
}
  • 这里引入了一个 createImpl 方法,我们先看下这个方法对 createState 的处理和返回值。
function createImpl(createState) {
  // 用于缓存上一次的 状态
  let state
  // 监听队列
  const listeners = new Set()

  const setState = (partial, replace) => {
    // 如果是 function 注入 state 并获取执行结果,否则直接取值
    // 例如:setCount: ()=> set(state=> ({state: state.count +1 })
    // 例如:setCount: ()=> set({count: 10})
    const nextState = typeof partial === "function" ? partial(state) : partial
    // 优化:判断状态是否变化了,再更新组件状态
    if (nextState !== state) {
      // 上一次状态
      const previousState = state
      // 当前状态最新状态
      state = replace ? nextState : Object.assign({}, state, nextState)
      // 通知队列中的每一个组件
      listeners.forEach((listener) => listener(state, previousState))
    }
  }

  // 函数获取 state
  const getState = () => state

  // 存在 selector 或 equalityFn 参数时,对订阅方法进行处理
  const subscribeWithSelector = (listener, selector = getState, equalityFn = Object.is) => {
    // 当前拿到的值
    let currentSlice = selector(state)
    // 实际添加到队列的是 listenerToAdd 方法,
    function listenerToAdd() {
      // 订阅通知执行时的值,即 下一次更新的值
      const nextSlice = selector(state)
      // 对比前后值不相等,则触发更新通知
      if (!equalityFn(currentSlice, nextSlice)) {
        // 上一次值
        const previousSlice = currentSlice
        // 执行添加的订阅函数
        // 例如:useStore.subscribe(console.log, state => state.paw)
        // 中的 console.log
        listener((currentSlice = nextSlice), previousSlice)
      }
    }
    // add listenerToAdd
    listeners.add(listenerToAdd)
    // Unsubscribe
    return () => listeners.delete(listenerToAdd)
  }

  // 添加订阅 
  // 列如:useStore.subscribe(console.log, state => state.paw)
  // 效果:只监听 paw 的变化,通知更新
  const subscribe = (listener, selector, equalityFn) => {
    // selector 或 equalityFn 参数存在,走该逻辑,添加指定的订阅通知
    if (selector || equalityFn) {
      return subscribeWithSelector(listener, selector, equalityFn)
    }
    // 否则 对所有变更添加订阅通知
    listeners.add(listener)
    // Unsubscribe
    // 执行结果为删除该订阅者函数
    // 即:const unsubscribe= subscribe() = () => listeners.delete(listener)
    return () => listeners.delete(listener)
  }

  // 清除 订阅
  const destroy = () => listeners.clear()
  // 返回给 create 方法的处理结果,即返回了 4 个处理方法
  const api = { setState, getState, subscribe, destroy }
  // 其对传入的 createState 函数注入了3个参数 setState, getState, api 
  // 使得在 create 创建 store时,可以在回调函数的参数里取用方法对数据进行处理
  // 如:create(set=> ({count: 0,setCount: ()=> set(state=> ({state: state.count +1 }))}))
  // 并调用然后返回 api = { setState, getState, subscribe, destroy } 属性方法
  state = createState(setState, getState, api)

  return api
}

可以得到 createImpl 的执行结果

const api = { setState, getState, subscribe, destroy }

然后我们再回来继续往下分析 create 方法

  • 简单介绍下代码中使用的 useEffect / useLayoutEffect 区别

    • useEffect 是异步执行的,而 useLayoutEffect 是同步执行的。
    • useEffect 的执行时机是浏览器完成渲染之后,而 useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前,和 componentDidMount 等价。
  • create 方法

import { useReducer, useLayoutEffect, useRef } from "react"

// 是否为非浏览器环境
const isSSR =
  typeof window === "undefined" ||
  !window.navigator ||
  /ServerSideRendering|^Deno\//.test(window.navigator.userAgent)

// useEffect 可以在服务端(NodeJs)执行,而 useLayoutEffect 不行
const useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect

export default function create(createState) {
  const api = typeof createState === "function" ? createImpl(createState) : createState

  // 返回 useStore 函数供外部使用
  // 闭包使得 api 作为执行上下文,供 useStore 内部使用,保证数据隔离
  const useStore = (selector, equalityFn = Object.is) => {
    // 用于触发组件更新
    const [, forceUpdate] = useReducer((c) => c + 1, 0)

    // 获取 state
    const state = api.getState()
    // 把 state 挂载到 useRef,避免副作用对其进行影响而更新
    const stateRef = useRef(state)
    // 挂载指定 selector 方法到 useRef
    // 列如:const bears = useStore(state => state.bears)
    const selectorRef = useRef(selector)
    // 等值方法
    const equalityFnRef = useRef(equalityFn)
    // 标记错误
    const erroredRef = useRef(false)

    // 当前 state 属性(state.bears)
    const currentSliceRef = useRef()
    // 空值处理
    if (currentSliceRef.current === undefined) {
      currentSliceRef.current = selector(state)
    }

    let newStateSlice
    let hasNewStateSlice = false

    // The selector or equalityFn need to be called during the render phase if
    // they change. We also want legitimate errors to be visible so we re-run
    // them if they errored in the subscriber.
    if (
      stateRef.current !== state ||
      selectorRef.current !== selector ||
      equalityFnRef.current !== equalityFn ||
      erroredRef.current
    ) {
      // Using local variables to avoid mutations in the render phase.
      newStateSlice = selector(state)
      // 新旧值是否相等
      hasNewStateSlice = !equalityFn(currentSliceRef.current, newStateSlice)
    }

    // Syncing changes in useEffect.
    useIsomorphicLayoutEffect(() => {
      if (hasNewStateSlice) {
        currentSliceRef.current = newStateSlice
      }
      stateRef.current = state
      selectorRef.current = selector
      equalityFnRef.current = equalityFn
      erroredRef.current = false
    })

    // 暂存 state
    const stateBeforeSubscriptionRef = useRef(state)
    // 初始化
    useIsomorphicLayoutEffect(() => {
      const listener = () => {
        try {
          // 触发更新时的最新获取 state
          const nextState = api.getState()
          // 注入 nextState 执行传入的 selector 方法,获取值,即 state.bears
          const nextStateSlice = selectorRef.current(nextState)
          // 对比不相等 ==> 更新
          if (!equalityFnRef.current(currentSliceRef.current, nextStateSlice)) {
            // 更新 stateRef 为最新 state
            stateRef.current = nextState
            // 更新 currentSliceRef 为最新属性值,即 state.bears
            currentSliceRef.current = nextStateSlice
            // 更新组件
            forceUpdate()
          }
        } catch (error) {
          // 登记错误
          erroredRef.current = true
          // 更新组件
          forceUpdate()
        }
      }
      // 添加 listener 订阅
      const unsubscribe = api.subscribe(listener)
      // state已经变更,通知更新
      if (api.getState() !== stateBeforeSubscriptionRef.current) {
        listener() // state has changed before subscription
      }
      // 卸载时 清除订阅
      return unsubscribe
    }, [])

    return hasNewStateSlice ? newStateSlice : currentSliceRef.current
  }

  // 合并 api 属性到 useStore
  Object.assign(useStore, api)

  // 闭包暴露唯一 方法供外部使用
  return useStore
}

简单总结下

  1. 创建 store 拿到对外暴露唯一接口 useStore ,定义全局状态。

  2. 通过 const bears = useStore(state => state.bears) 获取状态并与组件绑定。

    • 这一步 store 会执行 subscribe(listener) 添加订阅操作,同时该方法内置有 forceUpdate() 函数用于触发组件更新。
  3. 使用 set 钩子函数修改状态。

    • 即调用的 setState 方法,该方法会执行 listeners.forEach((listener) => listener(state, previousState)) 通知所有订阅者执行更新。

第三方状态管理库

Redux

  • 核心原理:reducer 纯函数
  • 使用 Context API
  • 遵循的是函数式(如函数式编程)的风格
  • 单一的全局存储来保存应用程序的所有状态
  • 更改只通过动作发生
  • bundle size 小(redux+react-redux约为3kb
function reducer(state = { name: null }, action) {
  switch(action.type) {
    case 'CHANGE_NAME':
      return { ...state, name: action.data }
  }
}

const store = createStore(reducer)

<Provider store={store}>
  ...
</Provider>

MobX

  • 核心原理:ES6 proxy (可以理解为vue的双向数据绑定)
  • MobX是基于观察者/可观察模式的。
  • 以真正的 "反应式 "方式管理状态,因此当你修改一个值时,任何使用该值的组件都会自动重新渲染。
  • 不需要任何动作或者reducers,只需修改你的状态,应用程序就会反映出来。
  • 要求使用ES6代理,意味着不支持IE11及以下版本。(或者旧版本)
class Store {
  @observable
  name = null

  @action.bound
  setName(name) {
    this.name = name // 类似vue this.name='' 即可触发更新监听
  }
}

const store = new Store()

<Provider store={store}>
  ...
</Provider>

Recoil

  • React 非常相似的简单 API,它的APIReactuseStateContext API的组合
  • 通过跟踪对useRecoilState的调用,Recoil可以跟踪哪些组件使用了哪些原子。这样它就可以在数据发生变化时,只重新渲染那些 "订阅 "某项数据的组件,所以这种方法在性能方面应该可以很好地扩展。
  • Redux一样需要在最外层提供类似Context Provider包裹的方式
  • 该库较新,存在未知的错误

Constate

  • 基于hook
function defineStore() {
  const [state, setState] = useState({ name: null })
  const setName = name => setState(state => {
    return { ...state, name }
  })
  return { state, setName }
}

const [Provider, useStore] = constate(defineStore)

<Provider>
  ...
</Provider>

Concent

// api
const storeConf = {
  store: {},
  reducer: {},
  ghost: {},
  watch: {},
  computed: {},
  lifecycle: {},
};
// 创建store子模块
import { run } from 'concent';

run({ 
  counter: {
    state: {
      name:'concent',
      firstName:'',
      lastName:'',
      age:0,
      hobbies:[]
    }
  }
});

// 注册成为Concent Class组件,指定其属于 counter 模块
import React, { Component } from 'react';
import { register } from 'concent';

@register('counter')
class HelloConcent extends Component {
  state = { name: 'this value will been overwrite by counter module state' }
  render() {
    const { name, age, hobbies } = this.state;
    return (
      <div>
        name: {name}
        age: {age}
        hobbies: {hobbies.map((v, idx) => <span key={idx}>{v}</span>)}
      </div>
    );
  }
}

// 函数式组件
import { useConcent } from 'concent';

function CounterFnComp() {
  const { state, setState } = useConcent('counter');
  return (
    <div>
      count: {state.count}
      <button onClick={() => setState({count: state.count+1})}>inc</button>
      <button onClick={() => setState({count: state.count-1})}>dec</button>
    </div>
  );
}

Dva

Redux的包装与再次封装,核心原理依然是redux

参考资料

官方文档

React状态管理库及如何选择?

React 全局状态管理器 redux, mobx, react-immut 等横向对比