萌新 redux 入门

878 阅读7分钟

我不萌,但新。
2018年要结束了,这一年,我。。。算了,不多说了。
想起上半年看的 react,到现在也没怎么用过,都快忘光了,趁现在有时间我来捋捋,首先从 redux 下手(与 react 有啥什么关系???😂)

为什么用 redux

我想学习 redux 的同学多少知道它的作用,官方定义 redux 是一个面向 JavaScript 应用的可预测状态容器。如果用过 vuex 的,就能明白它是干嘛的,它们作用是一样的,主要用来管理共享状态。不同于 vuex 与 vue 的关系,redux 与 react 是解耦的,虽然 redux 通常用在 react 中。至于为啥要用 redux,我觉得一张图更好说明问题:

在react应用中,父子组件之间的数据传递比较容易,但是在复杂组件层级关系中,如图左所示的那样,数据的传递就显得很麻烦了,而且容易混乱,这时如果有一个容器帮我们统一管理数据,并能共享到每个组件,那就很方便我们开发了,如图右所示。而redux正是一个提供这样功能的数据框架。

基本概念

概念这东西我不好说,我还是拿一张图说明:

在 react 中使用 redux 时,某组件需要一个数据,那么它就需要触发一个 action,通过 dispatch 给 store,表明自己需要数据啦,action 是行为动作,它告诉 store 需要什么样的数据,它也是 store 数据的唯一来源。store 相当于一个管理员,它自己不直接处理数据,而是把当前的数据和收到的 action 告诉 reducer,让 reducer 来处理数据,并将新数据返还给 store,再由 store 传递给组件,组件拿到的就是目标数据了。

初看时,可能有点绕,我们可以理解为一个在图书馆借书的场景。组件( Component)就相当于借书人,他要告诉管理员(Store),说:"我要借《时间简史》",那么说的这句话就可以理解为 Action,管理员(Store)听到后,他不可能记得每本书的位置,于是就查看记录本看看书的信息,那么这个记录本就相当于 Reducer,找到书的信息后,管理员(Store)就把信息告诉了借书人(Component)。可能比喻并不是那么恰当,但表达的意思也差不多了。redux 就是围绕 Action,Store,Reducer 这三点来展开的。

基本使用

为了方便起见,我直接用 create-react-app 脚手架创建一个项目来演示代码,安装以及创建我就略过了。把项目中多余的文件删除,src 目录下只保留一个 index.js 文件。

之前说过,redux 和 react 是解耦的,那么我们先只在 index.js 文件中来使用一下 redux,使用前别忘记安装以下 redux

npm install redux

index.js

// 引入 createStore 方法,用于创建 store
import { createStore } from "redux"

// 默认初始状态
const defaultState = {
  a: 1
}

// 创建 reducer
function reducer(state = defaultState, action) {
  switch (action.type) {
    case "ADD":
      return Object.assign({}, state, {
        b: action.num
      });
    default:
      return state;
  }
}

// 创建 store
const store = createStore(reducer)

// 获取 state
console.log('dispatch action 之前的数据:',store.getState())

// 派发 action
store.dispatch({
    type:"ADD",
    num:2
})

// 获取更新后的 state
console.log('dispatch action 之后的数据:',store.getState())

运行结果如图:

以上就是一个redux的最基本的使用过程,我们来看下。

reducer

首先说一下 reducer,它用来处理数据,本质上是一个函数,有两个参数 state 和 action,state 即是保存的状态,如果不给他赋初始值,那么它就等于undefined,我在这里给了它一个初始值 defaultState。action 即是行为,它是一个对象,必须有一个名为 type 的字段来表示将要执行的动作,如代码里的ADD就是我传入的一个表示添加的行为,(注意type的值可任意,应尽量语意化)。除了 type 字段外,你可以任意添加自己需要的字段,比如这里我传里一个 num 字段,我需要将它添加到 state 中去。

reducer的注意事项:

  1. 不要直接修改 state。可以看到,我再代码里使用 Object.assign() 新建了一个副本,再返回的。若不创建副本,redux 的所有操作都将指向内存中的同一个 state,所有的 state 都将被最后一次操作的结果所取代,我们将无法追溯 state 变更的历史记录。
  2. 在 default 情况下返回旧的 state。即没有 action 时,一定要返回旧的 state。

