状态管理
为什么 jQuery
时代,我们并没有谈论状态管理,但是在 React
,Vue
的时代,我们日常开发,基本离不开一些状态管理库?
jQuery
是针对 "过程" 的命令式编程,不会去管理数据。而现代前端框架则是把对 "过程" 的各种命令,变为了对 "状态" 的描述。因此前端开发也就从组合各式各样的命令完成用户界面更新和交互,变成了以状态驱动web界面变化的状态机式开发方式
什么是状态?
状态就是
UI
中的动态数据。对于React
框架而言,就是state
。React
框架的核心思想是UI = f(state)
,即「UI
是state
的投影」,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
和收到的Action
,Reducer
会返回新的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-redux
和 redux
的关系图:
总结一下 React-Redux
做了哪些事情(简化版,详细版参见文档 ):
- 包装渲染函数
- 对外只提供了一个
Provider
和connect
的方法,隐藏了关于store
操作的很多细节 Provider
接受store
作为参数,并且通过context
把store
传给所有的子组件- 子组件通过
connect
包裹了一层高阶组件,高阶组件会通过context
结合mapStateToProps
和store
,把数据传给被包裹的组件
- 对外只提供了一个
- 避免没有必要的渲染
- 缓存上次的计算的
props
,然后用新的props
和旧的props
进行对比,如果两者相同,就不调用render
- 缓存上次的计算的
- 更新机制
- 本质还是使用
redux
的store.subscribe
进行更新订阅,store.dispatch
进行更新派发 connect
包裹组件时,会通过react-redux
内部的subscription
去调用redux
的store.subscribe
注册监听- 任意一个组件
dispatch
了一次,所有组件的更新函数都要被batch
执行,所有connect
包裹的组件都会去判断是否真正需要执行更新
- 本质还是使用
使用示例:
// 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
- 使用
createStore
创建全局store
:createStore
返回一个对象,包含dispatch、subscribe、getState、replaceReducer
等方法 store.dispatch(action)
:派发action
,组合action
和当前state
,获取到最新state
数据,遍历收集的订阅函数store.subscribe(listener)
:订阅监听函数,存放在数组中,store.dispatch(action)
时遍历执行。store.getState()
:获取当前store
的数据
举个在小程序中使用的例子:
// 省略全局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
:入参函数放入监听队列,返回取消订阅函数
getState
和 replaceReducer
函数比较简单,不再展开
2. store.dispatch
- 调用
Reducer
,传参(currentState,action
) - 按顺序执行
listener
- 返回
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
先看下中间件如何在代码里使用
// 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)))
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
的增强。
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)))
中间件一般是【自己实现一个中间件】一节中的格式的函数,这样的话整个执行链路就是这样的:
h(store.dispatch)
,此时,next = store.dispatch
,返回一个 【action => {}
】 这样的函数g(h(store.dispatch))
,next = h(store.dispatch)
,返回一个 【action => {}
】 这样的函数f(g(h(store.dispatch)))
,next = g(h(store.dispatch))
,返回一个 【action => {}
】 这样的函数- 当业务代码中调用
dispatch
的时候- 如果派发的
action
不是一个函数,执行next(action)
,会依次执行g(h(store.dispatch))-> h(store.dispatch)->store.dispatch(action)
- 如果派发的
action
是一个函数,执行action(dispacth, getState)
,全局的store.getState
,和store
的dispatch
作为参数传递给业务侧定义的action
函数,如changeUserNameAsync
,最终执行dispatch(action)
,等价于使用store.dispatch
派发action
- 如果派发的
自己实现一个中间件
// 可以实现异步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
的工作流程
- 事件触发了
Actions
,Reactions
可以经过事件调用Actions
Actions
作为唯一修改State
的方式,修改了State
State
的修改更新了计算值Computed
- 计算值的改变引起了
Reactions
的改变
Mobx
原则:
- 所有的
derivations
将在state
改变时自动且原子化地更新。因此不可能观察中间值。 - 所有的
derivations
默认将会同步更新,这意味着action
可以在state
改变之后安全的直接获得computed
值。 computed value
的更新是惰性的,任何computed value
在需要他们的副作用发生之前都是不激活的。- 所有的
computed value
都应是纯函数,他们不应该修改state
。
Mobx
适用于哪些场景?
- 首先
Mobx
和Redux
一样都是状态管理库,所以Redux
的使用场景同样适用于mobx
mobx
比较容易上手,适用于需要快速迭代的小项目- 如果你之前已经习惯开发
vue
,因为mobx
和vue
都是响应式编程,所以使用mobx
会更顺手一些
Mobx
的使用
单独使用 mobx
-
定义一个可观察的状态
const userStore = observable({ userInfo: { nickName: 'xxx', gender: '', age: 0 } })
-
使用
autorun
定义响应函数。autorun
函数接受一个函数作为参数,每当该函数所观察的值发生变化时,它都会运行。autorun(() => { const nameBox = document.querySelector('#nameBox') nameBox.innerHTML = `${userStore.userInfo.nickName || ''}` })
-
action
:使用action
定义函数。通过action
显式地修改状态,使得状态的变化可预测(状态的变化能定位到是哪个action
引起的)。此外,action
函数是事务型的,通过action
修改状态时,响应函数不会立即执行,而是等到action
结束后才执行,这有助于提升性能。const userStore = observable({ changeName: action(function() { this.userInfo.nickName = `${new Date().getTime()}` }) }) const clicBox = document.querySelector('#nameClick') clicBox.addEventListener('click', () => { userStore.changeName() })
-
computed
:computed
属性有两个特性。使用属性.get()
获取其值- 被缓存:每当读取
computed
值时,如果其依赖的状态或其他computed
值未发生变化,则使用上次的缓存结果,以减少计算开销,对于复杂的computed
值,缓存可以大大提高性能 - 惰性计算:只有
computed
值被使用时才重新计算值。反言之,即使computed
值依赖的状态发生了变化,但是它暂时没有被使用,那么它不会重新计算。
const userStore = observable({ age: computed(function() { return Math.floor(Math.random() * 200) }) }) autorun(() => { console.log('>>>>', userStore.age.get()) }) // 因为age没有依赖状态,所以这里其实每次打印都会是同一个值
- 被缓存:每当读取
在 React
框架中使用
在 React
框架中使用时,可以借助 mobx-react
或 mobx-react-lite
(轻量级)框架
mobx-react
相对于 mobx
,主要增加了以下几点:
- 提供了
provider
和inject
,方便管理多store
- 提供了一个
observer
方法, 它是一个高阶组件,它接收React
组件并返回一个新的React
组件,返回的新组件能响应(通过observable
定义的)状态的变化
使用方式:
-
定义
store
通过provider
注入:多store
的话,可以聚合成一个store
import UserStore from "./userStore" import CountStore from "./countStore" export default { userStore: new UserStore(), countStore: new CountStore() }
-
在根组件中通过
Provider
注入store
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;
-
在
React
组件中使用Store
:利用observer
,让React
组件响应store
的变化
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
实现)
简单总结:
- 用
Object.defineProperty
或者Proxy
来拦截observable
包装的对象属性的get/set
- 在
autorun
或者reaction
执行的时候,会触发依赖状态的get
,此时将autorun
里面的函数和依赖的状态关联起来。也就是我们常说的依赖收集。 - 当修改状态的时候会触发
set
,此时会通知前面关联的函数,重新执行他们
mobx
的源码相比于 redux
真的很晦涩
只介绍依赖收集 observable
和响应更新 autorun
,其他不再介绍
observable
mobx
有多种定义观测对象的方式,有 makeAutoObservable
,observable
,makeObservable
,但无论是使用哪个 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
中更容易检查
- 如果你想把一个对象转化为可观察对象,而这个对象具有一个常规结构,其中所有的成员都是事先已知的,建议使用
ComputedValue
和 ObservableValue
的区别:
ComputedValue
既是观察者,也是被观察者,它有自己的依赖,同时又是别人的依赖,所以ComputedValue
中既有ObservableValue
的特性,又有Reaction
的特性。computed
装饰器是用来装饰get
方法的,它将这个方法包装为ComputedValue
,在进行get
操作的时候,会判断依赖项是否发生变化,进行方法的执行,判断值是否发生变化,通知观察者自身发生了变化。- 某个
ObservableValue
值发生改变后,会调用观察者的onBecomeStale
方法,表明这个观察者的依赖发生变更,如果这个观察者是ComputedValue
,那么这个观察者的值并没有发生改变,而是当调用到ComputedValue
的时候,才重新计算这个值。 computed
和autorun
进行依赖收集的方式一模一样,都是借助全局变量globalState.trackingDerivation
完成的。
对于 object
数据类型而言,核心是调用 asObservableObject
,该函数的调用流程是:asObservableObject -> ObservableObjectAdministration
make(Auto)Observable
:ObservableObjectAdministration.make_-> annotation.make_
observable
:ObservableObjectAdministration.extend_-> defineObservableProperty_
observable
核心:
observable
的目的是将正常的对象变成 ObservableValue
,adm
是管理 ObservableValue
的,decorator
和 enhance
用于配置 adm
。将每个属性都要转变为 ObservableValue
后,我们对属性的 set
和 get
其实都是对 adm
的 write
和 read
。
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
核心流程:
- 创建一个
Reaction
实例 - 将响应函数
view
先包裹一层track
函数,并绑定到Reaction
内部的onInvalidate_
- 通过
reaction.schedule_()
调度执行reaction.schedule_()
会先将这个Reaction
实例放入globalState.pendingReactions
- 执行
runReactions->runReactionsHelper
- 在
runReactionsHelper
里面会遍历我们的pendingReactions
数组,执行里面的reaction
实例的runReaction_
方法 - 执行
runReaction_
时,就会执行onInvalidate_
,也就是track(view)
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
函数
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
不再详细展开
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
的使用
单独使用
不在 react
或 vue
框架中使用的话,要使用 zustand/vanilla
库
以下使用示例为在小程序中的使用例子
- 创建
store
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
}
- 在
view
中使用
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
框架中使用
- 创建
store
:创建的store
是一个hook
,你可以放任何东西到里面(基础变量,对象、函数),状态必须不可改变地更新,set
函数合并状态以实现状态更新 store
绑定组件:可以在任何地方使用钩子,不需要提供provider
,基于selector
获取业务state
,组件将在状态更改时重新渲染
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
:利用redux
的dispatch\reducer
方式编写,通过这个中间件可以很方便的把useReducer
或者redux
管理的状态迁移到zustand
中combine
: 合并state
devtools
: 利用开发者工具 调试/追踪Store
subscribeWithSelector
: 让我们把selector
用在subscribe
函数上
import { immer } from 'zustand/middleware/immer'
const useDateStore = create(immer((...)))
Zustand
的实现
createStore
暴露五个 api
:
setState
:修改state
,遍历订阅列表,执行订阅函数getState
:获取当前state
getInitialState
:获取初始化state
subscribe
:添加订阅函数destroy
:清空全部订阅函数
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
- 先调用上文
createStore
方法生成store
- 再利用
useSyncExternalStoreWithSelector
方法对react
进行集成
// 对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
useSyncExternalStoreWithSelector
是 useSyncExternalStore
指定选择器优化版,useSyncExternalStore
是 react18
新增的特性,所以 react
团队发布了这个向后兼容的包 use-sync-external-store/shim
,以便 18
以前的版本也可以使用(当然要大于 16.8
版本),主要功能是订阅外部 store
的 hook
useSyncExternalStoreWithSelector
接收五个参数:
subscribe
:外部store
的订阅方法getSnapshot
:相当于getState
getServerSnapshot
:返回服务端渲染期间使用的state
selector
:返回指定状态的selector
函数,比如state
上有个data
数据,只想得到data
,就可以使用state => state.data
equalFn
:对比函数,决定是否更新
工作机制:
useSyncExternalStore
是当store
的状态改变时,订阅函数会执行,此时react
会调用getSnapshot
和之前的状态快照比较(通过Object.is
比较),如果状态发生改变,组件会重新渲染useSyncExternalStoreWithSelector
在useSyncExternalStore
的基础上增加了selector
和isEqual
,可以减少re-render
的次数,也能缓存state
- 在
store
不变的情况下,重复调用getSnapshot
返回同一个值
- 在
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
中间件源码
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
的特点
- 容易使用和理解:没有复杂的概念,只有两个核心方法
proxy
和useSnapshot
proxy
函数创建状态代理对象useSnapshot
获取状态快照
- 细粒度渲染:使用
useSnapshot
可以只在状态变化的部分触发组件的重新渲染
Valtio
的使用
单独使用 Valtio
要点:
- 使用
proxy
拦截state
- 使用
snapshot
获取一个不可变对象 - 使用
subscribe
订阅state
改变
举个在小程序中使用的例子
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
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
- 内部会用到两个关键的数据结构
proxyStateMap
和proxyCache
:proxyStateMap
: 跟踪和管理代理对象与原始对象之间的映射关系。在代理对象创建时建立映射,并在需要时提供从代理对象到原始对象或从原始对象到代理对象的查询功能proxyCache
:缓存已经创建的代理对象,以避免同一个原始对象被多次创建代理对象,目的是提高性能和避免不必要的内存消耗
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
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
框架中使用,因为它使用了 react
的 useSyncExternalStore
,关于 useSyncExternalStore
参见 zustand
一节
作用:
- 跟
snapshot
一样,都是用来获取不可变state
的 - 区别是,
snapshot
是只要代理对象或者子代理对象改变都会创建,而useSnapshot
是在组件里调用的,只有当组件重新渲染时才会重新创建
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
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
更易用
Rematch
和 Redux
的对比:
Rematch
移除了Redux
中的这些东西:- 声明
action
类型 action
创建函数thunks
store
配置mapDispatchToProps
sagas
- 声明
Rematch
的工作流程:
- 首先,用户通过
view
组件调用dispatch
触发reducer action
reducer action
会去修改state
,并返回新的state
state
改变触发组件重新渲染
Rematch
的特点:
- 更合理的数据结构设计,
rematch
使用model
的概念,整合了state, reducer
以及effect
- 移除了
redux
中大量的action.type
常量以及分支判断 - 更简洁的
API
设计,rematch
使用的是基于对象的配置项,更加易于上手 - 更少的代码
- 原生语法支持异步,无需使用中间件。
- 提供插件机制,可以进行定制开发
Rematch
的使用
单独使用 Rematch
举个在小程序中使用的例子:
- 定义
model
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
- 初始化
store
import { init } from '@rematch/core';
import count from './models/count';
import gift from './models/gift';
const store = init({
models: {
count,
gift
}
})
export default store
dispatch action
& 更新view
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
库包裹组件
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
。内置的的 dispatch
和 effects
也都是插件,分别用来增强 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
插件
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
的时候,有这几个问题:
Rematch
是如何减少模板代码的,即如何自动生成actionType
的?- 既然没有改写
Redux
,那么Rematch
如何和Redux
结合的? Rematch
如何处理异步action
?- 插件机制如何实现?
带着问题,让我们一起看下 rematch
源码
init
主要有两步:
-
createConfig
生成配置对象- 生成一个自增的
name
,如果没有传入name
,就使用这个自增的name
- 生成一个自增的
-
createRematchStore
生成最后的rematchStore
,rematchStore
既包含了redux.store
原本的方法,又在它的基础上做了扩展model
中的reducer、effects
绑定到dispatch
上- 调用
dispatch
时会自动拼装action.name
- 对于传入
Rematch
的插件,会在Rematch store
的初始化的各个阶段去调用生命周期钩子。forEachPlugin
是插件机制的核心,通过这个方法,执行插件钩子函数
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
主流程:
- 调用
createRematchBag,createRematchBag
会返回一个包含models、reduxConfig、forEachPlugin
的对象。models: [{name, reducers, ...}]
reduxConfig
:redux
配置相关forEachPlugin
: 传入两个参数,method
和fn
,如果config.plugins
能找到method
,则执行fn(config.plugins[method])
createEffectsMiddleware
:添加处理effects
的中间件,下面会详细介绍forEachPlugin('createMiddleware', cb)
:createMiddleware
来自Rematch
提供的插件plugins/typed-state
,如没有配置则不会执行createMiddleware
函数的返回值又是一个Redux
的中间件
createReduxStore
:核心,根据传入的配置,创建redux store
prepareModel
:往最终返回的store
对象暴露dispatch
钩子onStoreCreated
:调用插件的onStoreCreated
钩子,@rematch/persist
插件的
createEffectsMiddleware
其实就是一个 effects
的 Redux
中间件
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
流程:
- 遍历
models
执行createModelReducer
- 创建
rootReducer
- 应用中间件
- 生成
enhancers
- 生成初始化
state,initialState
- 调用
redux
的createStore
,传入2,4,5
步生成的参数创建最终的store
createModelReducer
通过这个函数,我们可以看到,rematch
是怎么无需定义 action type
的
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
时传入了redux
的devtoolComposer
,则使用传入的 - 未传入的话,会先判断以下三个条件是否满足:
- 处于浏览器环境
- 安装了
Redux Devtools
- 未禁用
Redux Devtools
满足之后,如果初始时传入devtoolOptions
,则会开启Redux Devtools
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
就是实现这一步的
具体流程:
- 创建一个空对象
- 将
model.name
作为key
,在dispatch
上绑定这个空对象 - 遍历
model
上的所有reducer
,通过createActionDispatcher
将actionDispatcher
作为value
绑定上去
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 友好 | 支持 | 支持 | 支持 | 支持 | 支持 |
参考文档
- tech.meituan.com/2017/07/14/…
- juejin.cn/post/684490…
- cn.redux.js.org/tutorials/f…
- zhuanlan.zhihu.com/p/465917281
- yingchenit.github.io/react/redux…
- github.com/Aaaaash/blo…
- zhuanlan.zhihu.com/p/80655889
- zhenhua-lee.github.io/react/redux…
- juejin.cn/post/718760…
- zhuanlan.zhihu.com/p/157176365
- juejin.cn/post/703747…
- mp.weixin.qq.com/s/d5Cuo9skg…
- www.mobxjs.com/reactions
- github.com/yinguangyao…
- juejin.cn/post/719551…
- juejin.cn/post/722324…
- github.com/yinguangyao…
- rematchjs.org/docs/
- awesomedevin.github.io/zustand-vue…
- github.com/ascoders/we…