React学习笔记[16]✨~Redux的基本使用👻

164 阅读12分钟

我正在参加「掘金·启航计划」

想要学习react-redux、ReduxToolkit,最好先了解一下redux,这对于后续学习会有很有帮助~

0、学习Redux前需要了解的

关于纯函数

纯函数是函数式编程中一个很重要的概念,纯函数也是React中多次提及的

  • React中组件就被要求要像一个纯函数(为什么说要像,是因为有类组件)
  • Redux中有一个Reducer的概念也要求必须是一个纯函数

什么是纯函数

纯函数要符合以下条件:

确定的输入,一定会产生确定的输出

函数在执行过程中不能产生副作用(所谓的副作用指的是,在调用函数的同时产生了一些附加的影响,比如说修改了全局的变量、修改了参数或者改变了外部存储的内容

纯函数在执行过程中不会产生副作用,这样就会有效避免一些不必要的问题

比如以下函数就是一个纯函数:

function add(a, b) {
  return a + b;
}

对于这个函数来说,输入相同的数据便会产生相同的输出,而且没有改变参数等副作用。

再比如对于数组相关函数来说,slice就是一个纯函数(不会对原数组进行任何操作,而是产生一个新数组);而splice则不是一个纯函数(因为会对原数组进行修改)

数组中的push、pop、shift、unshift、sort、reverse、splice等均不是纯函数

对于依赖外层作用域数据的函数也不是纯函数,因为这个外层作用域数据有可能会被修改掉,这样就无法保证相同的输入产生相同的输出,比如:

let a = 1;
function add(num) {
  return num + a
}
console.log(add(2));
a = 2;
console.log(add(2));

当参数传入的是一个对象时,在函数内部去修改对象上的属性这个函数也不属于纯函数,因为在一个地方调取了这个函数后,在其他地方使用了这个对象的话会产生不可预测的问题(因为这个函数将这个对象内部数据修改掉了),我们也不能够知道这个对象之前内部的数据是怎样的

如果想将以上函数变为纯函数,需要写成以下的方式:

我们需要先拷贝一份原有对象中的数据,然后再添加新的属性

对于数组的操作,同样也需要先拷贝一份原数组中的数据然后再在拷贝数据的基础上去修改内部数据。

拷贝时以下方式均可

再比如,在一个函数中想要对数组中的内容进行排序,如果在原数组的基础上使用sort是不可以的(改变了原有数据),可以变为以下写法:

纯函数的作用与优势

  • 可以安心编写业务逻辑,不需要去担心在使用了此函数之后其中依赖的外部变量是否已经发生了改变
  • 可以安心使用,输入的内容不会被任意篡改,并且确定的输入一定会有确定的输出

为什么需要Redux

React中,视图层帮助我们解决了渲染DOM的过程,但是State依然需要我们自己来管理:

  • 但是有时状态之间会存在相互依赖(一个状态的变化会引起另一个状态的变化)
  • 当应用程序很复杂的时候,State在什么时候什么地方因为什么原因发生了什么变化以及发生了怎样的变化就会变得非常难以控制、难以追踪
  • 而且当我们有多个组件需要共享和使用相同 state时,可能会变得很复杂,尤其是当这些组件位于应用程序的不同部分时。有时这可以通过 "提升 state" 到父组件来解决,但这并不总是有效。

解决以上问题的方式就是是从组件中提取共享 state,并将其放入组件树之外的一个集中位置。这样,我们的组件树就变成了一个大“view”,任何组件都可以访问 state 或触发 action去修改state(无论它们在树中的哪个位置)。这就是 Redux 背后的基本思想:应用中使用集中式的全局状态来管理,并明确更新状态的模式,以便让代码具有可预测性。

Redux 是一个小型的独立 JS 库,可以集成到任何的 UI 框架中,其中最常见的是 React

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

1、如何搭建Redux项目

  1. 创建一个文件夹如 learn-redux
  2. 使用vscode打开文件夹后初始化package.json

yarn init -y 或 npm init -y

  1. 安装Redux

yarn add redux 或 npm install redux

  1. 创建src目录并创建index.js入口文件
  2. 在package.json中的添加执行命令

此时便可以通过yarn start 或 npm start来进行执行了

2、Redux的基本使用

先来了解以下Redux的核心

包括三部分:Store、Action、Reducer

2-1、使用store中共享的数据

Redux中提供了创建store的方法:createStore

createStore中需要接收一个reducer

  • 而在reducer中接收两个参数:一个是state(当前数据)、一个是action(需要处理的事件描述);返回处理后的state

通过createStore创建好store后,便可以通过store.getState() 来获取store中保存的数据了

1. 准备好一个reducer(因为createStore的时候需要)

src/store/reducer.js

// 我是初始化数据
const initialState = {
  name: "zhangsan",
  age: 23,
};

// reducer 中接收当前的state数据以及action
function reducer(state = initialState, action) {
  // 需要将处理后的state数据返回
  return state;
}

module.exports = reducer;

2. 准备好reducer之后,就可以通过 createStore来创建一个store了

src/store/index.js

const { createStore } = require("redux");
const reducer = require("./reducer");

// 创建store
const store = createStore(reducer);
module.exports = store;

3. 通过store.getState()来获取其中存储的数据

src/index.js

const store = require("./store");

console.log(store.getState());

4. 此时便可以通过yarn start来查看运行结果啦

2-2、修改store中共享的数据

在Redux中,修改store中存储的数据,必须通过store.dispatch(action) 来修改

通过调取store中的dispatch方法来修改数据时:

  • dispatch中传递action对象,用来告知reducer事件的type是什么?携带的数据是什么?
  • 调取dispatch后,redux内部会执行reducer函数来修改state数据(返回一个新的state

1. 在Reducer中编写处理state的逻辑

比如在这个Reducer中可以修改state中保存的name,也可以修改state中保存的age

Reducer中通过判断传入的action的type来执行不同的修改逻辑

需要注意的是,Reducer中处理完修改state逻辑后需要返回一个新的state!切忌在state原有基础上去修改,比如state.name = 'xxx',这样Reducer就不是纯函数了就失去了纯函数的优势,需要通过 {...state, name: 'newName'} 的方式去返回新数据

src/store/reducer.js

const { CHANGE_NAME, CHANGE_AGE } = require("./constants");

// 我是初始化数据
const initialState = {
  name: "zhangsan",
  age: 23,
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case CHANGE_NAME:
      return { ...state, name: action.name };
    case CHANGE_AGE:
      return { ...state, age: action.age };
    default:
      return state;
  }
}

module.exports = reducer;

最好将action.type以常量的方式抽取一下,方便使用,避免写错的情况: 在reducer中处理逻辑的时候最好通过switch分支结构处理,因为一般开发中处理的type会有很多种

2. 通过 store.dispatch触发reducer的修改逻辑,然后获取修改后的state

src/index.js

const store = require("./store");
const { CHANGE_NAME, CHANGE_AGE } = require("./store/constants");

// 修改数据
store.dispatch({ type: CHANGE_NAME, name: "lisi" });
console.log("changename...", store.getState());

store.dispatch({ type: CHANGE_AGE, age: 18 });
console.log("changeage...", store.getState());

2-3、订阅store中的数据

在Redux中可通过store.subscribe来订阅数据的变化

subscribe中接收一个回调函数;

在数据发生改变时该回调函数便会被执行;

可以在回调函数中通过store.getState() 去获取到当前的state数据;

该方法会返回一个可以解绑监听器的函数,通过调取该函数可以解除订阅;

src/index.js

const store = require("./store");
const { CHANGE_NAME, CHANGE_AGE } = require("./store/constants");

const unsubscribe = store.subscribe(() => {
  console.log("变化了...", store.getState());
});

// 修改数据
store.dispatch({ type: CHANGE_NAME, name: "lisi" });
store.dispatch({ type: CHANGE_AGE, age: 18 });
// 取消订阅
unsubscribe();
store.dispatch({ type: CHANGE_AGE, age: 20 });

2-4、优化:抽取自定义生成action的方法

通过以上代码可以发现,我们在dispatch时需要手动去编写action,而且在业务逻辑开发过程中有可能在多处去修改某一字段,这样会造成大量重复性代码,所以可以将不同type的action生成逻辑抽取成一个函数:

store/createActions.js:

const { CHANGE_NAME, CHANGE_AGE } = require("./constants");

const changeNameAction = (name) => ({ type: CHANGE_NAME, name });
const changeAgeAction = (age) => ({ type: CHANGE_AGE, age });

module.exports = {
  changeNameAction,
  changeAgeAction,
};

src/index.js:

const store = require("./store");
// const { CHANGE_NAME, CHANGE_AGE } = require("./store/constants");
const { changeNameAction, changeAgeAction } = require("./store/createActions");

const unsubscribe = store.subscribe(() => {
  console.log("变化了...", store.getState());
});

// 修改数据
store.dispatch(changeNameAction("lisi"));
store.dispatch(changeAgeAction(18));
store.dispatch(changeAgeAction(20));
store.dispatch(changeNameAction("wangwu"));

2-5、总结

在编写Redux代码时最好按照以下方式来编写:

  1. 将reducer代码逻辑与默认值(initialState)放到一个独立的reducer.js文件中,而不是index.js
  2. 将派发的action生成过程放到一个个函数中,并将这些函数抽取到独立的createActions.js文件中管理
  3. 将action中的type值与reducer函数中使用的字符串常量抽取到constants.js中管理

3、Redux的三大原则

3-1、单一的数据源

整个应用的state应被存储在一棵object tree中,并且这个objet tree只存储在一个store中

  • 虽然Redux并没有强制让我们不能创建多个store,但创建多个store并不利于数据的维护

单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改

3-2、State是只读的

只能通过触发action去修改state,不要通过其他任何方式来修改state

  • 这样就确保了view或者网络请求都不能直接修改state,只能通过action来描述自己想要如何修改state
  • 这样保证了所有的修改都会被集中化处理,并且按照严格的顺序来执行

3-3、使用纯函数来执行修改

通过reducer来将旧的state与actions联系在一起,并且返回新的state

  • 随着项目的复杂度增加,可以将reducer拆分成多个小的reducers,分别取操作state不同部分
  • 需要注意的是reducer都应该是纯函数,不能产生任何副作用

4、Redux使用流程

5、如何在React使用Redux

比如现在要实现一个计数器案例,App.jsx中包含Home.jsx、Profile.jsx两个子组件

  • 其中App.jsx、Home.jsx、Profile.jsx中都要去展示共享的counter
  • 在Home.jsx中可以对counter进行加法操作
  • 在Profile.jsx中可以对counter进行减法操作

1. 使用create-react-app创建一个react项目

create-react-app xxx

2. 安装redux

npm install redux

3. 准备好App.jsx以及Home.jsx、Profile.jsx

并在src下创建store文件夹,并在store文件夹下创建index.js(入口,创建store)、constants.js(声明action type常量)、createActions.js(声明创建不同type的action方法)、reducer(声明初始化共享数据、编写reducer处理逻辑)

4. 接下来我们想要在各个组件中共享counter数据,并进行counter数据的加与减,并且可以在各个组件中时刻显示最新的counter数据。

那么action的type可以定义为一下两种:

store/contstants.js

export const ADD_COUNT = "add_count";
export const SUB_COUNT = "sub_count";

5. 准备好创建两种action的方法方便后续使用

store/createActions.js

import * as actionsType from "./constants";

export const addCountAction = (count) => {
  return {
    type: actionsType.ADD_COUNT,
    count,
  };
};

export const subCountAction = (count) => {
  return {
    type: actionsType.SUB_COUNT,
    count,
  };
};

6. 在reducer.js中编写处理逻辑

store/reducer.js

import * as actionsType from "./constants";

const initialState = {
  counter: 100,
};
function reducer(state = initialState, action) {
  switch (action.type) {
    case actionsType.ADD_COUNT:
      return { ...state, counter: state.counter + action.count };
    case actionsType.SUB_COUNT:
      return { ...state, counter: state.counter - action.count };
    default:
      return state;
  }
}

export default reducer;

7. 有了reducer后,就可以通过createStore来创建store了~

store/index.js

import { createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);

export default store;

8. 编写App.jsx中的逻辑

展示子组件;

展示最新counter;

订阅数据变化,当数据一旦变化时就要重新render;

App.jsx

import React, { PureComponent } from "react";
import Home from './pages/Home'
import Profile from './pages/Profile'
import "./style.css"
import store from './store'

export class App extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      counter: store.getState().counter
    }
  }
  componentDidMount() {
    store.subscribe(() => {
      // 获取到最新的数据
      const state = store.getState()
      this.setState({ counter: state.counter})
    })
  }
  render() {
    const { counter } = this.state
    return(
      <div>
        <h2>home-counter: {counter}</h2>
        <div className="home-page">
          <Home />
          <Profile />
        </div>
      </div>
    )
  }
}

