如何理解Redux

中文文档
英文文档

Redux = Reducer + Flux

Redux 是 React中的状态管理容器,可以理解为存放 公用数据的地方,提供一种更加规范和安全的方式去定义组件之间的公用数据(想象一下兄弟组件传值的过程)。对于初学者来说,它的使用并不简单。


Redux的理解.png

三大原则

  1. 单一数据源
  2. State 是只读的
  3. 使用纯函数来执行修改

单一数据源

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

State 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
这里的state是值 redux中的数据,不是组件的state

使用纯函数来执行修改

为了描述 action 如何改变 state tree ,你需要编写 reducers。

redux概念

Action

行为

Action 是把数据从应用。传到 store 的有效载荷。我们的数据只能通过 Action来触发,修改。

Reducer

Reducers 负责接收 action,然后根据action去处理store
不能直接修改原来的state。

Store

store其实是一个仓库,redux应用只有一个store,当需要拆分数据时,不能拆分store,但可以拆分reducer

工作流

Redux工作流.png

体验步骤

  1. 安装依赖
  2. 创建store
  3. 创建reducer
  4. 将store数据映射到组件中
  5. 组件触发事件 创建action
  6. 将action派发到store
  7. store自己调用reducer

初体验

实现目标

实现一下以上效果.gif
  1. 可以发请求加载数据
  2. 点击 + - 组件 会修改数据

安装依赖

redux 是核心库 react-redux是负责将react组件连接redux

npm install redux react-redux --save

新建redux配套文件

在src/store/目录下新建 以下文件

  1. index.js store核心文件
  2. reducer/index.js 负责记录操作的reducer文件

reducer.js

// 1 定义默认数据,后期可以从接口中获取
const defaultState = {
  num: -1
};

// 2 创建和对外暴露一个函数 返回state
export default (state = defaultState, action) => {
  return state;
}

store/index.js

// 1 引入 store生成器
import { createStore } from "redux";
// 2 引入reducer
import reducer from "./reducer";
// 3 创建和对外暴露store
export default createStore(reducer);

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// 1 引入 react-redux  负责将store和组件连接起来
import { Provider } from "react-redux";
// 1 引入 store
import store from "./store";

// 2 将App用 Provider 标签包裹起来
// 2 将store通过属性的方式传递到App组件上
ReactDOM.render(<Provider store={store} ><App /></Provider>, document.getElementById('root'));

App.js

import React, { Component } from 'react';
// 1 引入 react-redux 中 链接 组件和store的对象 connect
import { connect } from "react-redux";

class App extends Component {
  render() {
    return (
      // 4 使用store中的数据 
      <div className="App">
        {this.props.num}
        <hr />
        <button> + </button>
        <button> - </button>
      </div>
    );
  }
}

// 2 将 store中的数据传递到 App的props上
const mapStateToProps = (state) => {
  return {
    num: state.num
  }
}


// 3 用 connect 将store中的数据通过props的方式传递到App上
export default connect(mapStateToProps, null)(App)

思考

以上代码,“action去了哪里了呢? ”
答:没有操作数据的行为,当然么有action了

抽离组件,绑定事件

编辑App.js文件
将 两个按钮 抽离出来变成两个组件,这才满足组件共享数据的设计理念

// 加
class AddBtn extends Component {
  render() {
    return <button onClick={this.props.numAdd} >+</button>
  }
}
 
// 减
class SubstraBtn extends Component {
  render() {
    return <button onClick={this.props.numSubStra}>-</button>
  }
}

class App extends Component {
  render() {
    return (
      <div className="App">
        {this.props.num}
        <hr />
        {/* 使用组件 传递props */}
        <AddBtn {...this.props}></AddBtn>
        {/* 使用组件 传递props*/}
        <SubstraBtn {...this.props}></SubstraBtn>
      </div>
    );
  }
}

此时,我们发现 两个组件上都绑定了点击事件

<button onClick={this.props.numAdd} >+</button>
<button onClick={this.props.numSubStra}>-</button>

所以,现在我们需要另外定义 两个事件,在redux中就叫做行为 action
和 mapStateToProps同层级,创建 行为合集 mapDispatchToProps,并且把它传入 connect的第二个参数内。

// 2 将行为action 链接到store和组件上
const mapDispatchToProps = (dispatch) => {
  return {
    // 点击事件中的加
    numAdd: () => {
      // 创建一个action,负责将行为类型和数据交给reducer
      const action = {
        // type是一个自定义的字符串
        type: "NUM_ADD",
        value: 1
      };
      // 派发行为- 会将action 派发到  reducer中
      dispatch(action);
    },
    // 点击事件中的减
    numSubStra: () => {
      const action = {
        type: "NUM_SUBSTRA",
        value: 1
      };
      dispatch(action);
     }
  }
}

