React学习 --- 深入浅出redux(上)

286 阅读7分钟

纯函数

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;

如果一个函数需要成为纯函数,必须满足以下两点:

  1. 确定的输入,一定会产生确定的输出
  2. 函数在执行过程中,不能产生副作用 (也就是不可以修改函数作用域以外的值)
// 纯函数
function sum(num1, num2) {
  return num1 + num2
}
// 不是纯函数
let num = 30
function add(param) {
  // num是变量,在程序运行过程中,num的值可能会发生改变
  // 传入相同的param值,返回的结果可能是不一样的
  // 所以add函数不是一个纯函数
  return param + num
}
// 纯函数
const num = 30
function add(param) {
  // 因为num是常量
  // 在整个代码运行过程中,num的值不会发生改变
  // 所以相同的param传入一定会产生相同的返回值
  return param + num
}

纯函数对于确定的输入一定会有确定的输出,而且不会产生任何的副作用

所以对于纯函数而言,纯函数和其它函数之间的耦合度是非常低的

所以React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改

这样就可以保证react的单向数据流,也就是所有对于父组件提供的状态的修改都一定在父组件中

不会出现子组件修改父组件中状态的情况,这样可以避免bug的出现,也减低了后期维护的难度

Redux

JavaScript开发的应用程序,已经变得越来越复杂了:

  • JavaScript需要管理的状态越来越多,越来越复杂
  • 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,(比如某些元素是否被选中, 是否显示加载动效,当前分页)

而管理不断变化的state是非常困难的

  • 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化
  • 所以当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪

虽然React在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理

Redux就是一个帮助我们管理State的容器: Redux是JavaScript的状态容器,提供了可预测的状态管理

我们可以通过Redux来统一管理项目中的所有需要在多个组件间共享的状态,并统一对对应的状态进行操作

从而取代原本的在项目中不同的地方对对应的状态进行修改的方式。

以便于对这些需要在多组件间共享的状态的操作更好的维护,和调试

Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)

三大原则

单一数据源:

  • 整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中
  • 虽然Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
  • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改

State是只读的

  • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State

  • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改stat

  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题

    也就是多个地方同时操作同一个store,而产生冲突问题

使用纯函数来执行修改

  • 通过reducer将 旧state和 actions联系在一起,并且返回一个新的State
  • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分
  • 但是所有的reducer都应该是纯函数,不应该产生任何的副作用

基本使用

// Redux可以单独使用,不需要和React进行捆绑
// 所以先尝试在node环境下 单独使用Redux

const redux = require('redux')

// 实际管理数据的地方
const initialStore = {
  count: 0
}


// redux 三要素之一: reducer
// 必须是一个纯函数
// 用于关联store和action
// reducer做的事情就是将传入的state和action结合起来生成一个新的state

// 对于参数store 每执行一次action,store就会传入最新的那个store
// 而对于第一次执行的时候, store就是undefined,所以会使用initialStore
// reducer函数的功能就是根据不同的action更新store,并返回最新的store
function reducer(store = initialStore, action) {
  switch (action.type) {
    case 'INCREMENT':
      // count属性会覆盖原本store中的count属性
      return { ...store, count: store.count + 1 }
    case 'ADD':
      return { ...store, count: store.count + action.num }
    default:
      return store
  }
}

// redux 三要素之一: store
// 用于对需要共享的状态进行统一存储
const store = redux.createStore(reducer)

// redux 三要素之一: action
// 所有数据的变化,必须通过派发(dispatch)action来更新
// 而action本质其实就是一个JS对象
// 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的
const incrementAction = {
  type: 'INCREMENT' // type是必填项 一般大写 表示你需要进行什么样的操作
}

const addNumAction = {
  type: 'ADD',
  num: 10 // 这里传递的就是参数
}


// 订阅store的变化
// 一定要在redux派发对应的action之前就订阅store的变化
store.subscribe(() => {
  // 我们可以通过store.getState()来获取最新的那个store对象
  console.log('store', store.getState().count)
})

