手动实现react-redux及代码展示

400 阅读16分钟

为什么要使用react-redux

  1. react 是单向数据流的,父组件总是通过props传递数据给子组件。
  2. 每当有一些数据要在很多组件中使用的时候,而这些组件跨层级时,props 就不再适合。就需要把这些数据提取出来放在一个模块,我们把它称为公共状态,每当数据改变时,它会通知订阅它的所有组件

什么时候要用react-redux

  1. 像父子组件之间相互传值相互调用的情况,并且值的适用范围仅限于父子组件之间,这时不需要使用Redux.
  2. 当某个子组件去更新某种状态时,比如更新公共数据。而其他的页面又需要依赖这些数据时,此时可以考虑使用redux,把这些状态值放入到redux中进行管理。

redux的使用场景

  • 组件需要根据状态发生显示变化(主题颜色的切换)
  • 存在组件需要更新全局状态
  • 存在组件需要更新另一个组件的状态
  • 存在状态以许多不同的方式更新
  • 状态树结构复杂
  • 某个状态需要在全局使用或共享(例如角色权限等信息)

redux的工作原理

Redux 和 React-redux 的区别

Redux 是一种架构模式(Flux 架构的一种变种),它不关注你到底用什么库,你可以把它应用到 React 和 Vue,甚至跟 jQuery 结合都没有问题。 而 React-redux 就是把 Redux 这种架构模式和 React.js 结合起来的一个库,就是 Redux 架构在 React.js 中的体现。

利用react提供的api实现react-redux

demo演示(Counter) 全局存储count,通过dispatch(action)改变count,state数据变化,触发component更新

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './models/index';
import Index from './Counter'
ReactDOM.render(
  <Provider store={store}>
    <Index />
  </Provider>,
  document.getElementById('root')
)

count.js

class Counter extends React.Component {
  render() {
    const { count, increase, decrease } = this.props;
    // console.log('props', this.props)
    return (
      <div>
        <h1>Count : {count}</h1>
        <button onClick={increase}>Increase</button>
        <button onClick={decrease}>decrease</button>
      </div>
    );
  }
}
const mapStateToProps = state => {
    return {
      count: state.counter.count,
    }
}
const mapDispatchToProps =  dispatch => ({
    increase: () => dispatch(increase(1)),
    decrease: () => dispatch(decrease(1))
})
export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Counter);

store

import { createStore, combineReducers } from 'redux';
import counter from './counter/reducer';
const store = createStore(combineReducers({
    counter
  }));
export default store

reducer

import { INCREMENT, DECREMENT } from "./action";
const initState = {
  count: 0,
  loading: false,
  dataSource: {
    list: [],
    pagination: {
      pageSize: 10,
      current: 1,
    },
  },
}
export default (state = initState, action) => {
  const { type, payload } = action;
  switch (type) {
    case INCREMENT:
      return { ...state, count: state.count + payload };
    case DECREMENT:
      return { ...state, count: state.count - payload };
    default:
      return state;
  }
};

要了解react-redux,首先你需要知道context (你可能永远用不上的 React.js 特性Context)

React其实提供了一个全局注入变量的API,这就是context api。假如我现在有一个需求是要给我们所有组件传一个文字颜色的配置,我们的颜色配置在最顶级的组件上,当这个颜色改变的时候,下面所有组件都要自动应用这个颜色。那我们可以使用context api注入这个配置: 假设我们有这样的一个组件树

假设原来主题色是绿色,那么 Index 上保存的就是 this.state = { themeColor: 'green' }。如果要改变主题色,可以直接通过 this.setState({ themeColor: 'red' }) 来进行。这样整颗组件树就会重新渲染,子组件也就可以根据重新传进来的 props.themeColor 来调整自己的颜色。

但这里的问题也是非常明显的,我们需要把 themeColor 这个状态一层层手动地从组件树顶层往下传,每层都需要写 props.themeColor。如果我们的组件树很层次很深的话,这样维护起来简直是灾难。

