Redux及react-redux全面详解,一网打尽!

5,003 阅读8分钟

一、前言

redux是什么?

Flux演变而来,作为状态容器,提供可预测的状态管理。

为什么要用redux?

  • 复杂应用中无法获取操作记录,而状态的改变往往意味着对应视图的变化,这一过程是不可控的。

    提供一种机制,统一对状态的查询、改变、应用进行管理,同时对每一次的状态变更可进行回溯追踪

  • 多个组件件间可能存在数据的共享以及通信。

    这里可以简单将其理解为充当了eventBus的角色。

  • 视图的改变以及数据的获取等操作杂糅在一起,不利于维护

    具体操作 -> 状态变更 -> 触发视图更新,这一单向数据流的控制,不仅使得对状态变化的过程变得可控,同时解耦了数据M和视图V。见下图,针对相同的action,其数据流转以及state管理是独立于前端模板的,因此可以实现跨框架的复用。 数据和视图的解耦

redux适用所有项目?

  • 需不需要使用redux,还是要结合项目实际以及业务需求,它只是web架构的一种解决方案。比如完全没有必要因为第二种组件通信而使用,目前的主流框架如vue有vuex、依赖注入等实现。
  • react使用flux,主要是因为react只提供了组件化UI,是视图模板渲染的一种解决方案,并没有提供诸如双向绑定之类的对数据流的有效管理。

二、Redux概述

Redux主要有如下几个概念:

  • action:一般写法:<type:操作意图(一般定义为字符串常量), payload:{text:"要改变的新数据"}>,是store数据的唯一来源。

  • store:Redux应用只有一个单一的store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合而不是创建多个store。(常用方法:dispatchgetStatesubscribe等)

  • state:数据对象,由store对象管理,通过getStore()只读

    改变的唯一途径:由store的dispatch方法对action进行分发,reducer进行处理 范式化。

  • reducer:对action做出相应响应,返回新生成的state(这也是保证可回溯的原因)。
    【扩展】生成新的state有如下几种方式:

    • Object.assign({}, oldState, newState)
    • {...oldSate, 新值}
    • Immutable
      这里扩展下:javascript中的基本类型:Boolean、Number、String、Null、undefined、Symbol等都是不可变的(Immutable),只有Object是可变的(Mutable).
  • subscribe

    用于订阅事件,每次state变更时,都会触发其订阅的事件。在这里可以处理state->view的绑定相关逻辑,Redux没有对其做约束。
    react-redux提供了使用provide和connect绑定,不必关心隐含在内的订阅方法。

  • 中间件

    这里强调下Redux的中间件使用,在每次action触发时,都会先通过层层中间件再真正执行。这个过程赋予了我们诸如日志记录等能力。洋葱圈模型,和KOA的中间件原理类似。

    运作原理图如下:

    运作流程图

三、常见问题

1. 怎么处理异步事件?

解答这个问题前,先看下同步action的处理:

//该方法用于生成action
let actionCreator = (name) => ({
        type: 'ADD_ITEM',
        value: { name }
    });

使用:

dispatch(actionCreator('M2'));

现在处理异步的事件要面临一个问题:在何处发请求?无外乎三处选择:actionCreatormiddlewarereducer,而reducer是纯函数,middleware一般作用于所有action,因此不建议用于异步事件的处理。由于actionCreator用于生成action,其value往往是直接改变state的值,所以,在actionCreator中处理异步事件生成action,合情合理:

改造1:直接在actionCreator中使用异步

let asyncActionCreator = (name) => {
    setTimeout(() => {
        return {
            type: 'ADD_ITEM',
            name: { name }
        };
    }, 3000)
}
//问题:会直接返回undefined,不符合预期

改造2:异步情况下返回function,而不是返回action对象

