首页 > webfront > ECMAS > react > > 正文

再谈redux实现原理分析与优化工程设计—redux

发布人:zhoulujun@live.cn    点击:

redux有三大准则单一数据源:整个应用状态,都应该被存储在单一store的对象树中。只读状态:唯一可以修改状态的方式,就是发送(dispatch)

Redux 的设计思想很简单

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

  2. 所有的状态,保存在一个对象里面。

在组件化的应用中(比如react、vue2.0等),会有着大量的组件层级关系,深嵌套的组件与浅层父组件进行数据交互,变得十分繁琐困难。而redux,站在一个服务级别的角度,可以毫无阻碍地(这个得益于react的context机制,后面会讲解)将应用的状态传递到每一个层级的组件中。redux就相当于整个应用的管家。

redux有三大准则

  1. 单一数据源:整个应用状态,都应该被存储在单一store的对象树中。

  2. 只读状态:唯一可以修改状态的方式,就是发送(dispatch)一个动作(Action),通俗来讲,就是说只有getter,没有setter。

  3. 使用纯函数去修改状态:纯函数保障了状态的稳定性,不会因不同环境导致应用程序出现不同情况,听说是redux真正的精髓,日后可以深入了解。

redux的几个概念

  1. Action:唯一可以改变状态的途径,服务器的各种推送、用户自己做的一些操作,最终都会转换成一个个的Action,而且这些Action会按顺序执行,这种简单化的方法用起来非常的方便。Action 是一个对象:const action = {type: 'UPDATE_DEMO',a: 'Write Document'}; 

  2. Reducer:当dispatch之后,getState的状态发生了改变,Reducer就是用来修改状态的。Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

    import {List, Map} from 'immutable';
    let stateInit = Map({
        a:''
    });
    export default function demoReducer(state = stateInit, action = {}) {
        switch (action.type) {
            case "UPDATE_DEMO":
                state = state.set("a", action.msg);
                return state;
            default :
                return state
        }
    }
  3. Store:管理着整个应用的状态,Store提供了一个方法dispatch,这个就是用来发送一个动作,去修改Store里面的状态,然后可以通过getState方法来重新获得最新的状态,也就是state。

    注册store tree

    Redux通过全局唯一的store对象管理项目中的state:

    store = createStore(reducer,initialState);

    可以通过store注册listener,注册的listener会在store tree每次变更后执行

    store.subscribe(function () {
      console.log("state change");
    });

    更新store tree

    store调用dispatch,通过action把变更的信息传递给reducer

    store根据action携带type在reducer中查询变更具体要执行的方法,执行后返回新的state

    export function updateDemoDataAciton(selectBankCard){
       return (dispatch) => {
          dispatch({
             type:"UPDATE_DEMO",
             Object
          })
       }
    }

在 Redux 的源码目录 src/,我们可以看到如下文件结构:

├── utils/

│     ├── warning.js # 打酱油的,负责在控制台显示警告信息

├── applyMiddleware.js

├── bindActionCreators.js

├── combineReducers.js

├── compose.js

├── createStore.js

├── index.js # 入口文件

下面结合代码,分析redux的实现。

Store实现

Store — 数据存储中心,同时连接着Actions和Views(React Components)

连接的意思大概就是:

  • Store需要负责接收Views传来的Action

  • 然后,根据Action.type和Action.payload对Store里的数据进行修改

  • 最后,Store还需要通知Views,数据有改变,Views便去获取最新的Store数据,通过setState进行重新渲染组件(re-render)。

Store的主要方法:

  • createStore createStore方法用来注册一个store,返回值为包含了若干方法的对象

  • combineReducers 存在的目的就是解决了整个store tree中state与reducer一对一设置的问题

  • bindActionCreators

  • bindActionCreators

  • applyMiddleWare

  • compose


createStore源码分析

createStore(
    reducer:(state, action)=>nextState, //reducer必须是一个function类型,此方法根据action.type更新state
    preloadedState:any, //store tree初始值
    enhancer:(store)=>nextStore//enhancer通过添加middleware,增强store功能【很牛逼,可以实现中间件、时间旅行,持久化等】
)=>{
    getState:()=>any,//读取store tree中所有state
    subscribe:(listener:()=>any)=>any,//注册listener,监听state变化。Redux采用了观察者模式,store内部维护listener数组,用于存储所有通过store.subscrib注册的listener,store.subscrib返回unsubscrib方法,用于注销当前listener;当store tree更新后,依次执行数组中的listener。【可以理解成是 DOM 中的 addEventListener】
    dispatch:(action:{type:""})=>{type:""},//分发action 1、根据action查询reducer中变更state的方法,更新store tree,2、变更store tree后,依次执行listener中所有响应函数
    replaceReducer:(nextReducer:(state, action)=>nextState)=>void//替换reducer,改变state更新逻辑【一般在 Webpack Code-Splitting 按需加载的时候用】

}
Redux 规定:
  • 一个应用只应有一个单一的 store,其管理着唯一的应用状态 state

  • 不能直接修改应用的状态 state

  • 若要改变 state,必须 dispatch 一个 action,这是修改应用状态的唯一途径