如果这颗组件树能够全局共享这个状态就好了,我们要的时候就去取这个状态,不用手动地传:

就像这样,Index 把 state.themeColor 放到某个地方,这个地方是每个 Index 的子组件都可以访问到的。当某个子组件需要的时候就直接去那个地方拿就好了,而不需要一层层地通过 props 来获取。不管组件树的层次有多深,任何一个组件都可以直接到这个公共的地方提取 themeColor 状态。

实现React.js 的 context

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

2.我们修改 Index,让它往自己的 context 里面放一个 themeColor

class Index extends Component {
    static childContextTypes = {
      themeColor: PropTypes.string
    }
    constructor () {
      super()
      this.state = { themeColor: 'red' }
    }
    componentWillMount () {
        this.setState({ themeColor: 'green' })
    }
    getChildContext () {
      return { themeColor: this.state.themeColor }
    }
    render () {
      return (
        <div>
          <Header />
          <Main />
        </div>
      )
    }
  }
  export default Index

3.子组件获取themecolor

class Title extends Component {
    static contextTypes = {
        themeColor: PropTypes.string
      }
    render () {
      return (
        <h1 style={{ color: this.context.themeColor }}>React-redux 标题</h1>
      )
    }
  }
  export default Title

4.如果我们要改颜色,只需要在 Index 里面 setState 就可以了

componentWillMount () {
    this.setState({ themeColor: 'green' })
}

总结 1.一个组件可以通过 getChildContext 方法返回一个对象,这个对象就是子树的 context,提供 context 的组件必须提供 childContextTypes 作为 context 的声明和验证。

2.如果一个组件设置了 context,那么它的子组件都可以直接访问到里面的内容,它就像这个组件为根的子树的全局变量。任意深度的子组件都可以通过 contextTypes 来声明你想要的 context 里面的哪些状态,然后可以通过 this.context 访问到那些状态。

3.context 打破了组件和组件之间通过 props 传递数据的规范,极大地增强了组件之间的耦合性。而且,就如全局变量一样,context 里面的数据能被随意接触就能被随意修改,每个组件都能够改 context 里面的内容会导致程序的运行不可预料。

creatStore又是什么(实现react-redux中创建store的必要条件)

function createStore (stateChanger) {
  let state = null
  const getState = () => state
  const dispatch = (action) => stateChanger(state, action)
  return { getState, dispatch }
}

现在我们把全局状态集中到一个地方,给这个地方起个名字叫做 store,然后构建一个函数 createStore,用来专门生产这种 state 和 dispatch 的集合,这样别的 App 也可以用这种模式了:

createStore 接受一个参数,stateChanger,它来描述应用程序状态会根据 action 发生什么变化,其实就是dispatch 代码里面的内容。

createStore 会返回一个对象,这个对象包含两个方法 getState 和 dispatch。getState 用于获取 state 数据,其实就是简单地把 state 参数返回。

dispatch 用于修改数据,和以前一样会接受 action,然后它会把 state 和 action 一并传给 stateChanger,那么 stateChanger 就可以根据 action 来修改 state 了。

思考????我们每次通过 dispatch 修改数据的时候,其实只是数据发生了变化,如果我们不手动调用 renderApp,页面上的内容是不会发生变化的。但是我们总不能每次 dispatch 的时候都手动调用一下 renderApp,我们肯定希望数据变化的时候程序能够智能一点地自动重新渲染数据,而不是手动调用。

我们希望用一种通用的方式“监听”数据变化,然后重新渲染页面,这里要用到观察者模式。修改 createStore

function createStore (stateChanger) {
  let state = null
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = stateChanger(state, action)
    listeners.forEach((listener) => listener())
  }
  dispatch({})
  return { getState, dispatch, subscribe }
}

我们在 createStore 里面定义了一个数组 listeners,还有一个新的方法 subscribe,可以通过 store.subscribe(listener) 的方式给 subscribe 传入一个监听函数,这个函数会被 push 到数组当中。

