redux
那样在最外层包裹一层高阶组件,只绑定对应关联组件即可(当在其他组件/方法修改状态后,该组件会自动更新)hook
组件使用、组件外使用middleware
拓展能力(redux
、devtools
、combine
、persist
)创建 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 }
创建 store
拿到对外暴露唯一接口 useStore
,定义全局状态。
通过 const bears = useStore(state => state.bears)
获取状态并与组件绑定。
store
会执行 subscribe(listener)
添加订阅操作,同时该方法内置有 forceUpdate()
函数用于触发组件更新。使用 set
钩子函数修改状态。
setState
方法,该方法会执行 listeners.forEach((listener) => listener(state, previousState))
通知所有订阅者执行更新。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>
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>
React
非常相似的简单 API
,它的API
像React
的useState
和Context API
的组合useRecoilState
的调用,Recoil
可以跟踪哪些组件使用了哪些原子。这样它就可以在数据发生变化时,只重新渲染那些 "订阅 "某项数据的组件,所以这种方法在性能方面应该可以很好地扩展。Redux
一样需要在最外层提供类似Context Provider
包裹的方式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>
// 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> ); }
对Redux
的包装与再次封装,核心原理依然是redux
官方文档
React状态管理库及如何选择?
React 全局状态管理器 redux, mobx, react-immut 等横向对比