Redux 简明教程

305 阅读25分钟

本文参考 Redux 入门教程(一):基本用法Redux 入门教程(二):中间件与异步操作Redux 入门教程(三):React-Redux 的用法前端技术栈(三):redux-saga,化异步为同步Redux-sagaredux-sagareact-redux@7.1的api,用于hooks

demo

  • create-react-app 新建项目

  • 安装 redux 依赖

    • npm i -S redux

/// 目录结构如下
--- src
	---| pages
		---| main.jsx
		
	---| store
  	---| reduers
  		---| index.js
  	---| index.js	
  	
	---| index.js
...

/// main.jsx 替换 index.js 中 App 组件
import React, { Component } from 'react';

import store from '../store';

class Main extends Component {
    state = store.getState();
    render() {
        return (
            <div style={{padding: '10px 20px'}}>
                <ul>
                    {this.state.listData && this.state.listData.map((item, index) => (
                        <li key={index}>{item}</li>
                    ))}
                </ul>
            </div>
        );
    }
}

export default Main;
...

/// store/index.js
import {createStore} from 'redux';

import reducers from './reduers/index';

const store = createStore(reducers);

export default store;
...

/// store/reduers/index.js
const defaultState = {
    listData: [
        'list1',
        'list2',
        'list3',
        'list4'
    ]
}

export default (state = defaultState, action) => {
    return state;
}

简述

Redux 概念比 Vuex 弄的复杂,但其思路是一致的,都源于对 Flux 概念的实现。下面我按自己的理解以图书馆的借书流程,来简述下整个 Redux 的概念

在下面的例子中,我们假定自己管理的最小数据单元是图书(state),借书者(Component,调用这个 store 的组件)通过图书管理系统查询图书,再借阅(从 store 取出 state,或者更新 state)

  • createStore 创建一个图书馆
    • createStore 函数接受另一个函数作为参数,返回新生成的 Store 对象
    • 另一个函数 可以是一个 reducers
  • reducers 创建一个图书馆的分馆,比如科技图书馆,图书馆里存了各种图书
    • 这里你可以理解为 reducers 就是管理 state 的最小组合
    • reducers 本质是一个纯函数,该函数期望接收两个参数, (previousState, action) => newState
  • store.getState() 是通过图书系统查询当前图书馆全部图书的信息
    • 方面调用时,返回当前 store 的数据快照
    • 把拿到的数据信息,绑定到调用者的 state 上,这样组件就可以获取到想要的数据(store 和 component 就形成了数据关联)
  • dispatch(action) 要取走图书,就要去借书处登记下
    • 如果你想改变数据,必须调用指定方法 dispatch 去触发一个 action,这个方法调用最终会触发 reducers 方法的执行(此处暂不考虑多个 reuders 的情况,下面会补充),得到新的 State
    • action 只是一个通用的约束,一般形式如 {type:xx, payload:xxx}
/// dispatch(action) 的例子
/// main.jsx render 函数中新增下面代码
<button onClick={() => store.dispatch({ type: 'INCREMENT', payload: {value: 'list5'}})}>增加</button>
<button onClick={() => store.dispatch({ type: 'DECREMENT'})}>减少</button>
...
/// reduers/index.js 下新增下面代码
export default (state = defaultState, action) => {
    let newState = JSON.parse(JSON.stringify(state));
    switch (action.type) {
        case 'INCREMENT':
            newState.listData.push(action.payload.value);
            return newState;
        case 'DECREMENT':
            newState.listData.pop();
            return newState;
        default:
            return newState;
    }
}

可以看到上面例子中 action 中的信息最后都在 reducers 中使用,而且 type 、payload 也都只是一个约束,完全可以不这么些,但基本这就是社区规范,语意也比较清晰,没必要在这个地方进行什么创新。

  • store.subscribe 通知借书者
    • 这是一个监听函数,一旦 State 发生变化,就自动执行这个函数
/// main.jsx
constructor (props) {
  super(props);
  store.subscribe(() => {
  	this.setState(store.getState()); // State 一更新就调用 setState 进而更新视图
  });
}

整个过程中要理解 store 和 component 之间隔离,作为 component 完全不会去管 store 内部怎么管理 state。

component 通过 store.getState() 获取 state 数据,通过 dispatch 触发 action。

store 中的 reducers 接收 action ,根据要求对 state 进行更新。如果state更新了,就会触发 store.subscribe,component 再重写这个方法调用 setState 更新 state,最终就会触发 component 的视图更新。

Reduers 拆分

还是以图书馆为例子,一个图书馆不可能只有一个科技图书管,必然还要有人文、社科之类的分馆,要不所有的书都丢在科技管,管理起来也不方便。(reduers 拆分也是一个道理)

假定借书者在借书前会明确的知道是要借什么书,书在什么馆,自己要怎么过去。reducers 拆分后,调用的组件也需要像借书者那样,明确的指明自己想要获取哪个 reducers 中的数据。

  • combineReducers 进行 reduers 合并的辅助函数
/// 在reduers文件夹下新建 main.js
const defaultState = {
    name: 'main'
}

export default (state = defaultState, action) => {
    return state
}
...