我们修改了 dispatch,每次当它被调用的时候,除了会调用 stateChanger 进行数据的修改,还会遍历 listeners 数组里面的函数,然后一个个地去调用。相当于我们可以通过 subscribe 传入数据变化的监听函数,每当 dispatch 的时候,监听函数就会被调用,这样我们就可以在每当数据变化时候进行重新渲染:

纯函数reducer(实现react-redux中改变store的必要条件)

createStore 接受一个叫 reducer 的函数作为参数,这个函数规定是一个纯函数,它接受两个参数,一个是 state,一个是 action。

如果没有传入 state 或者 state 是 null,那么它就会返回一个初始化的数据。如果有传入 state 的话,就会根据 action 来“修改“数据。

reducer 是不允许有副作用的。你不能在里面操作 DOM,也不能发 Ajax 请求,更不能直接修改 state,它要做的仅仅是 —— 初始化和计算新的 state。 现在我们可以用这个 createStore 来构建不同的 store 了,只要给它传入符合上述的定义的 reducer 即可:

function themeReducer (state, action) {
  if (!state) return {
    themeName: 'Red Theme',
    themeColor: 'red'
  }
  switch (action.type) {
    case 'UPATE_THEME_NAME':
      return { ...state, themeName: action.themeName }
    case 'UPATE_THEME_COLOR':
      return { ...state, themeColor: action.themeColor }
    default:
      return state
  }
}
const store = createStore(themeReducer)

手动实现react-redux

我们可用把共享状态放到父组件的 context 上,这个父组件下所有的组件都可以从 context 中直接获取到状态而不需要一层层地进行传递了。但是直接从 context 里面存放、获取数据增强了组件的耦合性;并且所有组件都可以修改 context 里面的状态就像谁都可以修改共享状态一样,导致程序运行的不可预料。 既然这样,为什么不把 context 和 store 结合起来?毕竟 store 的数据不是谁都能修改,而是约定只能通过 dispatch 来进行修改,这样的话每个组件既可以去 context 里面获取 store 从而获取状态,又不用担心它们乱改数据了。

demo演示(store与context结合) 1 用 create-react-app 新建一个工程,然后安装一个 React 提供的第三方库 prop-types 2 src/ 目录下新增三个文件:Header.js、Content.js、ThemeSwitch.js。 3 运行基础页面 4 构建store及reducer

function createStore(reducer) {
  let state = null
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = reducer(state, action)
    // console.log('listeners', listeners)
    listeners.forEach((listener) => listener())
  }
  dispatch({}) // 初始化 state
  return { getState, dispatch, subscribe }
}

const themeReducer = (state, action) => {
    if (!state) return {
      themeColor: 'red'
    }
    switch (action.type) {
      case 'CHANGE_COLOR':
        return { ...state, themeColor: action.themeColor }
      default:
        return state
    }
  }
  
  const store = createStore(themeReducer)
  export default store

5 们把 store 放到 Index 的 context 里面,这样每个子组件都可以获取到 store 了

import Header from './Header'
import Content from './Content'
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import store from './store'

class Index extends Component {
    static childContextTypes = {
        store: PropTypes.object
    }
    getChildContext() {
        return { store }
    }
    render() {
        return (
            <div>
                <Header/>
                <Content />
            </div>
        )
    }
}
export default Index

6 各组件获取状态,主题色生效

import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Header extends Component {
  static contextTypes = {
    store: PropTypes.object
   }
   constructor (props) {
    super(props)
    this.state = { themeColor: '' }
  }

  componentWillMount () {
    const { store } = this.context
    this._updateThemeColor()
    store.subscribe(() => this._updateThemeColor())

  }

  _updateThemeColor () {
    const { store } = this.context
    const state = store.getState()
    this.setState({ themeColor: state.themeColor })
  }

  render () {
    return (
      <h1 style={{ color: this.state.themeColor }}>title: React-redux</h1>
    )
  }
}
const mapStateToProps = (state, props) => {
    return {
      themeColor: state.themeColor
    }
}

