React状态管理(一)—— Redux
前言
Redux是Dan Abramov在2015年发布,是React生态里最火的状态管理库,其主要有四个特性:
- 可预测
- reducer是纯函数,所以状态是可预测的
- 中心化
- 全局只有一个store
- 易调试
- action --> change state
- 灵活性
- middleware机制 他的源码十分的简洁,但是其扩展的生态却十分丰富,设计思想非常🐂,下面让我们一起来学习。
设计思想
要了解Redux的设计思想,首先看看React的设计思想——单向数据流,看下图:
State描述的就是当前用户的状态,View是根据当前State渲染出来的,根据View层响应不用的Action,Action改变State,重新渲染view层。
Redux的设计思想就是:
- 应用是一个状态机,视图和状态是一一对应的。
- 把所有的State都集中管理,让整个UI和整个状态都能有对应的管理。
基本概念
Store
Store数据保存的地方,也可以理解为一个容器,全局只能有一个store。
const store = createStore(reducer, initialValue)
const store2 = createStore(reducer, enhancer)
const store2 = createStore(recuder, initialvalue, enhancer)
State
某个时刻Store数据的快照就叫State。
const state = store.getState()
Action
改变State的唯一方式,他有一定格式规范:
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
Action creater 就是创建Action的工厂函数。
function createAction(payload){
return {
type: 'ADD_TODO',
payload,
}
}
Dispatch
View层发出Action的唯一方式。
store.dispatch(action);
Reducer
Reducer是一个纯函数,他接受Action和当前state为参数,这意味着他会返回一个全新的state,即要求数据流不可变性。
这里要说一下纯函数的定义:
- 相同的输入总是返回相同的输出。
- 不能有副作用。
纯函数、不可变性其实都是函数式编程的术语,JS本身不是一门函数的语言,但是他可以实现纯函数的特性。
function recuder(state, action) {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
return state;
}
}
store.subscribe
订阅数据变化。一旦state发生改变,执行回调。
显然我们可以在这里实现自动渲染。
store.subscribe(() => {
const state = store.getState()
console.log(state)
render(state)
})
总结
源码解析
源码实现
源码简单实现Demo:codesandbox.io/s/thirsty-f…
-
createStore
- 发布订阅模式
- 把state传递给listener,需要自己调用store.getState()。为什么?性能优化
-
combineReducers
- namespace
-
applyMiddleware
- compose
- 中间件机制
- 拓展灵活性的关键
Redux-react
我们一般说使用redux其实是指的是在react中使用redux-react,前者是跟任何框架无关的状态管理库,后者是将redux和react联系起来的关键。
redux在每次数据更新的时候,都会调用订阅数据更新的回调。
我们当然可以像之前的Demo那样,在每次数据更新的时候,重新渲染整个React组件。
store.subscribe(() => {
render()
});
但是这样每次都全量渲染性能肯定是不好的。
可以看到Redux本身是一个功能非常简单的状态管理库,一些性能优化的方法都是没有的。
版本更新历史
简单过一下更新历史:
4.x
- connect组件里面判断是否需要更新
5.x
- 解决了"zombie child" bugs
- 5.0实现了自己的一套Subscription,嵌套的子组件总是订阅最近的父节点。
- 所有的更新逻辑被移除到组件外面了。
6.x
- v5依赖componentWillReceiveProps
- 完全依赖new context api提供的渲染能力
- 性能有问题
7.x
- 5的性能
- Store 传入 prop
- 增加hooks api
- connect实现用函数组件实现了
- React.memo 提升性能
- unstable_batchedUpdates() 实现了api来提升性能。
- Hooks api
connect分析
Connect的心智模型:
function connect(mapStateToProps, mapDispatchToProps) {
// It lets us inject component as the last step so people can use it as a decorator.
// Generally you don't need to worry about it.
return function (WrappedComponent) {
// It returns a component
return class extends React.Component {
render() {
return (
// that renders your component
<WrappedComponent
{/* with its props */}
{...this.props}
{/* and additional props calculated from Redux store */}
{...mapStateToProps(store.getState(), this.props)}
{...mapDispatchToProps(store.dispatch, this.props)}
/>
)
}
componentDidMount() {
// it remembers to subscribe to the store so it doesn't miss updates
this.unsubscribe = store.subscribe(this.handleChange.bind(this))
}
componentWillUnmount() {
// and unsubscribe later
this.unsubscribe()
}
handleChange() {
// and whenever the store state changes, it re-renders.
this.forceUpdate()
}
}
}
}
这样的实现可以保证每次数据,更新都可以重新渲染组件,但这不够。
传递给connect
组件的参数(通过mapStateToProps
and mapDispatchToProps
生成的对象)的浅对比。
但是有一种情况是这样的:
const mapStateToProps = state => {
return {
objects: state.objectIds.map(id => state.objects[id])
}
}
reselect性能优化
可能state的数组并没有变化,但是每次都会map生成新的数组, 所以每次都会重新渲染。
解决办法可能是我们自己去实现should component update去深对比,性能不好,还挺麻烦。
更好的办法是 Reselect,详细可以看这篇文章:关于react, redux, react-redux和reselect的一些思考,详细解读使用过程中reselect优化性能的问题。
其实现原理,它有点像hooks中的useMemo,当依赖发生改变的时候,会重新计算值,如果依赖没有发生改变就直接返回旧的值,感兴趣可以看看源码。
上面的例子可以改写为:
import { createSelector } from 'reselect'
const objectIdsSelecter = state => state.objecctIds;
const objectsSelect = state => state.objects;
const objectsSelecter = createSelector(
objectIdsSelecter,
objectsSelect,
(objectIds, objects) => {
return objectIds.map(id => objects[id])
})
)
const mapStteToprops = state => {
return {
objects: objectsSelecter(state)
}
}
僵尸节点问题
原因分析:
- V5之前的版本,由于之前是class组件是在componentdidmount 里面去处理订阅逻辑的(源码实现部分),所以子组件会比父组件先订阅更新。
- selectors函数提取状态依赖父组件给他传的props,但是父组件删除了某些state,props还没来得及更新,如果子组件先订阅状态,意味着他会先更新,但是子组件的selector 函数去读state,此时已经删除了,就会报错。
解决方式:实现订阅树。
之前的订阅模式:
现在订阅模式:
也就是说子节点会订阅自己最近的父节点,而不是直接订阅store。
实现部分
实现subscription,
亮点:创建了一个订阅函数的双向链表 -> 好处就是增删的时候比较快。
整体用Function component重构
亮点:userMemo返回渲染节点的效果 和React.memo 和 shouldComponentUpdate 返回false的效果是一样的。实现部分
// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
return renderedChild
亮点: 对比props,判断是否更新组件的逻辑,变为了直接使用React.memo,因为React会帮我们自己浅对比,判断是否更新组件。
// https://github.com/reduxjs/react-redux/blob/master/src/components/connectAdvanced.js#L474
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
利用Context API特性实现订阅树
由于context会找最近的Privider提供的值,可以在这里实现store的状态树。源码
overriddenContextValue中的subscription,被覆盖为这一层的connect的subscription。
const subscription = new Subscription(
store, // 如果没有父,直接用store。
contextValue.subscription // 用父状态的
)
const overriddenContextValue = useMemo(() => {
return {
...contextValue,
subscription,
}
}, [didStoreComeFromProps, contextValue, subscription])
计算新props的逻辑。
// https://github.com/reduxjs/react-redux/blob/master/src/connect/selectorFactory.js#L18
function handleSubsequentCalls(nextState, nextOwnProps) {
const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
const stateChanged = !areStatesEqual(nextState, state)
state = nextState
ownProps = nextOwnProps
if (propsChanged && stateChanged) return handleNewPropsAndNewState()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState()
return mergedProps
}
分为三种情况:
- props 和 state都改变了
- 仅props改变
- 仅state改变
hook源码分析
function Component(){
const selectedValue = useSelecter(selector);
const store = useStore();
const dispatch = useDisaptch();
return //...
}
没啥好分析的。。。Hook 实现十分简单,这也导致了很多库选择只使用hooks API。。。
useSelecter实现:
- 通过subcribe订阅store更新,回调里通过seleter算出state,如果对比相等,阻止没必要更新,如果不相等forceUdpate。源码
- 如果其他原因更新:每次还是会算selctor,如果算出来是一样的话,其实还是返回上次的值,这算是一个小优化吧。。。。源码。
useStore就是返回Store
useDispatch 就是返回Store.dispatch
Redux相关库
由于社区上有太多的Redux相关或者使用redux思想的库,介绍不过来,但他们解决的问题大体相同,我认为主要有两点:
- 为了提高开发者体验(减少模版代码,聚合逻辑,TS类型)
- 集合一些redux最佳实践(immer,redux-thunk)
至于选择我觉得取决于团队或者个人的偏好。
Redux Toolkit
周下载量:592k
大小:11.02kb
Demo: codesandbox.io/s/agitated-…
特点:
- redux官方出品,redux丰富的生态可供选择。(中性)他们的概念依旧很多。
createReuducer
createAction
帮助生成模版代码,React使用部分还是依赖react-redux
的使用- 支持自定义中间件。
- 内置immer、redux-thunk。
- 不支持生成衍生状态。
- 如果一开始就是使用redux全家桶,方便迁移。
- 支持HOC API、支持Hook API(react-redux支持)。
Reduck/rematch
周下载量:26K
底层还是依赖redux、react-redux,用了一些api,魔改了一些api。
Demo:
特点:
- 应该是借鉴了rematch( Examples | Rematch),但相比rematch多了computed。。。
- 我司jupiter团队出品,作为插件集成在Jupiter中。
- 不依赖React-redux。(自己实现)
- 支持HOC API、支持Hook API。都需要显示传入modal。
- 像rematch一样支持plugin,如immer
- 支持组件级别的储存(不存全局)。有点类似React-recoil。。。
computed 的返回值(completedCount)会根据它的依赖参数被缓存起来,且只有当它的依赖值(参数)发生了改变才会被重新计算。
上述,computed 计算依赖于当前 Model 的 state,如果不只依赖于 Model 的 state,且依赖其余外部参数,来进行动态计算。或者对派生数据做更细致的缓存优化,请看 高级用例-computed 部分。
- 事实上easy-peasy也是这样做的。。。
easy-peasy
周下载量:32k
大小:10.21kb
Demo: codesandbox.io/s/easy-peas…
特点:
- 比较轻量
- 不依赖React-redux。自己实现了其hook部分,和React-redux的差不多。
- 10min内上手,上手极其简单,学习体验极其舒适。
- 只支持 Hook API
- 内置immer、redux-thunk
- 支持组件级别的储存(不存全局)。
zustand
demo:codesandbox.io/s/determine…
周下载量:74K
特点:
- 比较轻量
- 简化了redux 的概念,保留了state、middileware等概念。
- 把action 和 state都放在一起储存。
api十分简单:
const api = { setState, getState, subscribe, destroy }
核心源码部分:
看完有种鹈鹕醒脑的感觉,只像react setstate一样不可变数据流自己保证,然后就是数据发布订阅,这就是redux吗?
总结
React 的状态管理 redux 总是绕不过去的话题,Redux 的核心思想十分巧妙,我们可以研究学习,但是 Redux 本身并不适合直接使用,我们可以结合我们的业务场景选择一些生态的库搭配使用。
转载自:https://juejin.cn/post/7259168207055093819