export default App;

需要注意的是:

我们要获取的是共享数据中的counter,所以在一开始的state定义时,counter要通过store.getState().counter去获取

  • 因为我们要时刻展示最新的counter,由于在任何一个子组件中都可能去修改共享数据,所以要在componentDidMount中使用store.subscribe去订阅数据的变化,当数据变化时就会执行其中的回调函数,此时我们获取到最新数据后再通过this.setState去设置counter就可以重新执行render进而展示最新的数据了

9. 编写Home.jsx中的逻辑

展示最新的counter;

对counter进行加法操作;

订阅数据变化,当数据一旦变化时就要重新render;

取消订阅,当组件被卸载时取消订阅

pages/Home.jsx

import React, { PureComponent } from 'react'
import store from '../store'
import { addCountAction } from '../store/createActions'

export class Home extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      counter: store.getState().counter
    }
  }
  componentDidMount() {
    store.subscribe(() => {
      // 获取到最新的数据
      const state = store.getState()
      this.setState({ counter: state.counter})
    })
  }
  componentWillUnmount() {
    // 取消订阅
    this.unsubscribe()
  }
  addCount(count) {
    store.dispatch(addCountAction(count))
  }
  render() {
    const { counter } = this.state
    return (
      <div>
        <h2>home-counter: {counter}</h2>
        <div>
          <button onClick={e => this.addCount(5)}>+5</button>
          <button onClick={e => this.addCount(10)}>+10</button>
        </div>
      </div>
    )
  }
}

