likes
comments
collection
share

React状态管理(一)—— Redux

作者站长头像
站长
· 阅读数 11

前言

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

  • 可预测
    • reducer是纯函数,所以状态是可预测的
  • 中心化
    • 全局只有一个store
  • 易调试
    • action --> change state
  • 灵活性
    • middleware机制 他的源码十分的简洁,但是其扩展的生态却十分丰富,设计思想非常🐂,下面让我们一起来学习。

设计思想

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

React状态管理(一)—— Redux

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)

})

总结

React状态管理(一)—— Redux

React状态管理(一)—— Redux

源码解析

源码实现

源码简单实现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)

    }

}

僵尸节点问题

demo

原因分析:

  1. V5之前的版本,由于之前是class组件是在componentdidmount 里面去处理订阅逻辑的(源码实现部分),所以子组件会比父组件先订阅更新。
  2. selectors函数提取状态依赖父组件给他传的props,但是父组件删除了某些state,props还没来得及更新,如果子组件先订阅状态,意味着他会先更新,但是子组件的selector 函数去读state,此时已经删除了,就会报错。

解决方式:实现订阅树。

之前的订阅模式:

React状态管理(一)—— Redux

现在订阅模式:

React状态管理(一)—— Redux

也就是说子节点会订阅自己最近的父节点,而不是直接订阅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

  }

分为三种情况:

  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

React状态管理(一)—— Redux

底层还是依赖redux、react-redux,用了一些api,魔改了一些api。

React状态管理(一)—— Redux

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吗?

总结

React 的状态管理 redux 总是绕不过去的话题,Redux 的核心思想十分巧妙,我们可以研究学习,但是 Redux 本身并不适合直接使用,我们可以结合我们的业务场景选择一些生态的库搭配使用。