Redux、 React-Redux学习

1,642 阅读8分钟

前言

在学习 React 过程中,都会接触使用到 Redux, React-Redux ,不熟悉的小伙伴,可能疑惑有了 Redux,为什么还出现了 React-Redux,先带大家了解 Redux 的使用,以及使用过程中有哪些吐槽的点,再看看 React-Redux 为啥出现

一、Redux

在开始之前,需要记住的是

Redux 是一款著名 JavaScript 状态管理容器

也就是说,Redux 除了跟 React 配合使用,还可以配置 JS 、Vue 使用

1.1 设计思想

  • Redux 是将整个应用状态存储到一个叫做 store 的地方,里面存在一个状态树 state tree
  • 组件可通过 store.dispatch 派发行为 actionstore, store 不会直接修改 state, 而是通过用户编写的 reducer 来生成新的 state ,并返回给 store
  • 其它组件通过订阅 store 中的 state 状态变化来刷新自己的视图

redux-flow.png

1.2 三大原则

  • 整个应用有且仅有一个 store, 其内部的 state tree 存储整个应用的 state
  • state 是只读的,修改 state,只能通过派发 action,为了描述 action 如何改变 state 的,需要编写 reducer 纯函数
  • 单一数据源的设计让 React 组件之间通信更加方便,也有利于状态的统一管理

1.3 createStroe

通过实战写个 Counter 计数器来学习下 Redux 相关的 API

1.3.1 store

// ./src/store.js
import {createStroe} from 'react'

function reudcer(){}
let store = createStore(reudcer)

通过 createStore 方法可以创建一个 store, 需要传递一个参数 reducer (ps: 后续介绍),而 store 是个对象,有以下方法可调用

  • store.getState(), 获取最新的 state tree
  • store.dispatch(), 派发行为 action
  • store.subscribe(), 订阅 store 中 state 的变化

1.3.2 reducer

reducer 必须是个纯函数,接收 state, action 两个参数,state 是旧的状态,不可直接修改,而是需要根据 action.type 不同,来生成新的 state 并返回

// ./src/store.js
import {createStroe} from 'react'
export const ADD = 'ADD'
export const MINUS = 'MINUS'
function reducer (state = {count: 0}, action) {
  console.log('action', action); // {type: 'xxx'}  
  switch(action.type) {
    case ADD:
      return {count: state.count + 1}
    case MINUS:
      return {count: state.count - 1}
    default:
      return state
  }
}

let store = createStore(reudcer)
export default store

注意上面代码中,给 state 设置了初始值 {count: 0}, 接下来,会在 Counter 组件中去使用这个导出的 store

1.3.3 getState、dispatch、subscribe

// ./src/components/Counter.jsx
import React from 'react'
import store from '../store'

class Counter extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      number: store.getState().count
    }
  }
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
      <button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>
    </div>
  }
}

export default Counter

Counter 组件中,通过 store.getState() 可获取最新的 state, 点击按钮,会通过 store.dispatch 派发 action 给 store (ps:请注意 action 是个对象,必须存在 type 属性),store 内部会将当前 state, action 传递给 reducer 来生成新的 state 达到更新状态的目的, 遗憾的是,页面上数字并没有发生变化

截屏2020-11-14下午1.46.43.png

可以看到,reducer 函数中已经接受到了 action, 此时 store 中的 state 已经发生了变化,而页面不更新的原因在于 Counter 没有订阅 store 中 state 的变化,可在代码中加入下面代码

class Counter extends React.Component{
  componentDidMount () {
    this.unSubscribe = store.subscribe(() => {
      this.setState({
        number: store.getState().count
      })
    })
  }
  componentWillUnmount () {
    this.unSubscribe && this.unSubscribe()
  }
}

使用 store.subscribe 就可实现订阅,该方法接受一函数,当 storestate 中状态发生变化,就会执行传入的函数,同时 store.subscribe 方法返回一个函数,用于取消订阅。

至此,Counter组件已基本实现了。可能有些小伙伴发现应用首次加载后,控制台输出了

action {type: "@@redux/INIT1.s.m.m.c.n"}

这是 store 为了拿到 state 的初始值 {count: 0}, 会自动派发一次 action {type: "@@redux/INIT1.s.m.m.c.n"}

熟悉“发布-订阅”模式的小伙伴可能看得出,Redux 内部就是使用了“发布-订阅”模式。接下来,我们尝试实现个简陋版本 Redux

1.3.4 手写实现 createStroe