combineReducers源码分析

若整个项目只通过一个reducer方法维护整个store tree,随着项目功能和复杂度的增加,我们需要维护的store tree层级也会越来越深,当我们需要变更一个处于store tree底层的state,reducer中的变更逻辑会十分复杂且臃肿。

而combineReducers存在的目的就是解决了整个store tree中state与reducer一对一设置的问题。我们可以根据项目的需要,定义多个子reducer方法,每个子reducer仅维护整个store tree中的一部分state, 通过combineReducers将子reducer合并为一层。这样我们就可以根据实际需要,将整个store tree拆分成更细小的部分,分开维护。


Screen Shot 2019-01-30 at 7.40.25 PM.jpg

store enhancer基本概念及使用

这部分,其实事件项目,也鲜有关心,推荐阅读《浅析Redux 的 store enhancer


redux应用总结:

  • store 由 Redux 的 createStore(reducer) 生成

  • state 通过 store.getState() 获取,本质上一般是一个存储着整个应用状态的对象

  • action 本质上是一个包含 type 属性的普通对象,由 Action Creator (函数) 产生

  • 改变 state 必须 dispatch 一个 action

  • reducer 本质上是根据 action.type 来更新 state 并返回 nextState 的函数

  • reducer 必须返回值,否则 nextState 即为 undefined

  • 实际上,state 就是所有 reducer 返回值的汇总(本教程只有一个 reducer,主要是应用场景比较简单)

  • Action Creator => action => store.dispatch(action) => reducer(state, action) => 原 state state = nextState

Action Creator => action => store.dispatch(action) => reducer(state, action) => 原 state state = nextState

关于redux教程,建议通读:《Redux 进阶教程

immutable(数据不可变)的作用

React在利用组件(Component)构建Web应用时,其实无形中创建了两棵树:虚拟dom树和组件树,就像下图所描述的那样(原图):


react-redux原理分析

react技术栈不像angular,其进阶路线如下:

React ——> React + redux + React-redux ——> React + redux + React-redux + React-router

React其实跟Redux没有直接联系,也就是说,Redux中dispatch触发store tree中state变化,并不会导致React重新渲染。react-redux才是真正触发React重新渲染的模块。

redux与react-redux关系图

react-redux是一个轻量级的封装库,核心方法只有两个:

  • Provider

  • connect

实际应用如下:

const rootReducer = combineReducers({...});
function configureStore(preloadedState) {
    const store = createStore(
        rootReducer,
        preloadedState,
        enhancer
    )
}
const store = configStore();
render((            ), document.getElementById('view'));

下面我们来逐个分析其作用

Provider模块的功能

主要分为以下两点:

  • 封装原应用:在原应用组件上包裹一层,使原来整个应用成为Provider的子组件

  • 传递store:接收Redux的store作为props,通过context对象传递给子孙组件上的connect

connect模块的功能

connect模块才是真正连接了React和Redux

function mapStateToProps(state) {
    return {
        loading: state.global.get('loading'),
        demoData: state.demoData.get('demoData'),
    }
}
function mapDispatchToProps(dispatch) {
    return bindActionCreators(updateDemoDataAciton, dispatch);
}
class DemoComponent extends React.Component {}
const DemoContainer = connect(mapStateToProps, mapDispatchToProps)(DemoComponent);

connect完整函数声明如下:

connect=(
    mapStateToProps(state,ownProps)=>stateProps:Object, 
    mapDispatchToProps(dispatch, ownProps)=>dispatchProps:Object, 
    mergeProps(stateProps, dispatchProps, ownProps)=>props:Object,
    options:Object
)=>(
    WrappedComponent
)=>component

再来看下connect函数体结构,我们摘取核心步骤进行描述

export default function connect (mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
    //... 参数处理
    return function wrapWithConnect (WrappedComponent) {
        class Connect extends Component {
            constructor (props, context) {
                super(props, context);
                // 从祖先Component处获得store
                this.store = props.store || context.store;
                this.stateProps = computeStateProps(this.store, props);
                this.dispatchProps = computeDispatchProps(this.store, props);
                this.state = {storeState: null};
                // 对stateProps、dispatchProps、parentProps进行合并
                this.updateState();
            }
            shouldComponentUpdate (nextProps, nextState) {
                // 进行判断,当数据发生改变时,Component重新渲染
                if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
                    this.updateState(nextProps);
                    return true;
                }
            }
            componentDidMount () {
                // 改变Component的state
                this.store.subscribe(() => {
                    this.setState({
                        storeState: this.store.getState()
                    });
                });
            }
            //... 周期方法及操作方法
            render () {
                this.renderedElement = createElement(WrappedComponent, his.mergedProps); //mearge stateProps, dispatchProps, props);
                return this.renderedElement;
            }
        }
        return hoistStatics(Connect, WrappedComponent);
    };
}