/// store/index.js
import {createStore, combineReducers} from 'redux';

import reducers from './reduers/index';
import mainReduer from './reduers/main';

const allReduers = combineReducers({
    reducers,
    mainReduer
}); // 对 reduers 进行合并

const store = createStore(allReduers);

export default store;
...

/// main.jsx
/// 不需要修改别的地方,只需在调用处指定使用哪里的 reduers
/// listData 是在 reduers/index 中
/// name 是在 reduers/main 中
<ul>
  {this.state.reducers.listData && this.state.reducers.listData.map((item, index) => (
  <li key={index}>{item}</li>
  ))}
</ul>
{this.state.mainReduer.name && <p>{ this.state.mainReduer.name }</p>}

留意 reduers 的拆分和组合其实只会影响到对reduers中存储数据的使用,并不影响dispatch的调用,意思就是在发送diaptch时不用指明是往哪个 reduers 发送

/// 不用指明往哪个reduers发送
store.dispatch({ type: 'INCREMENT', payload: {value: 'list5'}})

因为只要触发 dispatch,所有 reduers 对应的函数都会执行。这就引出了另外一个通用社区规范,使用常量管理 action 中的 type,需要保证某一次 action 只会触发唯一更新

例如 type: 'INCREMENT' 现在是只在 reduers/index 中有定义,不能在其他 reuders 中再定义

/// store/types.js 新建一个统一文件管理
// index
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
...