let asyncActionCreator = postTitle => (dispatch, getState) => {
    dispatch(requestPosts(postTitle));
    return fetch(`/some/API/${postTitle}.json`)
        .then(response => response.json())
        .then(json => dispatch(receivePosts(postTitle, json)));
    };
};
# 这里需要使用middleware支持异步,例如redux-thunk
# var thunkMiddleware = function ({ dispatch, getState }) {
#   return function(next) {
#      return function (action) {
#            //如果是function,则传递dispatch、getState,并执行
#            return typeof action === 'function' ?
#                //原始的dispatch
#                action(dispatch, getState) :
#                next(action)
#         }
#   }
# }
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二:返回的Promise,可以在dispatch异步数据,reducer处理后,做一些处理
store.dispatch(fetchPosts('reactjs')).then(() =>
  console.log(store.getState())
);

【扩展】thunk的概念,可以简单理解为:将多参数函数替换成一个只接受回调函数作为参数的单参数函数(以闭包的形式实现延迟执行的一段代码)。 举例:

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
    return function (callback) {
        return fs.readFile(fileName, callback);
    };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

【扩展】异步解决方案中一般将异步操作包装成Thunk函数或者Promise对象,即Generator的next()方法返回值的value属性是一个Thunk函数或者Promise对象。换句话说,Thunk和Promise都是Generator自动执行的解决方案。

2. 多个reducer合并

不可能把所有的action处理放到一个reducer中,可以按业务或者组件对reducer进行拆分,方便维护。

Redux提供了combineReducers,将子 Reducer 合并成一个大的函数:

import { createStore, combineReducers } from 'redux'

var reducer = combineReducers({
    user: userReducer,
    items: itemsReducer
})

其对应的state结构,其中useritems等同于命名空间,可以进行数据隔离:

{
    user: {}, 
    items: {}
}

combineReducer的简单实现:

const combineReducers = reducers => {
    return (state = {}, action) => {
       return Object.keys(reducers).reduce(
         (nextState, key) => {
            //根据 State 的 key 去执行相应的子 Reducer,并将返回结果合并成一个大的 State 对象
           nextState[key] = reducers[key](state[key], action);
           return nextState;
          },
        {} 
     );
    };
};

3. appyMiddleware(middlewares)(createStore)(reducers)在干什么?

createStore(reducers)的区别是增强了store的dispatch,在dispatch前后根据提供的middleware,提供一些能力。

【扩展】在看源码的时候会发现柯里化(什么是柯里化?)的使用很常见:combinReducer、applyMiddleware等。好处:提前绑定参数,延迟计算。

thunk和柯里化的对比:

1)两者的返回值都是函数
2)柯里化:一次使用一个参数;配置部分参数,后续重复使用
3)thunk:同样是延迟执行,不过没有要求一次必须一个参数,只要最后转换为只接受回调函数作为参数的单参数函数。
个人认为,没必要纠结名词,知道有这种解决问题的思路即可(尤其在当前流行造各种花里胡哨名词的氛围下)。

四、redux实例

以下是仿Redux写的demo,实现了Redux的基本能力,缓存了操作记录,进一步扩展可以实现诸如回退等功能。