connect模块返回一个wrapWithConnect函数,wrapWithConnect函数中又返回了一个Connect组件。

Connect组件的功能有以下两点:

  1. 包装原组件,将state和action通过props的方式传入到原组件内部

  2. 监听store tree变化,使其包装的原组件可以响应state变化

此部分内容来自《Redux原理(一):Store实现分析》,推荐阅读原文。

再次也建议读一下《React.js 的 context》React.js 的 context 就是这么一个东西,某个组件只要往自己的 context 里面放了某些状态,这个组件之下的所有子组件都直接访问这个状态而不需要通过中间组件的传递。一个组件的 context 只有它的子组件能够访问,它的父组件是不能访问到的,你可以理解每个组件的 context 就是瀑布的源头,只能往下流不能往上飞。

Redux如何设计action、reducer、selector

错误1:以API为设计State的依据

以API为设计State的依据,往往是一个API对应一个子State,State的结构同API返回的数据结构保持一致(或接近一致)。

但是API是基于服务端逻辑设计的,而不是基于应用的状态设计的。

错误2:以页面UI为设计State的依据

页面UI需要什么样的数据和数据格式,State就设计成什么样。这种方式的优点就是模块与模块之间互相独立,不会相互影响,每个页面维护自己的reducer,并且只在store中存入该页面展示或者变化的最小数据,使用起来很方便,不太需要关心其他模块的缓存数据,比较符合redux设计的初衷(并不是用来做一个前端数据库)

以页面UI为设计State存在的问题:一、这种State依然存在数据重复的问题,二、当新增或修改一条记录时,需要修改不止一个地方。


合理设计State

最重要最核心的原则是像设计数据库一样设计State

把State看做一个数据库,State中的每一部分状态看做数据库中的一张表,状态中的每一个字段对应表的一个字段。设计一个数据库,应该遵循以下三个原则:

  1. 数据按照领域(Domain)分类,存储在不同的表中,不同的表中存储的列数据不能重复。

  2. 表中每一列的数据都依赖于这张表的主键。

  3. 表中除了主键以外的其他列,互相之间不能有直接依赖关系。

这三个原则,可以翻译出设计State时的原则:

  1. 把整个应用的状态按照领域(Domain)分成若干子State,子State之间不能保存重复的数据。

  2. State以键值对的结构存储数据,以记录的key/ID作为记录的索引,记录中的其他字段都依赖于索引。

  3. State中不能保存可以通过已有数据计算而来的数据,即State中的字段不互相依赖。

具体推荐阅读《如何优雅的设计Redux的Store中的State树》、《Redux进阶系列3:如何设计action、reducer、selector


用localStorage缓存Redux的state

基于Redux+React构建的单页面应用组件的 大部分状态 (一些非受控组件内部维护的state,确实比较难去记录了)都记录在Redux的store维护的state中。

正是因为Redux这种基于全局的状态管理,才让“UI模型”可以清晰浮现出来。

所以,只要在浏览器的本地存储(localStorage)中,将state进行缓存。

一种简(愚)单(蠢)的方式是,在每次state发生更新的时候,都去持久化一下。这样就能让本地存储的state时刻保持最新状态。

基于Redux,这也很容易做到。在创建了store后,调用subscribe方法可以去监听state的变化。

// createStore之后
store.subscribe(() => {
  const state = store.getState();
  saveState(state);
})

显然,从性能角度这很不合理(不过也许在某些场景下有这个必要)。所以机智的既望同学,提议只在onbeforeunload事件(刷新或关闭)上就可以。

window.onbeforeunload = (e) => {
  const state = store.getState();
 saveState(state,version);//读取state的时候,则要比较代码的版本和state的版本,不匹配则进行相应处理。版本维护方便
};


只需要在应用初始化的时候,Redux创建store的时候取一次就可以,就可以(基本)还原用户最后的交互界面了。




参考文章:

redux深入理解

Redux原理(一):Store实现分析

react-redux原理分析

用localStorage缓存Redux的state

关于Redux框架,Reducer中state处理方式的探讨

Redux 简明教程redux中间件的原理——从懵逼到恍然大悟

Redux 卍解

Redux 思想和源码解读(二)

解读redux工作原理