React状态管理一些思考(中篇)–Redux

传送门,上一期:React状态管理一些思考(上篇)

前言

Redux是Dan Abramov在2015年发布,是React生态里最火的状态管理库,其主要有四个特性:

  • 可预测

    • reducer是纯函数,所以状态是可预测的
  • 中心化

    • 全局只有一个store
  • 易调试

    • action –> change state
  • 灵活性

    • middleware机制

他的源码十分的简洁,但是其扩展的生态却十分丰富,设计思想非常?,下面让我们一起来学习。

设计思想

要了解Redux的设计思想,首先看看React的设计思想——单向数据流,看下图:

State描述的就是当前用户的状态,View是根据当前State渲染出来的,根据View层响应不用的Action,Action改变State,重新渲染view层。
Redux的设计思想就是:

  1. 应用是一个状态机,视图和状态是一一对应的。

  2. 把所有的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,即要求数据流为不可变性。
这里要说一下纯函数的定义:

  1. 相同的输入总是返回相同的输出。

  2. 不能有副作用。

纯函数、不可变性其实都是函数式编程的术语,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,前者是跟任何框架无关的状态管理库,后者是将它和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)
    }
}
复制代码

僵尸节点问题

demo

原因分析:

  1. V5之前的版本,由于之前是class组件是在componentdidmount 里面去处理订阅逻辑的( 源码实现部分 ),所以子组件会比父组件先订阅更新。

  2. selectors函数提取状态依赖父组件给他传的props,但是父组件删除了某些state,props还没来得及更新,如果子组件先订阅状态,意味着他会先更新,但是子组件的selector 函数去读state,此时已经删除了,就会报错。

解决方式:实现订阅树。
订阅模式:

现在订阅模式:

也就是说子节点会订阅自己最近的父节点,而不是直接订阅store。

实现部分

实现 subscription

亮点:创建了一个订阅函数的双向链表 -> 好处就是增删的时候比较快。

整体用Functioncomponent重构

亮点: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](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](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
  }
复制代码

分为三种情况:

  1. props 和 state都改变了

  2. 仅props改变

  3. 仅state改变

hook源码分析

function Component(){
    const selectedValue = useSelecter(selector);
    const store = useStore();
    const dispatch = useDisaptch();
  
    return //...
}
复制代码

没啥好分析的。。。Hook 实现十分简单,这也导致了很多库选择只使用hooks API。。。
useSelecter实现:

  1. 通过subcribe订阅store更新,回调里通过seleter算出state,如果对比相等,阻止没必要更新,如果不相等forceUdpate。 源码

  2. 如果其他原因更新:每次还是会算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

图片[1]-React状态管理一些思考(中篇)–Redux-一一网
底层还是依赖redux、react-redux,用了一些api,魔改了一些api。

Demo:
code.byted.org/toutiao-fe-…

特点:

  • 应该是借鉴了 rematchExamples | 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 }
复制代码

核心源码部分:
github.com/pmndrs/zust…
看完有种鹈鹕醒脑的感觉,只像react setstate一样不可变数据流自己保证,然后就是数据发布订阅,这就是redux吗?

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享