react状态管理器-zustand

特性

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

先来看看用法

创建 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状态管理器-zustand | 小帅の技术博客 (ssscode.com)

参考资料

官方文档

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容