特性
- 不需要像
redux
那样在最外层包裹一层高阶组件,只绑定对应关联组件即可(当在其他组件/方法修改状态后,该组件会自动更新) - 异步处理也较为简单,与普通函数用法相同
- 支持
hook
组件使用、组件外使用 - 提供
middleware
拓展能力(redux
、devtools
、combine
、persist
) - 可通过 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 {bears} around here ...
}
// 控制组件,通过 store 内部创建的 increasePopulation 方法执行点击事件,可触发数据和UI组件更新
function Controls() {
const increasePopulation = useStore(state => state.increasePopulation)
return
}
组件外使用
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
}
简单总结下
创建
store
拿到对外暴露唯一接口useStore
,定义全局状态。-
通过
const bears = useStore(state => state.bears)
获取状态并与组件绑定。- 这一步
store
会执行subscribe(listener)
添加订阅操作,同时该方法内置有forceUpdate()
函数用于触发组件更新。
- 这一步
-
使用
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)
...
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()
...
Recoil
- 与
React
非常相似的简单API
,它的API
像React
的useState
和Context 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)
...
Concent
- 文档: https://concentjs.github.io/concent-doc/
- Github:https://github.com/concentjs/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 (
name: {name}
age: {age}
hobbies: {hobbies.map((v, idx) => {v})}
);
}
}
// 函数式组件
import { useConcent } from 'concent';
function CounterFnComp() {
const { state, setState } = useConcent('counter');
return (
count: {state.count}
);
}
Dva
对Redux
的包装与再次封装,核心原理依然是redux
地址
原文博客:react状态管理器-zustand | 小帅の技术博客 (ssscode.com)
参考资料
官方文档
React状态管理库及如何选择?
React 全局状态管理器 redux, mobx, react-immut 等横向对比