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