/// store/reduers/index.js 
/// 在相关 reduers 替换
import {INCREMENT, DECREMENT} from '../types';
...
 switch (action.type) {
        case INCREMENT:
            ...
        case DECREMENT:
            ...
...

/// main.jsx
import {INCREMENT, DECREMENT} from '../store/types'; // 调用者也从相同文件引入 types
...
/// 具体调用处直接替换
<button onClick={() => store.dispatch({ type: INCREMENT, payload: {value: 'list5'}})}>增加</button>
<button onClick={() => store.dispatch({ type: DECREMENT })}>减少</button>

actionCreator

这和 action 一样,都只是一个社区规范,上面例子有一些代码是重复的,有一些逻辑也是没必要暴露出来的

import {INCREMENT, DECREMENT} from '../store/types';
...
store.dispatch({ type: DECREMENT })
...
store.dispatch({ type: INCREMENT, payload: {value: 'list5'}})

这些 type 都是 store 的逻辑,对于 main.jsx 其实是没必要知道这些实现细节(而且这些常量阅读起来也不友好)。就像开车的人知道,前进中汽车方向盘左打,车轮向左,这个过程中没必要知道具体的车轴之间是怎么传动的。所以,才会有了下面这种写法:

/// store/actionCreator.js 新建一个统一管理生成 action
import { CHANGE_INFO, CHANGE_FOO } from "./types";

export const changeInfoAction = payload => ({
  type: CHANGE_INFO,
  payload
});

export const changeFooAction = payload => ({
  type: CHANGE_FOO,
  payload
});
...

/// main.jsx
import { changeInfoAction, changeFooAction } from "../reducers/actionCreator"; // 引入新定义的actionCreator
...
handleChangeInfo = () => {
	// 调用相关的 action 生成函数
  const action = changeInfoAction({ info: "new info" });
  store.dispatch(action); // dispatch 触发
};
handleChangeFoo = () => {
  const action = changeFooAction({ foo: "new foo" });
  store.dispatch(action);
};

改造后在 main.jsx 中,没有 type 相关的内容。使用者只需要知道 store 暴露出,可改变的 action 生成函数即可,对于其内部定义的 type 叫什么完全不用关心。

中间件

中间件(英语:Middleware),又译中间件、中介层,是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件,应用软件可以借助中间件在不同的技术架构之间共享信息与资源。中间件位于客户机服务器的操作系统之上,管理着计算资源和网络通信。(来源维基百科)

Redux 中的中间件简单理解,就是一个独立运行于各个框架之间的代码,本质就是一个函数,可访问请求对象和响应对象,可对请求进行拦截处理,处理后在讲控制权向下传递,也可终止请求,向客户端做出响应。

放到现实中,可以用快递的概念来解释。比如你有个快递是从深圳寄去上海,你希望快递站给送到收件人手上(这是你发出的 action),这个过程中原本的动作是通过飞机从深圳到上海,再从上海用快递车送到收件人手上(没有使用 Middleware ,直接 dispatch action )。

但是由于现在有台风要经过深圳,飞机不能直接从深圳起飞,要看天气情况,等台风过境天气好了才能出发,那么这中间的看天气如何,等台风过境天气好了再出来的过程,就可以认为是一种中间件行为。也就是其本质上不改变 action 的运行流程(送深圳往上海送快递这个流程不变),只是在某些情况下对 action 进行一定的行为补充。(实际开发中这个行为可能是输出一段 logger, 或者处理异步请求拿到数据后继承后续流程)

就是把之前 action -> reducer 的流程,变成了 action -> Middleware -> reducer

下面是一个概念表述,但实际上中间件代码不是这么运行的例子

/// 在触发 dispatch 之前添加功能
/// 这个例子只是表述中间件干的就是这么个活,只不过 Redux 对其概念进行封装,并提供了统一管理的方式
handleChangeInfo = () => {
    console.log("这里开始日志记录");
    console.log(
      `此时的action为:{type: ${CHANGE_INFO}, payload: {info: 'new info'}}`
    );
    console.log(`此时的store为: ${JSON.stringify(store.getState())}`);
    store.dispatch({
      type: CHANGE_INFO,
      payload: {
        info: "new info"
      }
    });
  };

中间件 其实就是函数,本质是对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能

普通的异步操作

在不使用中间件时,如果要进行异步操作,可以按如下思路进行

/// main.jsx 简化了其他代码
componentDidMount() {
		// 发送异步请求,在成功回调中触发相应的 action 更新相关数据
    axios
      .get(
        "https://restapi.amap.com/v3/weather/weatherInfo?key=f4362c0ec04597b508a4b4e8cc8c4e70&city=深圳"
      )
      .then(res => {
        const action = getDataAction({ info: res.data.info });
        store.dispatch(action);
      });
  }

这种方式一方面代码的可读性不好,如果当前页面需要请求3 4个接口,那这种全部丢在生命周期写业务逻辑的方式,会造成生命周期的函数中有一长串的代码,而且这种方式也不利于自动化测试。

applyMiddlewares

Redux 的原生方法,作用是将所有中间件组成一个数组,依次执行

/// store/index.js 修改如下
import { createStore, combineReducers, applyMiddleware } from "redux"; // 引入 applyMiddleware 方法
import { createLogger } from "redux-logger"; // 引入上面例子中说的,一个实现logger 的库

import appReducer from "./app";
import fooReducer from "./foo";

const logger = createLogger();
const reduers = combineReducers({
  appReducer,
  fooReducer
});

// applyMiddleware(logger) 这就是使用中间件的方法,如果有多个中间件就会依次执行
const store = createStore(reduers, applyMiddleware(logger));

export default store;

redux-thunk

这是一个中间件

store.dispatch 默认下,只能接收对象形式的传承。使用这个中间件后,就可以接收函数形式的参数

/// index.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk"; // 引入 redux-thunk

import appReducer from "./app";
import fooReducer from "./foo";

const reduers = combineReducers({
  appReducer,
  fooReducer
});

const store = createStore(reduers, applyMiddleware(thunk)); // 使用这个中间件

export default store;
...

/// actionCreator.js 省略部分代码
export const getDataAction = payload => ({
  type: GET_DATA,
  payload
});

// 新增一个返回函数的action,这个action能使用,是因为引入 redux-thunk 
export const getSiteDataAction = () => {
  return dispatch => { // 这个返回的函数,接收 store 的两个方法 dispatch 和 getState
    axios
      .get(
        "https://restapi.amap.com/v3/weather/weatherInfo?key=f4362c0ec04597b508a4b4e8cc8c4e70&city=深圳"
      )
      .then(res => {
        const action = getDataAction({ info: res.data.info }); // 依然生成一个 action,这是一个标准的 action
        dispatch(action); // 触发这个 action
      });
  };
};
...

/// main.jsx
  componentDidMount() {
  	// 直接触发新增的 action
    store.dispatch(getSiteDataAction());
  }

从整个流程上来看,redux-thunk 这个中间件的引入,使 redux 处理 action 时可以接收函数形式。这样就可以设置一个异步函数作为 action,在这个异步函数的回调中,对返回的数据进行处理。然后继续去触发一个新的 action,把处理后的数据 更新到 store 中。

这样一个完整的异步获取数据,再同步到 store 中的操作就完成了。与前面的直接在生命周期中写业务逻辑的方式不同,这种方式本质思路上还是保持涉及到对 store 操作,统一在一处管理。

这样既可以保证页面的代码简洁性,也比较符合代码封装的思路(对外只暴露用处,不暴露实现),同时也方便自动化测试

redux-saga

redux-saga 是一个 Redux 中间件,用来帮你管理程序的副作用。或者更直接一点,主要是用来处理异步 action。(摘录《前端技术栈(三):redux-saga,化异步为同步》)

Saga 像个独立线程一样,专门负责处理副作用,多个 Saga 可以串行/并行组合起来,redux-saga 负责调度管理(摘录《redux-saga》)

redux-saga 与 redux-thunk 类似也是为了解决异步请求,区别在于其封装性更好,对 action 的逻辑没有有产生破坏,在 action 之外多了一层专门处理异步请求。而且解决 code in everywhere 的问题,也便于进行测试

下面用 saga 改写上面的异步请求

/// reducers/index.js
// redux-saga 比 redux-thunk 在初始化时,多了几步。最明显的就是有一个独立的操作文件,saga 在其官方在介绍自己时,称自己就像是应用程序中一个单独的线程(原因就在这里),run 方法就是触发这个操作线程
import { createStore, combineReducers, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga"; // 1、saga 引入createSagaMiddleware

import appReducer from "./app";
import fooReducer from "./foo";

import sagas from "./sagas"; // 2、自己写的根 rootSaga

const sagaMiddleware = createSagaMiddleware(); // 3、创建saga中间件

const reduers = combineReducers({
  appReducer,
  fooReducer
});

const store = createStore(reduers, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(sagas); // 4、启动 saga

export default store;
...

/// actionCreator.js
// 新建一个触发发送请求的 action
export const sendReqInfo = () => ({
  type: SEND_REQ_INFO
});
...

/// main.jsx
componentDidMount() {
  // 执行这个 action
  store.dispatch(sendReqInfo());
}
...

/// sagas.js
import { takeEvery, put } from "redux-saga/effects";
import axios from "axios";

import { SEND_REQ_INFO } from "./types";
import { getDataAction } from "./actionCreator"; // 所有的 action 其实还是在一个地方管理,这也体现出 saga 对代码的巧妙拆分

// work saga
function* getInfoData() {
  const res = yield axios.get(
    "https://restapi.amap.com/v3/weather/weatherInfo?key=f4362c0ec04597b508a4b4e8cc8c4e70&city=深圳"
  );
  const action = getDataAction({ info: res.data.info });
  yield put(action); // 触发 getDataAction
}

// 对外暴露的 Generator 函数
// watcher saga
function* sagas() {
	// 意思就是当 SEND_REQ_INFO 的 action 触发时(sendReqInfo),执行后面的方法
  yield takeEvery(SEND_REQ_INFO, getInfoData);
}

export default sagas;

从上面例子来看,saga 的整个运行链路其实非常明晰

  • 使用者发送一个要获取数据的 action
  • sagas 监听 store 发送的 action,如果是自己要处理的,就直接开始处理
  • 请求获得数据后,再去触发相应的 action,让相应的 action 去更新 store。

sagas 的 3 种类型

  • root saga: 将所有 watcher saga 组合成一个
  • watcher saga: 监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务
  • worker saga: 执行具体的逻辑处理,如进行异步请求,处理返回结果等,根据结果派发 action

阻塞调用 和 非组塞调用

  • 阻塞调用
    • 阻塞调用的意思是: saga 会在 yield 了 effect 后会等待其执行结果返回,结果返回后才恢复执行 generator 中的下一个指令
  • 非阻塞调用
    • 非阻塞调用的意思是: saga 会在 yield effect 之后立即恢复执行
常用 Effect creator
  • call 异步阻塞调用
  • fork 创建一个新的进程或者线程,并发发送请求
  • put 相当于 dispatch,分发一个 action
  • select 相当于getState,用于获取 store 中相应部分的state
  • cancel 指示 middleware 取消之前的 fork 任务,cancel 是一个无阻塞 Effect。也就是说,Generator 将在取消异常被抛出后立即恢复
  • race 竞速执行多个任务
  • throttle 节流

API

Middleware API
  • createSagaMiddleware(options)

    • 创建一个 redux 中间件,并将 sagas 与 Redux store 建立链接,可通过 createStore 第三个参数传入
      • options: 传递给 middleware 的选项列表,默认可以不用传递
  • middleware.run(saga, ...args)

    • 动态地运行 saga,只能用于在 applyMiddleware 之后,这个方法返回一个 Task 描述对象
      • saga: Function: 一个 Generator 函数
      • args: Array: 提供给 saga 的参数 (除了 Store 的 getState 方法)
Saga 辅助函数

Saga Helper用来监听action,API 形式是 takeXXX,其语义相当于 addActionListener

  • takeEvery(pattern, saga, ...args)
    • 允许并发action(上一个没完成也立即开始下一个),触发多少次异步的action,就会执行多少次异步的任务

takeEvery 内部实现就是不停地 take -> fork -> take -> fork …循环。当接收到指定 action 时,会启动一个 worker saga,并驱动其中的 yield 调用

  • takeLatest(pattern, saga, ...args)
    • 不允许并发action(只做最新的),每次触发,会取消掉上一次正在执行的异步任务

takeEvery, takeLatest 是在 take 之上的封装,take才是底层API,灵活性最大,能手动满足各种场景

takeEvery、takeLatest 这个所谓的执行多少次,还和网速,异步响应时间有关系。我们做一个假设,在2秒内,分别点击两个按钮7次,一个按钮绑定辅助函数由 takeEvery 监听,另一个按钮绑定的辅助函数由 takeLatest 监听,两个按钮对应的异步请求是同一个请求,这个异步请求1秒后会响应。

/// 注意在 reduers 和相关 saga 加上一些 console,方便比较
yield takeEvery(按钮1, getInfoData1);
yield takeLatest(按钮2, getInfoData2);
...
function* getInfoData1() {
  console.log("getInfoData1");
  const res = yield call(axios.get, xxx);
  const action = getDataAction({ info: res.data.info });
  yield put(action);
}
function* getInfoData2() {
  console.log("getInfoData2");
  const res = yield call(axios.get, xxx);
  const action = getDataAction({ info: res.data.info });
  yield put(action);
}

假定在弱网的情况下,按钮1 和 按钮2,分别点击了 7 次。在浏览器控制台能看到,相关接口依次的调用了 7 次。

但是 takeEvery 对应的异步任务,都会执行到 put(action),进入到 reducer 流程中。而 takeLatest 只会执行一次 put(action),对于进行的重复点击,虽然不会取消接口的请求操作,但是不会继续后续的流程,只执行最新的一次操作(这个成立的条件,是建立在现在这个按钮大概每次0.3秒左右点击一次,接口1秒后才有反应的情况下才成立。最终出会了点击7次,只有最后一次走完完整流程。要是这个接口 0.1 秒就有返回,那么 takeLatest 的表现和 takeEvery 就看起来就都一样)

  • throttle(ms, pattern, saga, ...args)

    • 匹配到一个对应的 action 后,会执行一个异步任务,但是同时还会接受一次对应的 action 的异步任务,放在底层的 buffer 中,然后就会无视给定的时长内新传入的 action。
    • 这个辅助函数在指定时长内,只会发两次异步请求,takeEvery、takeLatest 则是有几次,发送几次
  • takeLeading(pattern, saga, ...args)

    • 匹配到 action 后,会执行一个异步任务。 派生一次任务之后开始阻塞(不会多次重复发送请求,takeLatest 会),直到派生的 任务 完成,然后又再次开始监听指定的 action
    • takeLeading 在其开始之后便无视所有新传入的任务,可以保证如果用户以极快的速度连续多次触发 action,都只会保持以第一个 action 运行
Effect 创建器

Effect 指的是描述对象,相当于 redux-saga 中间件可识别的操作指令,可以看作是 redux-saga 的任务单元

  • take(pattern)
    • 阻塞方法,用来匹配发出的 action,在匹配之前 Generator 将暂停
      • pattern为空 或者 * ,将会匹配所有发起的 action
      • pattern为一个函数,action 会在 pattern(action) 返回为 true 时被匹配

      例如,take(action => action.entities) 会匹配那些 entities 字段为真的 action

      • pattern是一个字符串,action 会在 action.type === pattern 时被匹配

      例如:take(GET_SEND_INFO)

      • pattern是一个数组,会针对数组所有项,匹配与 action.type 相等的 action

      例如,take([INCREMENT, DECREMENT]) 会匹配 INCREMENT 或 DECREMENT 类型的 action

// 比如有这么一个 rootSaga,默认通过 moddleWare.run(rootSaga) 时,会阻塞在第一个判断处,要等待触发 type1 的 action 后,后面的监听才会执行
export default function* rootSaga () {
	yield take(type1);
  yield takeEvery(type2, type2Saga);
  yield takeLeading(type3, type3Saga);
  yield throttle(2000, type4, type4Saga);
} 
  • call(fn, ...args)

    • 阻塞方法,call()执行完,才会往下执行。创建一条 Effect 描述信息,指示 middleware 调用 fn 函数并以 args 为参数,和js普通的call方法类似
    • fn: Function - 一个 Generator 函数, 或者返回 Promise 的普通函数
    • args: Array - 一个数组,作为 fn 的参数
  • put(action)

    • 非阻塞方法,发送对应的 dispatch,触发对应的 action
    • put 是异步的,不会立即发生
function* getInfoDataSH() {
  console.log("getInfoDataSH");
  const res = yield call(
    axios.get,
    "https://restapi.amap.com/v3/weather/weatherInfo",
    { key: "f4362c0ec04597b508a4b4e8cc8c4e70", city: "上海" }
  ); // 第一个参数发ajax请求的方法,第二,第三就是这个函数的相关参数
  const action = getDataAction({ info: res.data.info }); // actionCreator
  yield put(action); // 当作 dispatch 使用,发送 action
}
  • fork(fn, ...args)
    • 非阻塞方法,执行 fn 时,不会暂停 Generator,方法执行后会返回的是一个 Task 对象。创建一个新的进程或者线程,并发发送请求
      • fn: Function - 一个 Generator 函数, 或者返回 Promise 的普通函数
      • args: Array - 一个数组,作为 fn 的参数
    • fork 类似于 call,可以用来调用普通函数和 Generator 函数。但 fork 的调用是无阻塞的,在等待 fn 返回结果时,middleware 不会暂停 Generator。 相反,一旦 fn 被调用,Generator 立即恢复执行
    • fork 与 race 类似,是一个中心化的 Effect,管理 Sagas 间的并发
    • fork: 是分叉,岔路的意思 ( 并发 )
// 下面的代码就会在触发 type1 的 action 后,运行 saga1,并且不会阻断其后的 console
  yield take(type1);
  yield fork(saga1);
  console.log('saga1')
  yield takeLatest(type2, saga2);
...

function* rootSaga () {
	while(true) {
		yield take(type1);
		yield fork(saga1);
	} // 这种写法和 takeEvery(type1, saga1); 用法一致
}
...

function* rootSaga () {
	while(true) {
		yield take(type1);
		yield fork(saga1);
		yield fork(saga2);
		yield fork(saga3);
	} // 因为 fork 是并发,这样就可以触发一个 action 后,运行多个任务
}
  • join(task)
    • 用来获取非阻塞的task的返回结果
    • task: Task - 之前的 fork 指令返回的 Task 对象
function* sagas() {
    while(true) {
        yield take(GET_SYSTEM_TIME);
        const timeTask = yield fork(getSystemTime);
        console.log(timeTask); // 这里是返回一个 Task 对象
        const result = yield join(timeTask); // 通过 join 就能拿到 fork 任务的返回结果,也就是 getSystemTime 的返回信息
        console.log('result', result);
        yield fork(getInfo);
    }
    yield takeLatest(GET_SUID, getSupid);
}
...
function* getSystemTime() {
    const res = yield call(axios.get, xxx);
    const action = setSystemTime();
    yield put(action);
    return res; // 需要有返回信息否则join的信息为空
}
  • cancel(task)
    • 取消之前的 fork 任务
      • task: Task - 之前的 fork 指令返回的 Task 对象
    • cancel 是一个无阻塞 Effect。也就是说,Generator 将在取消异常被抛出后立即恢复
function* sagas() {
    while(true) {
        yield take(GET_SYSTEM_TIME);
        const timeTask = yield fork(getSystemTime);
        console.log(timeTask);
        yield cancel(timeTask); // 这里直接取消 timeTask,要留意此处的取消只是取消后续的代码处理,但是已经发起的异步请求还是继续执行(getSystemTime)
        const result = yield join(timeTask); // 由于 join 要等待 timeTask 的返回,这么用代码就卡在此处不再执行(后面的fork也不执行了),如果删了此处的join ,后续的 fork 才会执行
        console.log('result', result);
        yield fork(getInfo);
    }
    yield takeLatest(GET_SUID, getSupid);
}
  • select(selector, ...args)
    • 得到 Store 中的 state 中的数据
      • selector: Function - 一个 (state, ...args) => args 函数. 通过当前 state 和一些可选参数,返回当前 Store state 上的部分数据
      • args: Array - 可选参数,传递给选择器(附加在 getState 后)
    • 如果 select 调用时参数为空( yield select() ),那 effect 会取得整个的 state
    • 这个 select 获取的 state,一定不能是 当前 saga 对应任务需要修改的数据
/// 下面简化代码,只保留关键代码,说明相关问题
/// reducers/index.js 这是一个 reducer 的默认 state
const defaultState = {
    time: ''
}
...
/// sagas.js
function* getSystemTime() {
    const res = yield call(axios.get, xxx);
    const action = setSystemTime({time: res.data.systemTime});
    yield put(action); // 这个 action 会触发对 store 中 time 的更新
}
function* sagas() {
    while(true) {
        const oldState = yield select((state) => {
            return state.reducers; // 这是在混合的 reducers 中的 state
        });
        console.log(oldState); // 此时查看 state 对应的 time 值为 '',也就是改变之前的值,所以通过 select() 获取 state 时,注意获取的是改变前的值,还是改变后的值 
        yield take(GET_SYSTEM_TIME);
        yield fork(getSystemTime);
    }
}

select() 方法的存在并不是为了解决通过action传递异步参数的问题,如果要获取action中的参数传递给异步请求,可以这么操作

/// 假定操作的是标准的action形式 {type:xxx, payload: {...}}
/// 第一种情况使用 take fork 的形式
function* rootSaga () {
		...
    let payload = {};
    yield take(action => {
        console.log('action', action);
        if (action.type === GET_SYSTEM_TIME) {
            payload = action.payload; // 取出当前 action 的 payload
            return true;
        }
    });
    yield fork(getSystemTime, payload); // 把取到的参数,传入 fork 中
    yield fork(getInfo);
    yield takeLatest(GET_SUID, getSupid);
    ...
}
...
function* getSystemTime(params) {
    console.log('params', params); // 此时就可以取出相关参数,拿到这些参数就可以传到异步请求中
    ...
}    
...

/// 使用 takeEvery 的形式
function* rootSaga () {
	...
	yield takeEvery(GET_SYSTEM_TIME, getSystemTime);
	...
}
...
function* getSystemTime(action) {
    console.log('action', action); // 此时返回的是 action,可以通过 action.payload 获取相关参数
    ...
}

Effect 组合器(combinators)

  • race(effects)
    • 多个 Effect 并行运行,等最快运行结束,就会自动地取消所有输掉的 Effect,不再等待其返回(与 Promise.race([...]) 的行为类似)
function* sagas() {
  console.log("run start");
  while (true) {
    yield take(SEND_REQ_INFO_SH);
    const info = yield race([call(sagaEffect1), call(sagaEffect2)]);
    console.log(info); // 假定 sagaEffect1、sagaEffect2都会返回数据,这个info会返回一个数组,里面会是对应sagaEffect 的返回数据,只不过只会有一个值
  }
}
...

/// 除了上面的写法,还可以写成一个对象形式
const { sagaData1, sagaData2 } = yield race({
  sagaData1: call(sagaEffect1),
  sagaData2: call(sagaEffect2)
});
    • all(effects)
      • 多个 Effect 并行运行,等全部返回后继续后续逻辑(与 Promise.all 行为类似)
      • 用法与 race 一样
// 对象写法
const { shData, bbData } = yield all({
  shData: call(getInfoDataSH),
  bbData: call(getInfoDataBB)
});
...
// 数组写法
const info = yield all([call(sagaEffect1), call(sagaEffect2)]);

其他还有些外部 api ,由于从开始设定的使用场景就是在 react 环境下,所以这部分 api 就不再展开

redux-devtools 与 compose

这是一个配置问题,当启用中间件时,如果还要继续使用 redux-devtools 可以这么设置

/// 简化了一些代码
import { createStore, applyMiddleware, compose } from "redux";
...
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // 设定使用哪个增强函数
...
const store = createStore(
  reduers,
  composeEnhancers(applyMiddleware(sagaMiddleware)) // 使用增强函数包裹中间件函数
);

react-redux

React-Redux 是 Redux 官方 React 绑定库,能够使你的 React 组件从 Redux store 中读取数据,并且向 store 分发 actions 以更新数据。 如果要使用它,要遵守它的组件拆分规范。

React-Redux 将所有组件分成两大类:

  • UI 组件(presentational component)
    • UI 组件负责 UI 的呈现
    • UI 组件都由用户提供
  • 容器组件(container component)
    • 容器组件负责管理数据和逻辑
    • 容器组件则是由 React-Redux 自动生成

connect、Provider、store

  • connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
    • 用于从 UI 组件生成容器组件,将 UI组件 和 容器组件 连起来
    • mapStateToProps?: (state, ownProps?) => Object 函数目的是建立一个从(外部的)state 对象到(UI 组件的)props对象的映射关系,mapStateToProps 会订阅 Store,每当 state 更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
      • state: Object 传入的 state
      • ownProps?: Object 容器组件的 props 对象
    • mapDispatchToProps?: Object | (dispatch, ownProps?) => Object 用来建立 UI 组件的参数到store.dispatch方法的映射。定义了哪些用户的操作行为应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象
      • dispatch: Function
      • ownProps?: Object
    • mergeProps?: (stateProps, dispatchProps, ownProps) => Object 如果指定了这个参数,mapStateToProps()mapDispatchToProps() 的执行结果和组件自身的 props 将传入到这个回调函数中
      • 如果不指定这个参数,默认的返回 { ...stateProps, ...dispatchProps, ...ownProps };
    • options?: Object 如果指定这个参数,可以定制 connector 的行为
  • Provider
    • React-Redux 提供的组件,可以让容器组件拿到 state
    • store 该组件上的接收外部 store 的属性
/// components/main.jsx 此时暂不使用 react hooks 的方式,并简化了一些代码
import React, { Component } from "react";
import { connect } from "react-redux";

class Main extends Component {
  render() {
    return (
      <div>
        <p>main</p>
      </div>
    );
  }
}

export default connect()(Main);
...

/// index.js
import React from "react";
import ReactDom from "react-dom";
import { Provider } from "react-redux";
import Main from "./components/main";
import store from "./reducers";

ReactDom.render(
  <Provider store={store}> // Provider 的组件使用
    <Main />
  </Provider>,
  document.getElementById("root")
);

mapStateToProps

/// components/main.jsx
...
const mapStateToProps = (state, ownProps) => {
  console.log(state);
  console.log(ownProps);
  return state;
};
// 新增 connect 函数第一个参数 mapStateToProps
export default connect(mapStateToProps)(Main);
...

/// index.js

ReactDom.render(
  <Provider store={store}>
    <Main bar="bar" /> // 入口文件处,添加一个 prop 
  </Provider>,
  document.getElementById("root")
);
...

/// 在控制台会输出当前的 store 和 Main 组件的 props,拿到这些信息后,就可以在 Main 组件的 render 方法中以 props 的形式使用 store 中的值
class Main extends Component {
  render() {
    console.log(this.props); // 此时组件的 props 除了组件本身的 bar,还有经过 mapStateToProps 处理后,把 state 转为 props 的那些值,之前的方法是返回了所有的 reducers ,正常开发中,我们会按需筛选下
    return (
      <div>
        <p>main</p>
      </div>
    );
  }
}
...

/// 输出指定的 reducer
const mapStateToProps = state => state.appReducer;

mapDispatchToProps

import React, { Component } from "react";
import { connect } from "react-redux";
import { sendReqInfoSH } from "../reducers/actionCreator"; // 这是 action

class Main extends Component {
  render() {
    const { onClick } = this.props;
    return (
      <div>
        <p>main</p>
        <button onClick={() => onClick("payload")}>点击</button>
      </div>
    );
  }
}

const mapStateToProps = state => state.appReducer;

// 此处演示的是 mapDispatchToProps 为函数的形式,接收两个参数,第一个就是发起 action 的 dispatch 方法,第二个是组件的 props,返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onClick: val => { // 这个方法可以接收参数,这个参数可以传递到 action 的 payload,这样就可以给异步请求传参
      console.log("ownProps", ownProps);
      console.log("val", val);
      dispatch(sendReqInfoSH());
    }
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Main);
...

/// 如果已经设定好了 action creator,mapDispatchToProps 可以使用一种对象的形式
const mapDispatchToProps = {
  onClick: val => sendReqInfoSH(val)
};

mergeProps

const mergeProps = (stateProps, dispatchProps, ownProps) => {
  console.log(stateProps); // mapStateToProps 的结果
  console.log(dispatchProps); // mapDispatchToProps 的结果
  console.log(ownProps); // 组件本身的 props
  return { ...stateProps, ...dispatchProps, ...ownProps };
};

export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Main);