function createStore(reducer){
	let state
  const listeners = []
  
  // 返回最新的 state
  function getState () {
  	return state
  }
  
  // 派发 action
  function dispatch(action){
  	state = reducer(state, action)
    listeners.forEach(listener => listener())
  }
  
  // 订阅,返回取消订阅函数
  function subscribe(listener){
  	listeners.push(listener)
    return function () {
    	const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }
  
  // 获取state默认值
  dispatch({type: "@@redux/INIT1.s.m.m.c.n"})
  
  // 返回 store, 一个对象
  return {
  	getState,
    dispatch,
    subscribe
  }
}

export default createStore

通过测试,我们简陋版 Redux 已经实现的 Counter 组件的功能

1.4 bindActionCreators

1.4.1 原理及使用

在 Counter 组件中,我们是直接使用 store.dispatch 派发action

<button onClick={() => store.dispatch({type: 'ADD'})}>+</button>
<button onClick={() => store.dispatch({type: 'MINUS'})}>-</button>

上面写法的缺陷在于,多次重复写了  store.dispatch, 并且 action.type 容易写错还不易发现,此时 redux 提供了 bindActionCreators 功能,将派发 action 的函数与 store.dispatch 进行绑定

// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '../store'

// add 函数返回 action, 所以该函数可称作 actionCreator
function add() {
  return {type: 'ADD'}
}

function minus() {
  return {type: 'MINUS'}
}

const bindAdd = bindActionCreators(add, store.dispatch)
const bindMinus = bindActionCreators(minus, store.dispatch)

class Counter extends React.Component{
	// ...
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={bindAdd}>+</button>
      <button onClick={bindMinus}>-</button>
    </div>
  }
}

export default Counter

其实,👆代码中可将 bindActionCreators 逻辑抽离到单独文件中,可在其它组件中去使用。同时,上面代码的缺陷在与 每个函数都需要去手动绑定,并不合理,所以,bindActionCreators 支持传入对象,将所以的 actionCreator 函数包装成对象

// ./src/components/Counter.jsx
import React from 'react'
import {bindActionCreators} from 'redux'
import store from '../store'

// add 函数返回 action, 所以该函数可称作 actionCreator
function add() {
  return {type: 'ADD'}
}

function minus() {
  return {type: 'MINUS'}
}

const actions = {add, minus}

const bindActions = bindActionCreators(actions, store.dispatch)

class Counter extends React.Component{
  // ...
  render () {
    return <div>
      <p>{this.state.number}</p>
      <button onClick={ bindActions.add }>+</button>
      <button onClick={ bindActions.minus }>-</button>
    </div>
  }
}

export default Counter

1.4.2 手写实现

function bindActionCreators (actionCreater, dispatch) {
  // actionCreater 可以是函数/对象
  if (typeof actionCreater === 'function') {
  	return function (...args) {
    	return dispatch(actionCreater(...args))
    }
  } else {
  	let bindActionCreaters = {}
    Object.keys(actionCreater).forEach(key => {
    	bindActionCreaters[key] = function (...args) {
        return dispatch(actionCreater(...args))
      }
    })
    return bindActionCreaters
  }
}

export default bindActionCreaters

1.5 combineReducers

1.5.1 原理及使用

当一个应用包含多个模块,将所以模块的 state 放在并不合理,更好的做法是按照模块进行划分,每个模块有各自的 reducer、action,最终通过 Redux 中的 combineReducers 合并成一个大的 reducer

// src\store\reducers\index.js
import {combineReducers} from 'redux';
import counter1 from './counterReducer1';
import counter2 from './counterReducer2';
export default combineReducers({
    x: counter1,
    y: counter2
});

// src/store/reducers/counterReducer1.js
import * as types from '../action-types';
export default function (state= {count: 0},action){
    switch(action.type){
        case types.ADD1:
            return state.count + 1;
        case types.MINUS1:
            return state.count - 1;
        default:
            return state;
    }
}

// src/store/reducers/counterReducer2.js
import * as types from '../action-types';
export default function (state= {count: 0},action){
    switch(action.type){
        case types.ADD2:
            return state.count + 1;
        case types.MINUS2:
            return state.count - 1;
        default:
            return state;
    }
}

combineReducers 方法接受一个对象,属性key 可任意设置,属性value对应每个模块的 reducer 函数, 返回最终的一个合并之后的 reducer 方法。

通过 reducer 合并之后,store 中的 state tree 也会按照模块进行划分

store.getState() 
{
  x: {count: 0}
  y: {count: 0}
}

这样,在组件中,使用 state 需要修改成下面这样