action

前面说了 action 本质上是一个JavaScript中的对象,约定 action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action,如下:

// actionTypes.js
export const ADD = "ADD"
import { ADD } from './actionTypes'

const action = {
    type: ADD,
    num: 2
}

通过store.dispatch()方法将 action 传到 store。

store

store将 action 和 reducer 联系起来,维持应用的 state,可以通过store.getState()来获取 state,通过 store.dispatch() 来更新 state。我们通过引入 createStore 方法,并传入 reducer 为第一个参数来创建 store。

在 react 中使用 redux

前面介绍了 redux 的基本使用方法,但是我们最终还是要使用在 react 中,现在我们就结合 react 来用下 redux。

首先,修改项目目录,添加store目录,用来存放 redux 相关文件,而src/index.js文件为组件入口:

我们就写一个烂大街的计数器的例子,通过加减按钮实现数字的增减。

首先是store中的代码:

actionTypes.js 用来统一存放 action 的 type 类型,并导出

// 增
export const ADD = "ADD";
// 减
export const REDUCE = "REDUCE";

reducer.js 创建 reducer 函数,并导出

import { ADD, REDUCE } from "./actionTypes";

const defaultVal = 0;

function reducer(state = defaultVal, action) {
  switch (action.type) {
    case ADD:
      let newVal1 = state + 1;
      return newVal1;
    case REDUCE:
      let newVal2 = state - 1;
      return newVal2;
    default:
      return state;
  }
}

export default reducer;

index.js 文件创建 store,并导出

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

const store = createStore(reducer);

export default store;

然后 src/index.js 文件,是组件入口:

import React, { Component } from "react";
import ReactDOM from "react-dom";
import { ADD, REDUCE } from "./store/actionTypes";
import store from "./store";

class APP extends Component {
  render() {
    return (
      <div>
        <div>{store.getState()}</div>
        <div>
          <button onClick={this.handleReduce.bind(this)}>-</button>
          <button onClick={this.handleAdd.bind(this)}>+</button>
        </div>
      </div>
    );
  }

  // 执行减操作
  handleReduce() {
    store.dispatch({
      type: REDUCE
    });
  }

  // 执行加操作
  handleAdd() {
    store.dispatch({
      type: ADD
    });
  }
}

ReactDOM.render(<APP />, document.getElementById("root"));

此时运行项目,在浏览器中实现加减操作。但是会发现并没有用,怎么点击都没有预想的效果。其实这里少了关键的一步,在 react 中使用不同于第一个例子里那样,第一个例子中我 dispatch 后会重新获取 state,也就是store.getState(),但在 react 组件里使用时,可以看到我只是获取了一次 state,那么我想要的效果是每当我 dispatch 后,这个 state 会自动更新。

redux 中提供了这样的一个功能,就是store.subscribe(listener),它是 store 下的一个方法,会添加一个变化监听器,每当 dispatch action 的时候就会执行。那么有了监听器,每次 dispatch 时我们需要执行啥呢?没错,我们给组件重新 render 下。修改ReactDOM.render这部分代码,如下:

function render() {
  ReactDOM.render(<APP />, document.getElementById("root"));
}
render();

store.subscribe(render);

用一个函数包裹ReactDOM.render方法,并执行,这个函数其实就是 listener 了,把它传给 subscribe,这样每次 dispatch 后,就可以更新 state 了。

以上就是结合 react 和 redux 的一个简单入门小例子,我写的不一定清楚,但是代码敲一遍也能了解个大概。

补充

两天后,我觉得还是要补充介绍下 redux 三大原则:

  1. 整个应用只能有一个 store,即const store = createStore(reducer) 这样的创建 store,只能存在一个,保证单一数据源;
  2. state 是只读的,唯一改变 state 的方法就是触发 action。通过dispatch(action)来表达想要修改的意图,所有的修改会被集中化处理;
  3. reducer 必须是纯函数。前面也说过了,reducer 是接收先前的 state 和 action,并返回新的 state,而不是直接修改 state,这就是符合纯函数的约束。

什么是纯函数?

  • 不能改写参数,我们是通过参数返回新的数值的,同样的输入,得到同样的输出
  • 不能调用系统 I/O 的API
  • 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果

文中有写的不对的地方,望有大佬指点。