• 又不是不能用系列之 Redux 快速入门


    Redux 是普遍用于 React 项目中的状态管理工具,类似 Vue 使用的 Vuex。使用 Redux 需要记住一条至关重要的原则:能不用就不用,强行用非死即残

    Redux 工作流程中的协作方

    React Components 就是开发者自己编写的组件,需要改变状态时创建一个 Action 分发给 Store,需要获取状态时直接从 Store 拿。

    Action Creators 负责创建 Action,实际就是写一堆函数来创建不同的 Action,每个 Action 都是只有两个属性的平平无奇的对象。属性 type 表示这个 Action 要干啥,值永远是字符串;属性 data 表示做 type 表示的这件事需要的数据,值可以是任何类型。

    {
      type: string
      data: any
    }
    

    Store 是整个 Redux 的核心。当收到一个 Action 时负责将其维护的状态的当前值和 Action 打包发给 Reducer 进行状态变更,Reducer 变更结束后保存变更结果。

    Reducers 是个没有感情的打工人,根据 Store 发过来的 Action 对状态进行对应变更并返回给 Store。

    简单用一下 Redux

    假设有个计算器组件 Calculator,它可以加减输入的操作数,最后把计算结果显示出来。在不使用 Redux 的情况下实现如下:

    src/components/Calculator/index.jsx

    import React, { Component } from 'react'
    export default class Calculator extends Component {
      state = {
        result: 0
      }
      handleIncrease = () => {
        const {result} = this.state
        const value = this.node.value
        this.setState({
          result: result + (value * 1)
        })
      }
      handleDecrease = () => {
        const {result} = this.state
        const value = this.node.value
        this.setState({
          result: result - (value * 1)
        })
      }
      render() {
        return (
          <div>
            <h2>当前计算结果:{this.state.result}</h2>
            <input ref={c => this.node = c} type="text" placeholder="操作数"/>&nbsp;&nbsp;
            <button onClick={this.handleIncrease}>加</button>&nbsp;&nbsp;
            <button onClick={this.handleDecrease}>减</button>
          </div>
        )
      }
    }
    

    开始 Redux 改造前记得安装一下 Redux,Redux 相关的文件通常放置在 src/redux 目录下,这样便于管理。

    yarn add redux
    

    上文提到 Action 的 type 属性永远是字符串,并且 Reducer 也要接收这个 Action,那么用脚想一下也知道 type 会到处用。字符串满天飞最可怕的事情是什么?那肯定是同一个字符串在不同的使用位置出现拼写错误。所以通常会在 src/redux 目录下创建一份 JS 文件来专门维护 Action 的 type 。在上面的示例中我们要做的 Action 有两个,一个是「加」,一个是「减」。

    src/redux/constants.js

    export const CalculatorActionType = {
      INCREASE: 'increase',
      DECREASE: 'decrease',
    }
    

    通常全局只需要一份常量文件,所以最好按照不同组件创建对象进行维护,避免出现不同组件之间重名的情况,也避免维护的时候看着满屏的常量懵逼。常量名和对应的值随便写,你自己看得懂又不会被合作的同事打死就行。

    接下来是创建 Action Creator 来生成不同的 Action,实际上就是在一个 JS 文件里面写一堆函数生成 Action 对象。

    src/redux/actions/calculator.js

    import {CalculatorActionType} from '../constants' // 引入常量
    export const increase = data => ({type: CalculatorActionType.INCREASE, data})
    export const decrease = data => ({type: CalculatorActionType.DECREASE, data})
    

    返回值是一个对象,用小括号包裹避免把大括号识别为函数体,还用了对象简写等 JS 的骚操作(作为后端表示记住就行,JS 骚操作太多学不完)。

    有了 Action 接下来就是召唤打工人 Reducer 进行状态变更。Reducer 按照要求一定要是一个「纯函数」,简单理解就是输入参数不允许在函数体中被改变,返回的一定是个新的东西,具体概念自行 wiki。

    src/redux/reducers/calculator.js

    import {CalculatorActionType} from '../constants'
    const initState = 0 // 初始化值
    export default function calculatorReducer(prevState = initState, action) {
      const {type, data} = action
      switch (type) {
        case CalculatorActionType.INCREASE:
          return prevState + data
        case CalculatorActionType.DECREASE:
          return prevState - data
        default:
          return prevState
      }
    }
    

    函数名可以随便改无所谓,第一个参数是要变更状态的当前值,即此时此刻 Store 里面保存的状态值,第二个参数就是上面创建的 Action 对象。函数内部就是 switch 不同的 ActionType 做不同的状态变更。需要注意的是,不要在 Reducer 里面去做网络请求和数据计算,这些工作放到业务组件或者 Action Creator 里完成。不要问为什么,记住就行!Reducer 在 Redux 开始工作时会默认调用一次对状态进行初始化,而第一次调用时 prevStateundefined,所以建议直接给一个初始化值避免恶心的 undefined

    最后就是创建 Redux 中的资本家 Store。创建 Store 之前一定要有 Reducer,否则资本家没有可以 996 压榨的打工人。Store 全局只需要一份。

    src/redux/store.js

    import {createStore} from 'redux'
    import calculatorReducer from './reducers/calculator'
    export default createStore(calculatorReducer)
    

    createStore() 返回一个 Store 对象,所以必须要暴露出去,否则没法在组件里用。到这里 Redux 就能用了,接下来就是去组件里使用。将原来基于组件 state 的示例改造如下:

    src/components/Calculator/index.jsx

    import React, { Component } from 'react'
    import store from '../../redux/store' // 引入 store
    import {increase, decrease} from '../../redux/actions/calculator' // 引入 action
    export default class Calculator extends Component {
      // 订阅 Redux 维护的状态的变化
      componentDidMount() {
        store.subscribe(() => {
          this.setState({})
        })
      }
      handleIncrease = () => {
        const value = this.node.value
        store.dispatch(increase(value * 1)) // 使用 store 分发 action
      }
      handleDecrease = () => {
        const value = this.node.value
        store.dispatch(decrease(value * 1)) // 使用 store 分发 action
      }
      render() {
        return (
          <div>
          	{/* 从 store 上获取维护的状态的值 */}
            <h2>当前计算结果:{store.getState()}</h2>
            <input ref={c => this.node = c} type="text" placeholder="操作数"/>&nbsp;&nbsp;
            <button onClick={this.handleIncrease}>加</button>&nbsp;&nbsp;
            <button onClick={this.handleDecrease}>减</button>
          </div>
        )
      }
    }
    

    对操作数的加减运算通过 Store 实例调用 dispatch() 函数分发 Action 来完成,Action 的创建也是调用对应的函数,value * 1 是为了把字符串转换成数字。 展示计算结果时调用 getState() 函数从 Redux 获取状态值。

    在组件挂载钩子 componentDidMount 中调用 Store 的 subscribe() 函数进行 Store 状态变化的订阅,当 Store 维护的状态发生变化时会触发监听,然后执行一次无意义的组件状态变更,目的是为了触发 render() 函数进行页面重新渲染,否则 Store 中维护的状态值无法更新到页面上。

    补充一点,Action 分为同步和异步两种。如果返回的是一个对象就表示同步 Action,如果返回的是一个函数就表示异步 Action。

    export const increaseAsyn = (data, delay) => {
      return (dispatch) => {
        setTimeout(() => {
          dispatch(increase(data))
        }, delay)
      }
    }
    

    这里用一个定时器来模拟异步任务,实际上这里可以是发起网络请求获取数据。异步 Action 内部一定是调用同步 Action 来触发状态变更。异步 Action 需要单独的库支持:

    yarn add redux-thunk
    

    然后需要对 Store 配置进行修改:

    import {createStore, applyMiddleware} from 'redux' // 引入 applyMiddleware
    import thunk from 'redux-thunk' // 引入 thunk
    import calculatorReducer from './reducers/calculator'
    export default createStore(calculatorReducer, applyMiddleware(thunk)) // 应用 thunk
    

    React-Redux

    Redux 和 React 其实没有一毛钱关系,只是刚好名字长得像而已。但是 Redux 在 React 项目中使用的非常多,所以 React 官方就基于 Redux 做了层封装,目的是让 Redux 更好用,然而我并不觉得好用了多少。

    React-Redux 搞出了「容器组件」和「UI 组件」两个概念。「容器组件」是「UI 组件」的父组件,负责与 Redux 打交道,也就是分发 Action 通知 Store 改变状态值和从 Redux 获取状态值。这样「UI 组件」就完全和 Redux 解耦,原本对 Redux 的直接操作通过「容器组件」代理完成。「容器组件」与「UI 组件」通过 props 进行交互。使用前先安装库:

    yarn add react-redux
    

    改造流程就两步,创建一个「容器组件」,然后把 Calculator 改造成符合规范的「UI 组件」放在「容器组件」中。

    src/containers/Calculator/index.jsx

    import {connect} from 'react-redux'
    import Calculator from '../../components/Calculator/index'
    export default connect()(Calculator)
    

    容器组件通常放在 src/containers 目录下,和「UI 组件」的 src/components 目录进行区分,当然这不是必须的,你开心就好。「容器组件」的创建就是使用 connect() 函数连接「UI 组件」,不要问为什么,记住就行!有了「容器组件」后就不用在 App.js 中使用「UI 组件了」,而应该换成「容器组件」。

    import React, { Component } from 'react'
    import store from './redux/store' // 引入 store
    import Calculator from './containers/Calculator' // 引入容器组件
    export default class App extends Component {
      render() {
        return (
          <div>
            <Calculator store={store}/>
          </div>
        )
      }
    }
    

    记得引入 Store 并通过 props 传递给 Calculator 容器组件,否则没法做状态操作。如果有很多个「容器组件」都需要传递 Store,可以直接在入口文件 index.js 中使用 <Provider> 组件一把梭,这样就不用在 App.js 里写了。

    import React from 'react';
    import ReactDOM from 'react-dom';
    import store from './redux/store' // 引入 store
    import {Provider} from 'react-redux' // 引入 Provider 组件
    import App from './App';
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById('root')
    );
    

    使用 <Provider> 组件还有一个好处是不用使用 store.subscribe() 订阅 Redux 状态变更了,<Provider> 已经提供了这个功能。

    回到容器组件 Calculator 中,connect() 函数可以接收两个函数用于将 Redux 维护的状态和变更状态的行为映射到「UI 组件」的 props 上。

    import {connect} from 'react-redux'
    import Calculator from '../../components/Calculator/index'
    import {increase, decrease} from '../../redux/actions/calculator'
    
    function mapStateToProps(state) {
      return {result: state}
    }
    function mapDispatchToProps(dispatch) {
      return {
        increase: data => dispatch(increase(data)),
        decrease: data => dispatch(decrease(data)),
      }
    }
    export default connect(mapStateToProps, mapDispatchToProps)(Calculator)
    

    mapStateToProps() 将 Redux 维护的状态值映射到「UI 组件」(不是「容器组件」)的 props 上,你别管怎么映射,照着规范写就行了。返回值是一个对象,key 表示映射到 props 后的 key。也就是你在「UI 组件」里调用 this.props.xxx 时那个 xxx 的名字。

    mapDispatchToProps() 将 Redux 操作状态的动作映射到「UI 组件」(不是「容器组件」)的 props 上。返回对象的 keymapStateToProps() 里的 key 一个意思。

    当然这样写有点麻烦,所以可以来个骚操作简写一波:

    import {connect} from 'react-redux'
    import Calculator from '../../components/Calculator/index'
    import {increase, decrease} from '../../redux/actions/calculator'
    
    export default connect(
      state => ({result: state}),
      {
        increase,
        decrease,
      }
    )(Calculator)
    

    这里骚得有点厉害,稍微解释一下。首先是把 mapStateToProps()mapDispatchToProps() 两个函数直接放到参数位置,不在外部定义。然后 React-Redux 支持 mapDispatchToProps() 里直接写 Action,而不用多余写 dispatch()

    export default connect(
      state => ({result: state}),
      {
        increase: increase,
        decrease: decrease,
      }
    )(Calculator)
    

    然后两个创建 Action 的函数的名字刚好和 key 一致,所以使用对象简写语法变成了极简版。到这里就把 Redux 维护的状态值和变更状态的动作都映射到「UI 组件」的 props 上了,接下来改造「UI 组件」。

    src/components/Calculator/index.jsx

    import React, { Component } from 'react'
    export default class Calculator extends Component {
      handleIncrease = () => {
        const value = this.node.value
        this.props.increase(value * 1) // 调用 props 上的方法
      }
      handleDecrease = () => {
        const value = this.node.value
        this.props.decrease(value * 1) // 调用 props 上的方法
      }
    
      render() {
        return (
          <div>
          	{/* 从 props 上取值 */}
            <h2>当前计算结果:{this.props.result}</h2>
            <input ref={c => this.node = c} type="text" placeholder="操作数"/>&nbsp;&nbsp;
            <button onClick={this.handleIncrease}>加</button>&nbsp;&nbsp;
            <button onClick={this.handleDecrease}>减</button>
          </div>
        )
      }
    }
    

    现在就不需要在「UI 组件」上引入 Store 和 Action 了,直接从 props 上调用方法和取值即可。

    原来写一个组件只需要一个文件,引入 React-Redux 后现在需要写两个文件,一份「UI 组件」,一份「容器组件」,多少有些麻烦,而且组件多了以后容易把眼睛看瞎。所以可以把「UI 组件」和「容器组件」写到一份文件里。

    src/containers/Calculator/index.jsx

    import React, { Component } from 'react'
    import {connect} from 'react-redux'
    import {increase, decrease} from '../../redux/actions/calculator'
    // UI 组件不暴露
    class Calculator extends Component {
      handleIncrease = () => {
        const value = this.node.value
        this.props.increase(value * 1)
      }
      handleDecrease = () => {
        const value = this.node.value
        this.props.decrease(value * 1)
      }
      render() {
        return (
          <div>
            <h2>当前计算结果:{this.props.result}</h2>
            <input ref={c => this.node = c} type="text" placeholder="操作数"/>&nbsp;&nbsp;
            <button onClick={this.handleIncrease}>加</button>&nbsp;&nbsp;
            <button onClick={this.handleDecrease}>减</button>
          </div>
        )
      }
    }
    // 只暴露容器组件
    export default connect(
      state => ({result: state}),
      {
        increase,
        decrease,
      }
    )(Calculator)
    

    关键点就一个,不要暴露「UI 组件」,只暴露「容器组件」。凡是需要和 Redux 交互数据的组件都放 src/containers 目录下,其他不需要和 Redux 交互的都放 src/components 目录下。

    Redux 管理多个状态

    上面的例子中 Redux 都只管理了一个数字类型的状态,实际开发中 Redux 肯定要管理各种类型五花八门的状态。比如现有另外一个书架组件 BookShelf,它需要委托 Redux 管理所有的书籍。按照套路,先在定义所有操作书籍的 ActionType。

    src/redux/constants.js

    export const CalculatorActionType = {
      INCREASE: 'increase',
      DECREASE: 'decrease',
    }
    export const BookShelfActionType = {
      ADD: 'add' // 添加图书
    }
    

    然后创建 Action Creator 生成 Action:

    import {BookShelfActionType} from '../constants'
    export const add = data => ({type: BookShelfActionType.ADD, data})
    

    然后写 Reducer 的状态变更逻辑:

    import {BookShelfActionType} from '../constants'
    const initState = [
      {id: '001', title: 'React 从入门到入土', auther: '子都'}
    ]
    export default function bookshelfReducer(prevState = initState, action) {
      const {type, data} = action
      switch (type) {
        case BookShelfActionType.ADD:
          return [data, ...prevState] // 这里用了展开运算符
        default:
          return prevState
      }
    }
    

    上文已经说过,store.js 全局只会有一份,这时候出现了两个 Reducer 就需要做一些处理。

    import {createStore, applyMiddleware, combineReducers} from 'redux'
    import thunk from 'redux-thunk'
    
    import calculatorReducer from './reducers/calculator'
    import bookshelfReducer from './reducers/bookshelf'
    const combinedReducers = combineReducers(
      {
        result: calculatorReducer,
        books: bookshelfReducer,
      }
    )
    export default createStore(combinedReducers, applyMiddleware(thunk))
    

    使用 combineReducers() 函数将多份 Reducer 合并为一个,使用合并后的 Reducer 创建 Store。这里要注意 combineReducers() 函数中的对象参数,这里的概念绕不清那基本就废了。

    首先要明确,每个 Reducer 只能管理一个状态,这里的「一个」可以是一个数字、数组、对象、字符串等等,反正只能是「一个」。那么 combineReducers() 中,key 就是你给状态取的名字,而 value 就是管理这个状态的 Reducer。这里的命名会影响你在「UI 组件」通过 props 获取状态值时的 key

    多个 Reducer 合并后,Calculator 「容器组件」的取值就不能直接取 state,而是取 state 上的 result:

    export default connect(
      state => ({result: state.result}), // 从 state 上取 result
      {
        increase,
        decrease,
      }
    )(Calculator)
    

    最后看一下新增的 BookShelf 组件的结构,仅简单做了数据展示:

    src/containers/BookShelf/index.jsx

    import React, { Component } from 'react'
    import { connect } from 'react-redux'
    import {add} from '../../redux/actions/bookshelf'
    class BookShelf extends Component {
      render() {
        return (
          <div>
            <ul>
              {
                this.props.books.map((book) => {
                  return <li key={book.id}>{book.name} --- {book.auther}</li>
                })
              }
            </ul>
          </div>
        )
      }
    }
    export default connect(
      state => ({books: state.books}), // 从 state 的 books 上取值,对应 store 中合并 Reducer 时的 key
      {
        add
      }
    )(BookShelf)
    

    最后

    记住本文开篇的那句话,Redux 能不用就不用,强行用非死即残

  • 相关阅读:
    ES6 => 箭头函数
    从零开始, 探索transition(vue-2.0)
    从零开始,零基础,一点一点探索vue-router(vue2.0)
    解决Vue请求 ‘No 'Access-Control-Allow-Origin' header is present on the requested resource’错误
    超详细,用canvas在微信小程序上画时钟教程
    钱兔
    天天飞燕
    小鱼
    键盘处理
    兼容iOS 10 资料整理笔记
  • 原文地址:https://www.cnblogs.com/zidu/p/redux-fast-learn.html
Copyright © 2020-2023  润新知