import store from '../store';
export default class Counter extends Component {
    constructor(props) {
        super(props);
        this.state = {
          value: store.getState().x.count
        }
    }
  	//...
}

当组件中派发 action 时,action 会传递到 combineReducers 返回的函数中,在该函数中,会调用每个模块各自的 reducer 生成各自新的 state, 最终将所以 state 合并之后,去更新 store 中的 state

1.5.2 手写实现

function combineReducers(reducers){
  // 返回合并之后的 reducer 函数
  return function (state, action){
  	const nextState = {}
    Object.keys(reducers).forEach(key => {
    	nextState[key] = reducers[key](state[key], action)
    })
    return nextState
  }
}

可以看出,主要派发 action,每个模块的的 reducer 函数都会执行的

1.6 小结

可以看出,在 React 组件中使用 store, 都需要手动去引入 store 文件, 手动订阅 store 中状态的变化,这是不合理的,接下来,我们看下 react-redux 是如何解决的

二、React-Redux

2.1 原理及使用

react-redux 提供一个 Provider 组件,通过 Provider 组件,可以向其子组件、孙组件传递 store, 而不需要每个组件都手动引入

// ./src/index.js
import { Provider } from 'react-redux'
import store from './store'

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

在后代组件 Counter1 中,可使用 react-redux 提供 connect 函数,将 store 与 Counter1 组件的 props 进行关联

import React from 'react'
import { connect } from 'react-redux'
import action from '../store/actions/Counter1'

class Counter1 extends React.Component{
  render () {
    return <div>
      <p>{ this.props.count }</p>
      <button onClick={ this.props.add }>+</button>
      <button onClick={ this.props.minus }>-</button>
    </div>
  }
}

const mapStateToProps = state => state
const mapDispatchToProps = {
  ...action
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter1)

从上面代码中,可以看出在 Counter1 组件内部,属性或方法都是通过 props 访问,我们完全可以将 Counter1 组件转换成函数组件(无状态组件),通过函数组件外部都是一个容器组件(有状态组件)进行包裹,所有 connect(mapStateToProps, mapDispatchToProps)(Counter1) 最终返回的就是一个容器组件,接下来我们看下如何手写一个 react-redux

2.2 手写实现

想跨组件传递 store,react-redux 内部使用了 React Context API

创建一个 ReactReduxContext 上下文对象

// src/react-redux/Context.js

import React from 'react'
export const ReactReduxContext = React.createContext(null)
export default ReactReduxContext

在 Proveider 组件中,需要使用 ReactReduxContext 对象中提供的 Provider 组件

// src/react-redux/Provider.js
import React from 'react'
import {ReactReduxContext} from './Context'

class Provider extends React.Component{
  constructor(props) {
  	super(props)
  }
  render () {
  	return <ReactReduxContext.Provider value={{ store: this.props.store }}>
      {this.props.children}
    </ReactReduxContext.Provider>
  }
}
export default Provider

而 connect 方法,接收 mapStateToProps, mapDispatchToProps 两个参数,返回一个函数,返回的函数接收 自定义组件(例如 Counter1 ),函数执行后,返回最终的容器组件

// src/react-redux/connect.js
import React from 'react'
import {bindActionCreators} from 'redux'
import {ReactReduxContext} from './Context'

function connect(mapStateToProps, mapDispatchToProps) {
  return function (WrappedComponent) {
    // 返回最终的容器组件
     return class extends React.Component{
    	static contextType = ReactReduxContext
    	constructor(props, context){
      	  super(props)
          this.state = mapStateToProps(context.store.getState())
        }
    	shouldComponentUpdate() {
          if (this.state === mapStateToProps(this.context.store.getState())) {
            return false;
          }
          return true;
        }
    	componentDidMount () {
      	  this.unsubscribe = this.context.subscribe(() => {
        	this.setState(mapStateToProps(this.context.store.getState()))
          })
        }
    	componentWillUnmount (){
      	  this.unsubscribe && this.unsubscribe()
        }
        render(){
          const actions = bindActionCreators(
            mapDispatchToProps,
            this.context.store.dispatch
          )
      	  return <WrappedComponent {...this.state} {...this.props} {...actions}/>
       }
     }
   }
}

export default connect

可以看出,connect 方法中,有 bindActionCreators 绑定 action 与 store.dispatch, 有订阅 store 中的 state 变化,这些都是我们只使用 redux ,需要在 react 组件中需要手动去写的,幸运的是,现在 react-redux 帮我们去干了

三、总结

通过上面的分享,我们终于知道,为什么 react 应用中需要同时引入 redux 和 react-redux 了