// 3 用 connect 将store中的数据通过props的方式传递到App上
export default connect(mapStateToProps, mapDispatchToProps)(App)

编辑 reducer逻辑

// 1 定义默认数据,后期可以从接口中获取
const defaultState = {
  num: -1
};

// 2 创建和对外暴露一个函数 返回state
export default (state = defaultState, action) => {
  // 当 action被派发时(dispatch),会触发
  if (action.type === "NUM_ADD") {
    // 复制一份旧的state
    let newState = Object.assign({}, state);
    newState.num += action.value;
    // 将新的state返回,即可触发store的更新
    return newState;
  }
  if (action.type === "NUM_SUBSTRA") {
    // 复制一份旧的state
    let newState = Object.assign({}, state);
    newState.num -= action.value;
    return newState;
  }

  return state;
}

优化手段

通过以上步骤,可以把redux的使用流程走完,但是在公司中,还会对以上的代码进行优化,存在以下优化的步骤

  1. 将state中的数据修改为对象形式,因为数据一般不会这么简单。
  2. 将action的type类型提取成常量的形式,避免手写字符串出错
  3. 将action的创建由字面量改为 action生成器来创建,方便后期代码的维护和测试
  4. 拆分和合并reducer,有时候,会根据不同的数据使用不同的reducer
  5. 添加异步action,因为有时候我们的数据是从异步中获取的不是同步的方式。

将state中的数据修改为对象形式

编辑 reducer/index.js

const defaultState = {
  // 修改为对象形式
  payload: {
    num: -1
  }
};

export default (state = defaultState, action) => {
  if (action.type === "NUM_ADD") {
    let newState = Object.assign({}, state);
    // 修改为对象形式
    newState.payload.num += action.value;
    return newState;
  }
  if (action.type === "NUM_SUBSTRA") {
    let newState = Object.assign({}, state);
    // 修改为对象形式
    newState.payload.num -= action.value;
    return newState;
  }

  return state;
}

修改App.js中使用的state的代码

const mapStateToProps = (state) => {
  return {
     {/* 修改为对象的形式 */}
    num: state.payload.num
  }
}

将action的type类型提取成常量的形式

新建文件 src/store/actionType/index.js

export const NUM_ADD = "NUM_ADD";
export const NUM_SUBSTRA = "NUM_SUBSTRA";

修改 使用到了 NUM_ADD的文件
编辑 src/store/reducer/index.js

// 1 导入 type常量
import { NUM_ADD, NUM_SUBSTRA } from "../actionType";

export default (state = defaultState, action) => {
  // 2 修改为常量的方式
  if (action.type === NUM_ADD) {
    ......
  }

  return state;
}

编辑 src/App.js

// 1 导入 type 常量
import { NUM_ADD, NUM_SUBSTRA } from "./store/actionType";