export default Home

10. 编写Profile.jsx中的逻辑

展示最新的counter;

对counter进行减法操作;

订阅数据变化,当数据一旦变化时就要重新render;

取消订阅,当组件被卸载时取消订阅

import React, { PureComponent } from 'react'
import store from '../store'
import { subCountAction } from '../store/createActions'

export class Home extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      counter: store.getState().counter
    }
  }
  componentDidMount() {
    store.subscribe(() => {
      // 获取到最新的数据
      const state = store.getState()
      this.setState({ counter: state.counter})
    })
  }
  componentWillUnmount() {
    // 取消订阅
    this.unsubscribe()
  }
  subCount(count) {
    store.dispatch(subCountAction(count))
  }
  render() {
    const { counter } = this.state
    return (
      <div>
        <h2>profile-counter: {counter}</h2>
        <div>
          <button onClick={e => this.subCount(5)}>-5</button>
          <button onClick={e => this.subCount(10)}>-10</button>
        </div>
      </div>
    )
  }
}

export default Home

通过以上案例了解了如果在React中使用Redux去编写数据共享逻辑,但是我们也可以发现了这样一个问题:一旦组件中需要使用共享的数据并对数据进行操作时,就要在组件中去编写以下重复的逻辑

  • 从store中去获取数据
  • 订阅store中的变化,重新执行render方法
  • 在组件卸载时去取消订阅

这些逻辑实际上可以抽取到高阶组件中,在高阶组件中对组件进行拦截去统一加入以上的逻辑,这就是接下来要学习的内容啦,后续文章会慢慢讲解~