export default Header

7 实现点击按钮,主题色变化,react-redux生效(注意点:点击按钮改变state的数据之后,要想页面渲染生效,必须在componentWillMount 生命周期中加入store.subscribe(() => this._updateThemeColor()))

handleSwitchColor(color) {
    const { store } = this.context
    store.dispatch({
        type: 'CHANGE_COLOR',
        themeColor: color
    })
}

8.观察刚才几步操作的弊端

  1. 有大量重复的逻辑:它们基本的逻辑都是,取出 context,取出里面的 store,然后用里面的状态设置自己的状态,这些代码逻辑其实都是相同的。

  2. 对 context 依赖性过强:这些组件都要依赖 context 来取数据,使得这个组件复用性基本为零。想一下,如果别人需要用到里面的 ThemeSwitch 组件,但是他们的组件树并没有 context 也没有 store,他们没法用这个组件了。

对于第一个问题,就引入了react-redux中的高阶组件connect

手动实现react-redux中的connect

目标: 希望connect帮助我们从 context 取数据,把里面数据取出来通过 props 传给 相应的 组件。 import React, { Component } from 'react' import PropTypes from 'prop-types' export connect = (WrappedComponent) => { class Connect extends Component { static contextTypes = { store: PropTypes.object } // TODO: 如何从 store 取数据? render () { return } } return Connect } connect 函数接受一个组件 WrappedComponent 作为参数,把这个组件包含在一个新的组件 Connect 里面,Connect 会去 context 里面取出 store。现在要把 store 里面的数据取出来通过 props 传给 WrappedComponent 但是每个传进去的组件需要 store 里面的数据都不一样的,所以除了给高阶组件传入 Dumb 组件以外,还需要告诉高级组件我们需要什么数据,高阶组件才能正确地去取数据。为了解决这个问题,我们可以给高阶组件传入类似下面这样的函数: const mapStateToProps = (state) => { return { themeColor: state.themeColor, themeName: state.themeName, fullName: ${state.firstName} ${state.lastName} ... } }

这个函数会接受 store.getState() 的结果作为参数,然后返回一个对象,这个对象是根据 state 生成的。mapStateToProps 相当于告知了 Connect 应该如何去 store 里面取数据,然后可以把这个函数的返回结果传给被包装的组件:

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)
        : {} 
        // 防止 mapStateToProps 没有传入
      let dispatchProps = mapDispatchToProps
        ? mapDispatchToProps(store.dispatch, this.props)
        : {} 
        // 防止 mapDispatchToProps 没有传入
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render() {
      const { store } = this.context
      let stateProps = mapStateToProps(store.getState())
      // {...stateProps} 意思是把这个对象里面的属性全部通过 `props` 方式传递进去
      return <WrappedComponent {...this.state.allProps} />
    }
  }

  return Connect
}

connect 现在是接受一个参数 mapStateToProps,然后返回一个函数,这个返回的函数才是高阶组件。它会接受一个组件作为参数,然后用 Connect 把组件包装以后再返回。 connect 的用法是:

const mapStateToProps = (state) => {
  return {
    themeColor: state.themeColor
  }
}
Header = connect(mapStateToProps)(Header)

demo演示(connect) 1 增加react-redux文件,暴露connect 2 修改header、content、themeswitch文件 3 实现mapDispatchToProps 原themeSwitch

handleSwitchColor (color) {
  const { store } = this.context
  store.dispatch({
    type: 'CHANGE_COLOR',
    themeColor: color
  })
}

想一下,既然可以通过给 connect 函数传入 mapStateToProps 来告诉它如何获取、整合状态,我们也可以想到,可以给它传入另外一个参数来告诉它我们的组件需要如何触发 dispatch。我们把这个参数叫 mapDispatchToProps