Hooks

7.1.0 版本之上的 react-redux 支持 react hooks,这些 hooks 的相关语法只能应用函数组件上,不可应用 class 组件

useSelector()

const result: any = useSelector(selector: Function, equalityFn?: Function)

允许使用选择器功能从 Redux 存储状态提取数据(类似 connect 的 mapStateToProps),每当函数组件渲染时,选择器都将运行(除非引用没有更改)。useSelector() 还将订阅 Redux 存储,并在 dispatch action 时运行 selector

  • 选择器可以返回任何结果,而不仅仅是对象。选择器的返回值将用作 useSelector() 挂钩的返回值。
  • 当一个 action 被 dispatch 时,useSelector() 将对前一个选择器结果值和当前结果值进行参考比较。如果它们不同,则将强制重新渲染组件。如果它们相同,则组件将不会重新渲染。
  • selector 函数并没有收到 ownProps 说法(这是相比 connect 而言)。但是,可以通过闭包或使用柯里化 selector 来使用 props。
  • 使用记忆(memoizing) selector时必须格外小心
  • useSelector() 默认情况下使用严格相等(===),而不是浅相等(==
/// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

import Detail from './pages/detail';
import store from './store'; // 可以继续沿用之前写 class 组件时的 store 


ReactDOM.render(
  <Provider store={store}>
    <Detail/>
  </Provider>,
  document.getElementById('root')
);
...

/// detail.jsx
import React, {useState} from 'react';
import { useSelector } from 'react-redux';

const Detail = () => {
    const count = useSelector(state => {
        console.log(state);
        return state.mainReduer.count;
    }); // 这和 mapStateToProps 功能类似,可以直接读取 store 中的数据
    const [name, setName] = useState('detail'); // 这还可以和 react hooks 混用
    return (
        <div>
            <p>{ count }</p>
            <p>{ name }</p>
            <button onClick={() => setName(`${name}-`)}>修改名称</button>
        </div>
     );
}

export default Detail;

现在这个代码有个问题,如果点击按钮,useSelector 会一直执行,按目前的逻辑其实不应该执行的,因为此时,并没有设定更新 count 的 action,那这个count 正常应该是获取后,相关逻辑就应该不再执行,如果是纯的react hooks,我们可以借助useMemo,useCallback来解决这个问题,但是这里需要使用一个 redux 的中间件 reselect

reselect

reselect 是 redux 的一个中间件,主要解决重复计算的问题

  • createSelector(...inputSelectors | [inputSelectors], resultFunc) reselect 其中的一个ai,接受一个或多个选择器,或者一个选择器数组,计算他们的值并将它们作为参数传递给 resultFunc。只要输入选择器不发生改变(inputSelectors),reselect 会直接返回上一次计算缓存的结果,避免多余的重复计算
import React, {useState, useMemo} from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect'; // createSelector

const getCount = state => state.mainReduer.count;
const selectCount = createSelector(getCount, (res) => {
    console.log('res', res);
    return res;
}); // 计算并缓存相关值,这个缓存还可以应用于 class 组件中

const Detail = () => {
    const count = useSelector(selectCount); // 使用缓存 selector,这样除非 getCount 更新,否则缓存 selector 是不会重新计算的
    const [name, setName] = useState('detail');
    const [list, setList] = useState('list');
    return (
        <div>
            <p>{ count }</p>
            <p>{ name }</p>
            <button onClick={() => setName(`${name}-`)}>修改名称</button>
            <button onClick={() => setName(`${list}-`)}>修改list</button>
        </div>
     );
}

export default Detail;

useDispatch()

  • const dispatch = useDispatch() 这个Hook返回Redux store中对dispatch函数的引用
import React, {useState, useMemo} from 'react';
import { useSelector, useDispatch } from 'react-redux'; // 引入 useDispatch
import { createSelector } from 'reselect';
import { increment, decrement } from '../store/actionCreator';

const getCount = state => state.mainReduer.count;
const selectCount = createSelector(getCount, (res) => {
    console.log('res', res);
    return res;
});

const Detail = () => {
    const count = useSelector(selectCount);
    const dispatch = useDispatch();
    const [name, setName] = useState('detail');
    const [list, setList] = useState('list');
    return (
        <div>
            <p>{ count }</p>
            <p>{ name }</p>
            <button onClick={() => setName(`${name}-`)}>修改名称</button>
            <button onClick={() => setList(`${list}-`)}>修改list</button>
            <button onClick={() => dispatch(increment())}>count增加</button>
            <button onClick={() => dispatch(decrement())}>count减少</button>
        </div>
     );
}

export default Detail;

其他还有些 api ,只不过看官网也都没建议使用,也就不再研究了。通过使用 useSelectoruseDispatch ,可以替代之前在 class组件 中使用的 connect,而且可以很容易的和 React hooks 配合使用。最主要的是可以在函数组件中和之前的 redux 无缝配合,中间件、时间旅行都可以正常使用,配合 reselect 的缓存能力,一部分情况下,也可以代替 useMemo 的功能