以下列出了几个核心函数:

  • store

    /**
    * store提供的方法:
     *  1. getState:获取当前state
    *  2. dispatch:触发行为
    *  3. subscribe:订阅事件
     */
     function createStore(reducers, initState) {
        //historyState可用作时光机
        let state = initState, historyState = [{action:'init', state: initState}], listeners = [];
    
        let getState = () => state;
    
        let subscribe = (event) => {
           listeners.push(event);
            /**
             * 注册事件的同时,获取事件句柄
             * let fnHandle = fnsubscribe(fn)
             * 删除对应注册事件
             * fnHandle();
            *  */
            return () => {
                listeners = listeners.filter(eventItem => eventItem != event);
            }
        }
    
     let dispatch = (action) => {
        //reducer根据action对state做改变,返回全新的state
         state = reducers(state, action);
         historyState.push({action, state})
         //state改变的情况下,触发事件
         listeners.forEach(listener => listener());
      }
     return {
          dispatch,
          getState,
           subscribe,
           getHistoryState: () => historyState
       }
    }
    
  • 绑定中间件applyMiddleware

    //将中间件绑定到
    let applyMiddleware = middlewares => (createStore) => (...args) => {
      let store = createStore(...args);
       let dispatch = store.dispatch;
      let params = {
          dispatch,
          getState: store.getState
      };
       middlewares = middlewares.map(mw => mw(params));
      //组合中间件,增强dispatch
      //装饰者模式 & 洋葱圈,compose对中间件的组合
       dispatch = compose(middlewares)(dispatch);
      //返回增强后的dispatch
       return Object.assign(store, {dispatch});
    }
    
  • 使用

    • reducers
      /**
       * 对action做处理,返回全新的state
       */
      function reducers(state, action){
          switch(action){
              case 'add':
                  return state += 1;
              case 'minus':
                  return state -=1;
              default:
                  return 0;
          }
      }
      
    • middleware
      /**
      * 中间件(注意中间件的约定格式)
       */
      function middleware1({dispatch, getState}) {
         return function(next) {
             return function(action) {
                 console.log(`【日志】当前state值:${getState()},执行${action}`);
                next(action);
                console.log(`【日志】操作完成后,state值:${getState()}`);
           }
        }
      }
      function middleware2({dispatch, getState}) {
         return function(next) {
             return function(action) {
                 console.log(`>>>>>>>>>>>>>>>>>>`);
                  next(action);
               console.log(`<<<<<<<<<<<<<<<<<<`);
            }
       }
      }
      
    • 初始化store
      /**
       * 对外暴露:
       *  1. createStore: 产生store
       *  2. applyMiddleware:中间件处理
       */
      let initState = 1;
      let store = applyMiddleware([middleware2, middleware1])(createStore)(reducers, initState);
      
      
  • 展示
    demo展示效果如下:

    这里只是简单说明redux的能力,具体功能可自行扩展。

    完整代码见:github.com/269117220/w…

五、React-Redux

1. 组件的拆分

基于容器组件和展示组件相分离的开发思想,将组件分为:UI组件容器组件

展示组件 容器组件
作用 描述如何展示(骨架、样式) 描述如何运行(数据获取、状态更新)
直接使用Redux
数据来源 props 监听Redux state
数据修改 从props调用回调函数 向Redux派发actions
调用方式 手动 通常由React Redux生成

2. 容器组件

用于关联展示组件和Redux,利用store.subscribe()从Redux state中读取需要的数据,并通过props来把这些数据提供给要渲染的组件。

import { connect } from 'react-redux'
//第一次是设置参数,第二次是组件与 Redux store 连接
const VisibleTodoList = connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList)
export default VisibleTodoList

connect(): 不会改变原来的组件类,返回一个新的与 Redux store 连接的组件类。生成的容器组件,做了性能优化避免不必要的重复渲染。

容器组件做的两件事:

提前声明:容器组件的stateActionCreator作为子组件(展示组件)的props传入。

  • mapStateToProps:state -> view
    订阅了全局状态state的变化,每次state更新时,对state进行过滤,只返回该容器型组件关注的局部状态,继而触发渲染。
    将state映射到展示组件的props,即下面例子中的todos为props的属性,其值的来源为state。
    每一次全局状态变化都会调用所有容器型组件的mapStateToProps方法,该方法返回一个纯对象,并将其合并到容器型组件的props上。

    const getVisibleTodos = (todos, filter) => {
        switch (filter) {
            case 'SHOW_COMPLETED':
                return todos.filter(t => t.completed)
            case 'SHOW_ACTIVE':
                return todos.filter(t => !t.completed)
            case 'SHOW_ALL':
            default:
                return todos
        }
    }
    // 函数接收整个 Redux store 的 state 作为 props,然后返回一个传入到组件 props 的对象。
    const mapStateToProps = state => {
        return {
            todos: getVisibleTodos(state.todos, state.visibilityFilter)
        }
    }
    

    返回的对象会与组件的 props 合并。

    • Immutable的使用

      问题: 由于state变动,会通知所有订阅事件,所有容器组件的mapStateToProps都会执行一遍,如果不做特殊处理,会导致全部组件重新渲染。
      解决: 因此为了避免无意义的重新渲染,在遵从Redux的immutable状态规范的情况下,当一个容器型组件的默认shouldComponentUpdate函数返回true时,则表明其对应的局部状态发生变化,需要将状态传播到各个子组件,相应的所有子组件也都会进行虚拟DOM比较,以确定是否需要重新渲染。

  • mapDispatchToProps: view -> action
    建立 UI 组件的参数到store.dispatch方法的映射,返回期望注入到展示组件的 props 中的回调方法,即ActionCreator

    const mapDispatchToProps = dispatch => {
        return {
            onTodoClick: id => {
              dispatch(toggleTodo(id))
            }
        }
    }
    

    这些属性会被合并到组件的 props 中。