store.dispatch(incrementAction)
store.dispatch(addNumAction)

但是在实际开发过程中,我们在使用的时候,使用的是ESM,而且需要将代码进行拆分操作

代码拆分

package.json

{
  "name": "tmp",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  // 在node中使用ESM
  // 在node中使用ESM后,node不会再自动为对应的js文件添加js后缀,需要手动添加
  "type": "module", 
  "dependencies": {
    "redux": "^4.1.2"
  }
}

index.js --- 业务代码

import store from './store/index.js'

import {
  incAction,
  addAction
} from './store/actions.js'

store.subscribe(() => {
  console.log('store', store.getState())
})

store.dispatch(incAction())
store.dispatch(addAction(10))

store/index.js --- store的入口文件

import redux from 'redux'

import reducer from './reducer.js'

export default redux.createStore(reducer)

store/consts.js

// 将所有的常量都定义在这里
// 以确保某一个常量值发生改变的时候
// 所有使用这个常量的地方都会发生对应的改变

export const INCREMENT = 'INCREMENT'
export const ADD_NUMBER = 'ADD_NUMBER'

store/reducer.js

// reducer函数
import {
  INCREMENT,
  ADD_NUMBER
} from './consts.js'

const initialStore = {
  count: 0
}

export default function reducer(store = initialStore, action) {
  switch(action.type) {
    case INCREMENT:
      return { ...store, count: store.count + 1 }

    case ADD_NUMBER:
      return { ...store, count: store.count + action.num }

    default:
      return store
  }
}

store/actions.js

// 定义所有的action
import {
  ADD_NUMBER,
  INCREMENT
} from './consts.js' // js后缀不可以省略

export const addAction = num => ({
  type: ADD_NUMBER,
  num
})

// 虽然以下action不需要传递参数
// 但是为了整体统一,我们依旧会将其定义为函数形式
export const incAction = () => ({
  type: INCREMENT
})

使用流程

LPhnEp.png

结合React

需求

LPykJt.png

当点击+5-5按钮的时候,HomeProfile后面的数值都会发生相同的改变

实现

业务代码

App.js

import { PureComponent } from 'react'

import Home from './components/Home'
import Profile from './components/Profile'

import store from '../store'
import {
  add_number,
  sub_number
} from '../store/action'

export default class App extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      count: store.getState().count
    }
  }

  render() {
    return (
      <>
        <Home />
        <Profile />
        <button onClick={() => this.add(5)}>+5</button>
        <button onClick={() => this.sub(5)}>-5</button>
      </>
    )
  }

  add(num) {
    store.dispatch(add_number(num))
  }

  sub(num) {
    store.dispatch(sub_number(num))
  }
}

Home/Profile组件

import { PureComponent } from 'react'

import store from '../../store'

export default class Profile extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      count: store.getState().count
    }
  }

  // 在componentDidMount中订阅store
  // 确保store发生更新后,界面发生更新
  componentDidMount() {
    store.subscribe(() => {
      this.setState({
        count: store.getState().count
      })
    })
  }

  render() {
    return (
      <div>
        Home: { this.state.count }
      </div>
    )
  }
}

store

index.js

import { createStore } from 'redux'

import reducer from './reducer'

export default createStore(reducer)

store.js

export const ADD_NUMBER = 'ADD_NUMBER'
export const SUB_NUMBER = 'SUB_NUMBER'

action.js

import {
  ADD_NUMBER,
  SUB_NUMBER
} from './consts'


export const add_number = num => ({
  type: ADD_NUMBER,
  num
})

export const sub_number = num => ({
  type: SUB_NUMBER,
  num
})

reducer.js

import {
  ADD_NUMBER,
  SUB_NUMBER
} from './consts'

const initialStore = {
  count: 0
}

export default function reducer(store = initialStore, action) {

  switch (action.type) {
    case ADD_NUMBER:
      return { ...store, count: store.count + action.num }

    case SUB_NUMBER:
      return { ...store, count: store.count - action.num }

     default:
      return store
  }
}