const mapDispatchToProps = (dispatch) => {
  return {
    onSwitchColor: (color) => {
      dispatch({ type: 'CHANGE_COLOR', themeColor: color })
    }
  }
}

和 mapStateToProps 一样,它返回一个对象,这个对象内容会同样被 connect 当作是 props 参数传给被包装的组件。不一样的是,这个函数不是接受 state 作为参数,而是 dispatch,你可以在返回的对象内部定义一些函数,这些函数会用到 dispatch 来触发特定的 action 1 themeSwitch调整connect

import { connect } from './connect'

2 调整themeSwitch使用mapDispatchToProps

const mapDispatchToProps = (dispatch) => {
    return {
        onSwitchColor: (color) => {
            dispatch({ type: 'CHANGE_COLOR', themeColor: color })
        }
    }
}
ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch)

手动实现Provider

我们要把 context 相关的代码从所有业务组件中清除出去,现在的代码里面还有一个地方是被污染的。那就是 src/mainIndex.js 里面的 Index

class Index extends Component {
  static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext () {
    return { store }
  }
  render () {
    return (
      <div>
        <Header />
        <Content />
      </div>
    )
  }
}

其实它要用 context 就是因为要把 store 存放到里面,好让子组件 connect 的时候能够取到 store。我们可以额外构建一个组件来做这种脏活,然后让这个组件成为组件树的根节点,那么它的子组件都可以获取到 context 了。 我们把这个组件叫 Provider,因为它提供(provide)了 store:

provider代码

export class Provider extends Component {
  static propTypes = {
    store: PropTypes.object,
    children: PropTypes.any
  }

  static childContextTypes = {
    store: PropTypes.object
  }

  getChildContext () {
    return {
      store: this.props.store
    }
  }

  render () {
    return (
      <div>{this.props.children}</div>
    )
  }
}

Provider 做的事情也很简单,它就是一个容器组件,会把嵌套的内容原封不动作为自己的子组件渲染出来。它还会把外界传给它的 props.store 放到 context,这样子组件 connect 的时候都可以获取到。 demo演示(provider)

React16.3新版API实现recat-redux

  1. React.createContext 方法用于创建一个 Context 对象。该对象包含 Provider 和 Consumer两个属性,分别为两个 React 组件。
  2. Provider 组件。用在组件树中更外层的位置。它接受一个名为 value 的 prop,其值可以是任何 JavaScript 中的数据类型。
  3. Consumer 组件。可以在 Provider 组件内部的任何一层使用。它接收一个函数做为children。这个函数的参数是 Provider 组件接收的那个 value 的值,返回一个 React 元素。
let ThemeContext = React.createContext({
    style: {
        color: "green",
        background: "red",
    }
});
function Button({ theme, ...props }) {
    return (
        <button {...props} >我是按钮</button>
    );
}
function ThemedButton(props) {
    return (
        /*
        * Consumer会自动获取离其最近的Provider提供的value
        * */
        <ThemeContext.Consumer>
            {
                (context) => {
                    return <Button {...props} style={context.style} />;
                }
            }
        </ThemeContext.Consumer>
    );
}
function Toolbar(props) {
    return (
        <ThemeContext.Provider value={{
            style: {
                color: "red",
                background: "green",
            }
        }}>
            <ThemedButton />
        </ThemeContext.Provider>
    );
}
ReactDOM.render(
    <Toolbar />,
    document.getElementById("root")
);

这版 Context API 的几个特点:

  1. Provider 和 Consumer 必须来自同一次 React.createContext 调用。也就是说 NameContext.Provider 和 AgeContext.Consumer 是无法搭配使用的。
  2. React.createContext 方法接收一个默认值作为参数。当 Consumer 外层没有对应的 Provider 时就会使用该默认值。
  3. Provider 组件的 value prop 值发生变更时,其内部组件树中对应的 Consumer 组件会接收到新值并重新执行 children 函数。此过程不受 shouldComponentUpdete 方法的影响。
  4. Provider 组件利用 Object.is 检测 value prop 的值是否有更新。注意 Object.is 和 === 的行为不完全相同。具体细节请参考 Object.is 的 MDN 文档页
  5. Consumer 组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。