const mapDispatchToProps = (dispatch) => {
  return {
    numAdd: () => {
      const action = {
        // 2  使用 type常量
        type: NUM_ADD,
        value: 1
      };
      dispatch(action);
    }
}

使用action生成器来创建action

新建文件 src/store/actionCreator/index.js

import {  NUM_ADD,NUM_SUBSTRA} from "../actionType";
export const numAdd = () => ({
  type: NUM_ADD,
  value: 1
})
export const numSubstra = () => ({
  type: NUM_SUBSTRA,
  value: 1
})

修改 App.js

// 1 导入action
import { numAdd, numSubstra } from "./store/actionCreator";

const mapDispatchToProps = (dispatch) => {
  return {
    numAdd: () => {
      // 2 修改为 生成器生成的action
      dispatch(numAdd());
    }
  }
}

拆分和合并reducer

当需要共享的数据足够多时,一般会拆分多个reducer方便管理
如 拆分成两个 reducer 一个是操作 nums的,一个是操作水果的。

  1. 编辑 actionType/index.js
export const NUM_ADD = "NUM_ADD";
export const NUM_SUBSTRA = "NUM_SUBSTRA";
// 新增 增加 苹果action type
export const APPLE_NUM_ADD = "APPLE_NUM_ADD";
// 新增 减少 苹果action type
export const APPLE_NUM_SUBSTRA = "APPLE_NUM_SUBSTRA";
  1. 编辑 actionCreator/index.js
// 新增 添加苹果 action 
export const appleNumAdd = () => ({
  type: APPLE_NUM_ADD,
  value: 1
})
// 新增 减少苹果 action
export const appleNumSubstra = () => ({
  type: APPLE_NUM_SUBSTRA,
  value: 1
})
  1. 新建文件 reducer/numReducer.js

将 以前 reducer/index.js 全部复制过去即可

import { NUM_ADD, NUM_SUBSTRA } from "../actionType";
const defaultState = {
  payload: {
    num: -1
  }
};
export default (state = defaultState, action) => {
  if (action.type === NUM_ADD) {
    let newState = Object.assign({}, state);
    newState.payload.num += action.value;
    return newState;
  }
  if (action.type === NUM_SUBSTRA) {
    let newState = Object.assign({}, state);
    newState.payload.num -= action.value;
    return newState;
  }
  return state;
}

4.新建文件 reducer/fruitReducer.js

import { APPLE_NUM_ADD, APPLE_NUM_SUBSTRA } from "../actionType";
const defaultState = {
  payload: {
    appleNum: 110
  }
};
export default (state = defaultState, action) => {
  if (action.type === APPLE_NUM_ADD) {
    let newState = Object.assign({}, state);
    newState.payload.appleNum += action.value;
    return newState;
  }
  if (action.type === APPLE_NUM_SUBSTRA) {
    let newState = Object.assign({}, state);
    newState.payload.appleNum -= action.value;
    return newState;
  }
  return state;
}
  1. 编辑 reducer/index.js 用来合并 两个reducer fruitReducernumReducer
// 1 引入 合并reducer的对象
import { combineReducers } from "redux";
import fruitReducer from "./fruitReducer";
import numReducer from "./numReducer";
// 2 对象的形式传入 要合并的reducer
const rootReducer = combineReducers({ numReducer, fruitReducer });
export default rootReducer;
  1. 修改App.js
import React, { Component } from 'react';
import { connect } from "react-redux";
// 1 多导入两个action appleNumAdd 和 appleNumSubstra
import { numAdd, numSubstra, appleNumAdd, appleNumSubstra } from "./store/actionCreator";
class AddNumBtn extends Component {
  render() {
    return <button onClick={this.props.numAdd} >+</button>
  }
}

class SubstraNumBtn extends Component {
  render() {
    return <button onClick={this.props.numSubStra}>-</button>
  }
}
// 2 新增的组件
class AddFruitBtn extends Component {
  render() {
    // 2.1 新绑定的事件
    return <button onClick={this.props.appleNumAdd} >+</button>
  }
}
// 2 新增的组件
class SubstraFruitBtn extends Component {
  render() {
    // 2.1 新绑定的事件
    return <button onClick={this.props.appleNumSubStra}>-</button>
  }
}

class App extends Component {
  render() {
    // 3 修改过的页面代码
    return (
      <div className="App">
      数量  {this.props.num}
        <hr />
        <AddNumBtn {...this.props}></AddNumBtn>
        <SubstraNumBtn {...this.props}></SubstraNumBtn>
        <hr />
      水果数量  {this.props.appleNum}
        <br />
        {/* 3.1 引入新组件 */}
        <AddFruitBtn {...this.props}></AddFruitBtn>
        {/* 3.1 引入新组件 */}
        <SubstraFruitBtn {...this.props}></SubstraFruitBtn>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  // 4 修改数据的获取方法
  return {
    num: state.numReducer.payload.num,
    appleNum: state.fruitReducer.payload.appleNum
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    numAdd: () => {
      dispatch(numAdd());
    },
    numSubStra: () => {
      dispatch(numSubstra());
    },
    // 5 新增的action
    appleNumAdd: () => {
      dispatch(appleNumAdd());
    },
    // 5 新增的action
    appleNumSubStra: () => {
      dispatch(appleNumSubstra());
    },
  }
}

// 3 用 connect 将store中的数据通过props的方式传递到App上
export default connect(mapStateToProps, mapDispatchToProps)(App)
图示.png

添加异步action redux-thunk

想象一下,我们对数据库进行查询,编辑和删除,其实都是异步操作。现在,让我们的应用支持异步action操作。

  1. 安装依赖 redux-thunk
npm install redux-thunk --save
  1. 修改store/index.js
// 1 引入 redux的中间件连接器
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
// 1 引入 redux-thunk
import reduxThunk from "redux-thunk";
// 2 使用中间件连接器将redux-thunk传入 store构造器
export default createStore(reducer, applyMiddleware(reduxThunk));
  1. 修改 actionCreator/index.js
// 1 修改 减少苹果的action 为异步的形式
export const appleNumSubstra = () => {
   // 2 返回一个函数
  return (dispatch) => {
    // 3 开启异步  后期将 setTimeout 替换成异步的方式即可
    setTimeout(() => {
      const action = {
        type: APPLE_NUM_SUBSTRA,
        value: 1
      };
        // 4 开启派发
      dispatch(action);
    }, 2000);
  }
}

React的生命周期
React路由
闭包,原型链,继承,AJAX请求步骤等javaScript基础

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355