3. <Provider> 组件

connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。一种方式是把它以 props 的形式传入到所有容器组件中,存在层层繁琐的传递,而且往往中间组件并不需要的问题。建议的方式是使用指定的 React Redux 组件 <Provider>:

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

render(
    <Provider store={store}>
      <App />
    </Provider>,
    document.getElementById('root')
)

Provider在根组件外面包了一层,这样一来,App的所有子组件就默认都可以拿到state了。原理是React组件的利用context属性。

React-Redux的connect代码类似下面所示:

export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
    class Connect extends Component {
        static contextTypes = {
             store: PropTypes.object
        }
        constructor () {
            super();
            this.state = {
                allProps: {}
            }
        }
        componentWillMount () {
            const { store } = this.context
            this._updateProps()
            store.subscribe(() => this._updateProps())
        }
        _updateProps () {
            const { store } = this.context
            let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props): {} 
            let dispatchProps = mapDispatchToProps? mapDispatchToProps(store.dispatch, this.props) : {} 
            this.setState({
                allProps: {
                  ...stateProps,
                  ...dispatchProps,
                  ...this.props
                }
              })
        }
        render () {
            return <WrappedComponent {...this.state.allProps} />
        }
    }
    return Connect
}
3.1 context的使用
3.1.1 写法一:

生产者:

// 声明Context对象属性
static childContextTypes = {
    propA: PropTypes.string,
    methodA: PropTypes.func
}

// 返回Context对象,方法名是约定好的
getChildContext () {
    return {
      propA: 'propA',
      methodA: () => 'methodA'
    }
}

消费者:

// 通过静态属性contextTypes声明需要使用的Context属性
static contextTypes = {
    propA: PropTypes.string
}

//使用,为声明无法获取
this.context.propA

******************************
//example:无状态子组件访问父组件的context
import React from 'react'
import PropTypes from 'prop-types'

//函数式组件等同于类组件的render【第二个参数】
const ChildComponent = (props, context) => {
  const {
    propA
  } = context
    
  return ...
}
  
ChildComponent.contextProps = {
  propA: PropTypes.string    
}
3.1.2 写法二:
class Header extends React.Component {
  render () {
    return (
      <Title>Hello React Context API</Title>
    );
  }
}

//创建Context对象(<Provider /> 和 <Consumer />)
const ThemeContext = React.createContext({
  background: 'red',
  color: 'white'
});

class App extends React.Component {
  render () {
    return (
        //生产者:value等价于getChildContext
      <ThemeContext.Provider value={{background: 'green', color: 'white'}}>
        <Header />
      </ThemeContext.Provider>
    );
  }
}

class Title extends React.Component {
  render () {
    return (
        //消费者:其children为一个函数(函数式组件)
      <ThemeContext.Consumer>
        {context => (
          <h1 style={{background: context.background, color: context.color}}>
            {this.props.children}
          </h1>
        )}
      </ThemeContext.Consumer>
    );
  }
}

4. 总结

provide组件将store注入子组件,容器组件接管store,包括:view -> action, state -> view

参考文献

欢迎关注公众号,不定时更新哦~