新版api实现provider及connect demo演示(新版api) context.js

import React from "react";
export default React.createContext();

provider.js

import React,{Component} from "react";
import ReduxContext from './context'
import PropTypes from 'prop-types'
export default class Provider extends Component {
  static propTypes = {
    store: PropTypes.object,
    children: PropTypes.any
  }

  constructor(props) {
    super(props)
  }
  render(){
    return (
      <ReduxContext.Provider value={this.props.store }>
        {this.props.children}
      </ReduxContext.Provider>
    )
  }
}

connect.js

import React, { Component } from 'react'
import ReduxContext from './context'

export function connect(mapStateToProps, mapDispatchToProps) {
    return function (WrapComponent) {
      class ConnectComponent extends Component {
        constructor(props) {
          super(props)
          this.state = {
            props: {}
          }
        }
  
        componentDidMount() {
          const { store } = this.props
          store.subscribe(() => this.update())
          this.update()
        }
  
        update() {
          const { store } = this.props
          let stateToProps = mapStateToProps(store.getState())
          let dispatchToProps
          if (typeof mapDispatchToProps === 'function') {
            dispatchToProps = mapDispatchToProps(store.dispatch)
          } else {
            // 传递了一个 actionCreator 对象过来
            dispatchToProps = {}
          }
  
          this.setState({
            props: {
              ...this.state.props,
              ...stateToProps,
              ...dispatchToProps,
            },
          })
        }
  
        render() {
          return <WrapComponent {...this.state.props} />
        }
      }
      
      return () => (
        
        <ReduxContext.Consumer>
          {
          value => {
            return  <ConnectComponent store={value} />
          }
         }
        </ReduxContext.Consumer>
      )
    }
  }

总结

  1. context Context 提供了一种在组件之间共享状态值的方式,而不必显式地通过组件树的逐层传递 props

  2. createStore 我们把全局状态集中到一个地方,给这个地方起个名字叫做 store,然后构建一个函数 createStore,用来专门生产这种 state 和 dispatch 的集合

  3. reducer 这个函数定义怎样修改state里面的状态

  4. 结合context与store 有大量重复的逻辑,获取state,修改当前状态,不够有复用性,抽离成connect

  5. connect 我们尝试通过构建一个高阶组件 connect 函数的方式,把所有的重复逻辑和对 context 的依赖放在里面 connect 函数里面,而其他组件保持 Pure(Dumb) 的状态,让 connect 跟 context 打交道,然后通过 props 把参数传给普通的组件。 mapStateTopProps 相当于告知了 Connect 应该如何去 store 里面取数据,然后可以把这个函数的返回结果传给被包装的组件 mapDispatchToProps它返回一个对象,这个对象内容会同样被 connect 当作是 props 参数传给被包装的组件,参数dispatch,你可以在返回的对象内部定义一些函数,这些函数会用到 dispatch 来触发特定的 action

  6. provider 我们构建了一个 Provider 组件。Provider 作为所有组件树的根节点,外界可以通过 props 给它提供 store,它会把 store 放到自己的 context 里面,好让子组件 connect 的时候都能够获取到。

redux 优点

  1. 流程规范,按照官方推荐的规范和结合团队风格打造一套属于自己的流程。
  2. 函数式编程,在 reducer 中,接受输入,然后输出,不会有副作用发生,幂等性。
  3. 可追踪性,很容易追踪产生 BUG 的原因。

redux 缺点

  1. 流畅太繁琐,需要写各种 type、action、reducer,不易上手,初学者可能会觉得比较绕。
  2. 同步数据流,异步需要引用其他库(redux-thunk/redux-saga 等)或者通过异步 dispatch 的技巧。

参考文章:

juejin.cn/post/684490…

huziketang.mangojuice.top/books/react…