前言

这篇介绍一下redux,还有react和redux联用的方法

Redux基本概念

redux官方文档:https://redux.js.org/api/api-reference

redux的工作流大概是这样的

2019-08-20-14-23-05

Action

Action用于表示提交的修改的类型

  • action必须是一个plain-object,它的__proto__必须指向Object.prototype
  • action中必须有type属性,该属性用于描述操作的类型,一般是字符串,也可以是其他类型
  • 通常,使用payload属性表示附加数据
  • action创建函数应为无副作用的纯函数
  • 为了方便利用action创建函数来分发action,redux提供了一个函数bindActionCreators,该函数用于增强action创建函数的功能,使它不仅可以创建action,并且创建后会自动完成分发。

最简单的action可以是下面这样的

1
2
3
4
{
type : "increase",
payload : 1
}

这个action用于表示进行增加的操作

但我们一般会对action进行封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// usersAction.js
export const ADD_USER = Symbol("add-user");
export const DELETE_USER = Symbol("delete-user");
export const UPDATE_USER = Symbol("update-user");

export const createAddUserAction = (user) => ({
type: ADD_USER,
payload: user
})

export const createDeleteUserAction = (id) => ({
type: DELETE_USER,
payload: id
})

export const createUpdateUserAction = (id, newUserData) => ({
type: DELETE_USER,
payload: {
...newUserData,
id
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
// loginUserAction.js
export const SET_LOGIN_USER = Symbol("set-login-user");

/**
* 设置登录用户的action
* @param {*} user
*/
export function createSetLoginUserAction(user) {
return {
type: SET_LOGIN_USER,
payload: user
}
}

Reducer

Reducer是用于改变数据的函数,它负责根据action的类型,进行实际的数据修改

  • 一个数据仓库,有且仅有一个reducer,并且通常情况下,一个工程只有一个仓库,因此,一个系统,只有一个reducer
  • reducer必须是一个没有副作用的纯函数,它接收两个参数,返回新的state(不会和之前的state合并)
    • state:之前的状态
    • action:分发的action
  • reducer被调用的时机
    • 通过store.dispatch,分发了一个action,此时,会调用reducer
    • 当创建一个store的时候,会调用一次reducer,我们可以利用这一点,用reducer初始化状态
  • 可以用combineReducers来合并reducer,得到一个新的reducer,该新的reducer管理一个对象,该对象中的每一个属性交给对应的reducer管理。

简单的reducer实例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// loginUser.js
import { SET_LOGIN_USER } from "../action/loginUserAction"

const initialState = null

export default (state = initialState, { type, payload }) => {
switch (type) {
case SET_LOGIN_USER:
return payload
default:
return state
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// users.js
import * as usersAction from "../action/usersAction"
let id = 0;
function getId() {
return id++;
}

const initialState = [
{ id: getId(), name: "用户1", age: 11 },
{ id: getId(), name: "用户2", age: 12 }
];

export default (state = initialState, { type, payload }) => {
switch (type) {
case usersAction.ADD_USER:
return [...state, payload];
case usersAction.DELETE_USER:
return state.filter(it => it.id !== payload);
case usersAction.UPDATE_USER:
return state.map(it => it.id === payload.id ? {...it, ...payload} : it);
default:
return state
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// reducer/index.js
import loginUserReducer from "./loginUser"
import usersReducer from './users'
import { combineReducers } from "redux"

// export default (state = {}, action) => {
// const newState = {
// loginUser: loginUserReducer(state.loginUser, action),
// users: usersReducer(state.users, action)
// };
// return newState;
// }

// 这段代码和上面的代码的效果是一样的
export default combineReducers({
loginUser: loginUserReducer,
users: usersReducer
})

Store

数据仓库对象,内部保存了数据

可以通过createStore方法创建的对象

该对象的成员:

  • dispatch:分发一个action
  • getState:得到仓库中当前的状态
  • replaceReducer:替换掉当前的reducer
  • subscribe:注册一个监听器,监听器是一个无参函数,该分发一个action之后,会运行注册的监听器。该函数会返回一个函数,用于取消监听

使用的示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createStore } from "redux";
import reducer from "./reducer"
import { createAddUserAction, createDeleteUserAction } from "./action/usersAction"


const store = createStore(reducer);

console.log(store)

const unListen = store.subscribe(() => {
console.log(store.getState());
})

store.dispatch(createAddUserAction({
id: 3,
name: "sena",
age: 16
}));

unListen(); //取消监听

store.dispatch(createDeleteUserAction(3));

redux的简单实现

因为redux的实现还是比较简单的,所以这里就简单模拟一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// createStore.js
import ActionTypes from "./utils/ActionTypes"
import isPlainObject from "./utils/isPlainObject"

/**
* 实现createStore的功能
* @param {function} reducer reducer
* @param {any} defaultState 默认的状态值
*/
export default function (reducer, defaultState) {

let currentReducer = reducer, //当前使用的reducer
currentState = defaultState; //当前仓库中的状态

const listeners = []; //记录所有的监听器(订阅者)

function dispatch(action) {
//验证action
if (!isPlainObject(action)) {
throw new TypeError("action must be a plain object");
}
//验证action的type属性是否存在
if (action.type === undefined) {
throw new TypeError("action must has a property of type");
}
// store初始化时要先调用一次reducer
currentState = currentReducer(currentState, action)

//运行所有的订阅者(监听器)
for (const listener of listeners) {
listener();
}
}

function getState() {
return currentState;
}

/**
* 添加一个监听器(订阅器)
*/
function subscribe(listener) {
listeners.push(listener); //将监听器加入到数组中
let isRemove = false;//是否已经移除掉了
return function () {
if (isRemove) {
return;
}
//将listener从数组中移除
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
isRemove = true;
}
}

//创建仓库时,需要分发一次初始的action
dispatch({
type: ActionTypes.INIT()
})

return {
dispatch,
getState,
subscribe
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// bindActionCreators.js
export default function (actionCreators, dispatch) {
if (typeof actionCreators === "function") {
return getAutoDispatchActionCreator(actionCreators, dispatch);
}
else if (typeof actionCreators === "object") {
const result = {}; //返回结果
// 遍历key
for (const key in actionCreators) {
if (actionCreators.hasOwnProperty(key)) {
const actionCreator = actionCreators[key]; //取出对应的属性值
if (typeof actionCreator === "function") {
result[key] = getAutoDispatchActionCreator(actionCreator, dispatch);
}
}
}
return result;
}
else {
throw new TypeError("actionCreators must be an object or function which means action creator")
}
}

/**
* 得到一个自动分发的action创建函数
*/
function getAutoDispatchActionCreator(actionCreator, dispatch) {
return function (...args) {
const action = actionCreator(...args)
dispatch(action);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// combineReducers.js
import isPlainObject from "./utils/isPlainObject"
import ActionTypes from "./utils/ActionTypes"

function validateReducers(reducers) {
if (typeof reducers !== "object") {
throw new TypeError("reducers must be an object");
}
if (!isPlainObject(reducers)) {
throw new TypeError("reducers must be a plain object");
}
//验证reducer的返回结果是不是undefined
for (const key in reducers) {
if (reducers.hasOwnProperty(key)) {
const reducer = reducers[key];//拿到reducer
//传递一个特殊的type值
let state = reducer(undefined, {
type: ActionTypes.INIT()
})
// 进行一些验证操作
if (state === undefined) {
throw new TypeError("reducers must not return undefined");
}
state = reducer(undefined, {
type: ActionTypes.UNKNOWN()
})
if (state === undefined) {
throw new TypeError("reducers must not return undefined");
}
}
}
}

export default function (reducers) {
//1. 验证
validateReducers(reducers);
/**
* 返回的是一个reducer函数
*/
return function (state = {}, action) {
const newState = {}; //要返回的新的状态
// 遍历 & 合并
for (const key in reducers) {
if (reducers.hasOwnProperty(key)) {
const reducer = reducers[key];
newState[key] = reducer(state[key], action);
}
}
return newState; //返回状态
}
}
1
2
3
4
5
6
7
8
9
10
/**
* 判断某个对象是否是一个plain-object
* @param {*} obj
*/
export default function isPlainObject(obj) {
if (typeof obj !== "object") {
return false;
}
return Object.getPrototypeOf(obj) === Object.prototype;
}

中间件

中间件类似于插件,可以在不影响原本功能、并且不改动原本代码的基础上,对其功能进行增强。

在Redux中,中间件主要用于增强dispatch函数,Redux中间件的基本原理,是更改仓库中的dispatch函数(缓存原来的dispatch,然后进行一层包裹,这样就可以增强dispatch的功能了)

包裹后就成了这样

image-20210205171214915

举个例子,比如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 中间件
function M1(store) {
// 返回一个dispatch创建函数
return function(next) {
// 这就是实际调用的dispatch
// 创建的dispatch
return function(action) {
console.log('A middleware1 开始');
next(action)
console.log('B middleware1 结束');
};
};
}

function M2(store) {
return function(next) {
return function(action) {
console.log('C middleware2 开始');
next(action)
console.log('D middleware2 结束');
};
};
}

function M3(store) {
return function(next) {
return function(action) {
console.log('E middleware3 开始');
next(action)
console.log('F middleware3 结束');
};
};
}

function reducer(state, action) {
if (action.type === 'MIDDLEWARE_TEST') {
console.log('======= G =======');
}
return {};
}

var store = Redux.createStore(
reducer,
// 应用中间件
Redux.applyMiddleware(
M1,
M2,
M3
)
);

store.dispatch({ type: 'MIDDLEWARE_TEST' });

最后代码的运行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
            --------------------------------------
| middleware1 |
| ---------------------------- |
| | middleware2 | |
| | ------------------- | |
| | | middleware3 | | |
| | | | | |
next next next ——————————— | | |
dispatch —————————————> | reducer | — 收尾工作->|
nextState <————————————— | G | | | |
| A | C | E ——————————— F | D | B |
| | | | | |
| | ------------------- | |
| ---------------------------- |
--------------------------------------


顺序 A -> C -> E -> G -> F -> D -> B
\---------------/ \----------/
↓ ↓
更新 state 完毕 收尾工作

中间件处理的简单实现

首先,我们要有一个compose函数,用于组合函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export default function compose(...funcs) {
if (funcs.length === 0) {
//如果没有要组合的函数,则返回的函数原封不动的返回参数
return args => args;
}
else if (funcs.length === 1) {
//要组合的函数只有一个
return funcs[0];
}

// 进行函数组合
return funcs.reduce((a, b) => (...args) => a(b(...args)))

// 进行函数组合的另一种写法
// 效果是一样的
// 只是这种好读很多
// return function (...args) {
// let lastReturn = null; //记录上一个函数返回的值,它将作为下一个函数的参数
// for (let i = funcs.length - 1; i >= 0; i--) {
// const func = funcs[i];
// if (i === funcs.length - 1) {//数组最后一项
// lastReturn = func(...args)
// }
// else {
// lastReturn = func(lastReturn)
// }
// }
// return lastReturn;
// }
}

然后在applyMiddleware中使用compose函数对dispath使用中间件包装即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import compose from "./compose"
/**
* 注册中间件
* @param {...any} middlewares 所有的中间件
*/
export default function (...middlewares) {
return function (createStore) { //给我创建仓库的函数
//下面的函数用于创建仓库
return function (reducer, defaultState) {
//创建仓库
const store = createStore(reducer, defaultState);
let dispatch = () => { throw new Error("目前还不能使用dispatch") };
const simpleStore = {
getState: store.getState,
dispatch: store.dispatch
}
//给dispatch赋值
//根据中间件数组,得到一个dispatch创建函数的数组
const dispatchProducers = middlewares.map(mid => mid(simpleStore));
dispatch = compose(...dispatchProducers)(store.dispatch);
// 返回被覆盖了dispatch的仓库
return {
...store,
dispatch
}
}
}
}

另外,我们要对createStore进行一些参数上的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import ActionTypes from "./utils/ActionTypes"
import isPlainObject from "./utils/isPlainObject"

/**
* 实现createStore的功能
* @param {function} reducer reducer
* @param {any} defaultState 默认的状态值
*/
export default function createStore(reducer, defaultState, enhanced) {
//enhanced表示applyMiddleware返回的函数
if (typeof defaultState === "function") {
//第二个参数是应用中间件的函数返回值
enhanced = defaultState;
defaultState = undefined;
}
if (typeof enhanced === "function") {
//进入applyMiddleWare的处理逻辑
return enhanced(createStore)(reducer, defaultState);
}
}

这样就可以简单的模拟中间件的处理过程了

常用中间件

redux-logger

这个中间件用于打印一些日志,比我们自己写的好看,一般会把它放在中间件的最后一个

https://www.npmjs.com/package/redux-logger

1
yarn add redux-logger

image-20210205210606396

1
2
import logger from "redux-logger";
const store = createStore(reducer, applyMiddleware(logger));

redux-thunk

https://www.npmjs.com/package/redux-thunk

redux-thunkredux-promiseredux-saga都是在用来在dispatch中处理副作用的

redux-thunk允许你的action是一个带有副作用的函数,当action是一个函数被分发时,thunk会阻止该action继续被移交(不调用next),而是直接调用函数

也就相当于做了一个拦截过滤而已

thunk会向函数中传递三个参数

  • dispatch:相当于store.dispatch
  • getState:相当于store.getState
  • extra:用户设置的额外参数

一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// usersAction.js
export const Set_User = Symbol("set-user");

export const createSetUserAction = function (users) {
return {
type : Set_User,
payload : users
}
}
// redux-thunk允许你dispatch一个带有副作用的函数
// 这个函数里可以进行ajax请求
export function getAllUser() {
return async function (dispatch) {
let data = await getUserData();
dispatch(createSetUserAction(data));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// user.js
function getId() {
return id++;
}

const initialState = [

];

export default (state = initialState, { type, payload }) => {
switch (type) {
case usersAction.Set_User:
return payload;
default:
return state
}
}

1
2
3
4
5
6
7
8
9
10
11
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer"
import logger from "redux-logger";
import thunk from "redux-thunk"
import {getAllUser} from "./action/usersAction";


const store = createStore(reducer, applyMiddleware(thunk, logger));

// 调用dispatch
store.dispatch(getAllUser())

redux-promise

redux-promise中间件允许action是一个promise,它的内部流程如下

  • 如果action是一个promise,则会等待promise完成,将完成的结果作为action触发
  • 如果action不是一个promise,则判断其payload是否是一个promise,如果是,等待promise完成,然后将得到的结果作为payload的值触发。
  • 否则,进行移交
1
2
3
4
5
6
7
8
9
10
11
12
// usersAction.js

// redux-promise中间件允许action是一个promise
// 在promise中,如果要触发action,使用resolve即可
export function getAllUserPromisify() {
return new Promise(resolve => {
setTimeout(async () => {
const data = await getUserData();
resolve(createSetUserAction(data));
})
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.js
import reducer from "./reducer"
import logger from "redux-logger";
import thunk from "redux-thunk";
import reduxPromise from "redux-promise"
import {getAllUser, getAllUserPromisify} from "./action/usersAction";


const store = createStore(reducer, applyMiddleware(thunk, reduxPromise, logger));

// 调用dispatch
// store.dispatch(getAllUser())

store.dispatch(getAllUserPromisify())

redux-saga

https://redux-saga.js.org/

redux-saga不仅可以用来处理异步操作,因为它提供了很多指令,所以你可以用它玩出各种花里胡哨的操作(代价就是用起来复杂了)

和之前两个中间件的区别是,之前两个中间件是把异步操作放在action中,saga是把异步操作放在saga

在最开始时,要启动一个saga任务,在这个任务中可以使用指令,只要把指令放在yield后,saga中间件就可以捕获到指令,从而对不同的指令进行特殊处理,从而控制整个任务的流程(指令前面必须使用yield,以确保该指令的返回结果被saga控制)

PS:每个指令本质上就是一个函数,该函数调用后,会返回一个指令对象,saga会接收到该指令对象,进行各种处理

saga中间件会在saga任务完成后结束

下面是常用的saga指令

  • take指令:【阻塞】监听某个action,如果action发生了,则会进行下一步处理,take指令仅监听一次。yield得到的是完整的action对象
  • all指令:【阻塞】该函数传入一个数组,数组中放入生成器,saga会等待所有的生成器全部完成后才会进一步处理
  • takeEvery指令:不断的监听某个action,当某个action到达之后,运行一个函数。takeEvery永远不会结束当前的生成器
  • delay指令:【阻塞】阻塞指定的毫秒数
  • put指令:用于重新触发action,相当于dispatch一个action
  • call指令:【可能阻塞】用于副作用(通常是异步)函数调用(如果返回值是一个promise,等待promise完成,否则把返回值传入继续运行)
  • apply指令:【可能阻塞】用于副作用(通常是异步)函数调用(如果返回值是一个promise,等待promise完成,否则把返回值传入继续运行)
  • select指令:用于得到当前仓库中的数据
  • cps指令:【可能阻塞】用于调用那些传统的回调方式的异步函数(自动传入回调函数,并等待回调函数调用)
  • fork:用于开启一个新的任务,该任务不会阻塞,该函数需要传递一个生成器函数,fork返回了一个对象,类型为Task
  • cancel:用于取消一个或多个任务,实际上,取消的实现原理,是利用generator.return。cancel可以不传递参数,如果不传递参数,则取消当前任务线。
  • takeLastest:功能和takeEvery一致,只不过,会自动取消掉之前开启的任务
  • cancelled:判断当前任务线是否被取消掉了
  • race:【阻塞】竞赛,可以传递多个指令,当其中任何一个指令结束后,会直接结束,与Promise.race类似。返回的结果,是最先完成的指令结果。并且,该函数会自动取消其他的任务

举个例子,还是用之前的请求用户的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// index.js
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer"
import logger from "redux-logger";
import thunk from "redux-thunk";
import reduxPromise from "redux-promise";
import createSagaMiddleware from "redux-saga";
import {
createAddUserAction,
createGetAllUserSagaAction,
Get_All_User_Saga_Action,
getAllUser,
getAllUserPromisify
} from "./action/usersAction";
import rootSaga from "../saga";
import {createAddNumAsyncAction} from "./action/numAction";

// 创建一个saga的中间件
const sagaMid = createSagaMiddleware();

const store = createStore(reducer, applyMiddleware(thunk, reduxPromise, sagaMid, logger));

// 开启saga任务
sagaMid.run(rootSaga);

// 放在window方便调用
window.dispatchGetUserAction = function () {
store.dispatch(createGetAllUserSagaAction());
}

window.dispatchAddNumberAction = function () {
store.dispatch(createAddNumAsyncAction(10));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// action
export const Get_All_User_Saga_Action = Symbol("create-get-all-user-saga");
const addNumAsyncType = Symbol("add_num_async");
export function createGetAllUserSagaAction () {
return {
type : Get_All_User_Saga_Action
}
}
export function createAddNumAsyncAction(payload) {
return {
type : addNumAsyncType,
payload
}
}

然后对saga的任务进行配置

1
2
3
4
5
6
7
8
9
10
// saga/index.js
import { all } from "redux-saga/effects"
import userTask from "./userTask";
import numberTask from "./numberTask";
// saga任务
export default function *() {
console.log("saga启动");
yield all([userTask(), numberTask()]);
console.log("saga结束");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// saga/numberTask.js
import { takeEvery, put, delay} from "redux-saga/effects"
import {addNumAsyncType, createAddNumAction} from "../store/action/numAction";


// 回调
function * updateNumberAsync(action) {
console.log("updateNumberAsync执行");
yield delay(1000);
yield put(createAddNumAction(action.payload));
}

export default function *() {
let userData = yield takeEvery(addNumAsyncType, updateNumberAsync);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// saga/userTask.js
import { takeEvery, put, call, select } from "redux-saga/effects"
import {createSetUserAction, Get_All_User_Saga_Action, Set_User} from "../store/action/usersAction";
import {getUserData} from "../net/getUserData";

// 回调
function *requestUserData() {
console.log("requestUserData执行");
let data = yield getUserData();
yield put(createSetUserAction(data));
}

export default function *() {
yield takeEvery(Get_All_User_Saga_Action, requestUserData);
}

效果如下

image-20210206160724271

image-20210206161012385

常用中间件的实现

redux-thunk

非常简单,看看就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createThunkMiddleware(extra) {
//该函数返回一个thunk中间件
return store => next => action => {
// action是函数时调用
if (typeof action === "function") {
return action(store.dispatch, store.getState, extra);
}
// 否则进行移交
return next(action);
}
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

redux-promise

比上个复杂点,不过还是问题不大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { isPlainObject, isString } from "lodash"
import isPromise from "is-promise"

export default ({ dispatch }) => next => action => {
if (!isFluxStandardAction(action)) {
//如果不是一个标准的action
//如果action是一个promise,则将其resolve的值dispatch,否则,不做任何处理,交给下一个中间件
return isPromise(action) ? action.then(dispatch) : next(action);
}
return isPromise(action.payload) ?
action.payload
.then(payload => dispatch({ ...action, payload }))
.catch(error => dispatch({ ...action, payload: error, error: true })) :
next(action)
}

/**
* 判断一个action是不是标准的flux的action
* @param {*} action
*/
function isFluxStandardAction(action) {
//action必须是一个平面对象 plain-object
//action.type必须是一个字符串
//action的属性不能包含其他非标准属性 标准属性["type", "payload", "error", "meta"]
return isPlainObject(action)
&&
isString(action.type)
&&
Object.keys(action).every(key => ["type", "payload", "error", "meta"].includes(key));
}

redux-saga

头秃,写不动,有空再写orz

react-redux

用于连接redux和react

react-redux提供了一个Provider组件,它可以将redux的仓库放到一个上下文中,使用时需要传入reduxstore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from "react-redux"
import store from "./store/index"

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


然后加入一些actionreducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const actionTypes = {
increase: Symbol("increase"),
decrease: Symbol("decrease"),
asyncIncrease: Symbol("async-increase"),
asyncDecrease: Symbol("async-decrease")
}

export function increase() {
return { type: actionTypes.increase }
}

export function decrease() {
return { type: actionTypes.decrease }
}

export function asyncIncrease() {
return { type: actionTypes.asyncIncrease }
}

export function asyncDecrease() {
return { type: actionTypes.asyncDecrease }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// reducer/index.js
import loginUserReducer from "./loginUser"
import usersReducer from './users'
import { combineReducers } from "redux"
import {numReducer} from "./num";
import counterReducer from "./counterReducer"


export default combineReducers({
loginUser: loginUserReducer,
users: usersReducer,
num : numReducer,
counter : counterReducer
})
1
2
3
4
5
6
7
8
9
10
11
12
// counterReducer.js
import { actionTypes } from "../../action/counter"
export default function (state = 10, { type }) {
switch (type) {
case actionTypes.increase:
return state + 1;
case actionTypes.decrease:
return state - 1;
default:
return state;
}
}

编写一个Counter组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React from 'react'
import store from "../store"
import {increase, decrease } from "../store/action/counter"
import { connect } from "react-redux"

// 展示组件
function Counter(props) {
return (
<div>
<h1>{props.number}</h1>
<p>
<button onClick={props.onDecrease}> 减 </button>
<button onClick={props.onIncrease}> 加 </button>
</p>
</div>
)
}

/**
* 建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。
* @param {*} state
*/
function mapStateToProps(state) {
return {
number: state.counter
}
}

/**
* 建立 UI 组件的参数到store.dispatch方法的映射。
* @param dispatch
*/
function mapDispatchToProps(dispatch) {
return {
onDecrease() {
dispatch(decrease())
},
onIncrease() {
dispatch(increase())
},
}
}


export default connect(mapStateToProps, mapDispatchToProps)(Counter)

// 如果不用connect, 就可以这么写
// 手动创建使用一个容器组件来处理数据
// export default class CounterContainer extends React.Component {
// constructor(props) {
// super(props);
// this.state = mapStateToProps(store.getState());
// store.subscribe(() => {
// this.setState(mapStateToProps(store.getState()))
// })
// }
//
// render() {
// const eventHandlers = mapDispatchToProps(store.dispatch)
// return <Counter {...this.state} {...eventHandlers} />
// }
// }

在这里出现了两个新概念

  • UI 组件:

    • 只负责 UI 的呈现,不带有任何业务逻辑
    • 没有状态(即不使用this.state这个变量)
    • 所有数据都由参数(this.props)提供
    • 不使用任何 Redux 的 API
  • 容器组件

    • 负责管理数据和业务逻辑,不负责 UI 的呈现
    • 带有内部状态
    • 可以使用 Redux 的 API

简单来说就是,UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。

React-Redux 提供的connect方法,用于从 UI 组件生成容器组件。

可以看出来,使用还是非常简单的,只要使用connect方法对组件进行了一层高阶组件包装来获取数据就可以了

简单的react-redux实现

创建上下文

1
2
3
// ctx.js
import React from "react"
export default React.createContext();

提供Provider组件

1
2
3
4
5
6
7
8
import React from "react"
import ctx from "./ctx"

export default function Provider(props) {
return <ctx.Provider value={props.store}>
{props.children}
</ctx.Provider>
}

实现Connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import React, { PureComponent } from "react"
import { bindActionCreators } from "redux"
import ctx from "./ctx";

export default function (mapStateToProps, mapDispatchToProps) {
/**
* 返回一个高阶组件
*/
return function (Comp) {
//对于Container组件,只有它需要的数据发生变化时才会重新渲染
class Container extends PureComponent {
static contextType = ctx;

/**
* 得到需要传递的事件处理属性
*/
getEventHandlers() {
if (typeof mapDispatchToProps === "function") {
return mapDispatchToProps(this.store.dispatch, this.props);
}
else if (typeof mapDispatchToProps === "object") {
return bindActionCreators(mapDispatchToProps, this.store.dispatch)
}
}

constructor(props, context) {
super(props, context);
this.store = this.context;
// 映射state
if (mapStateToProps) {
// 状态中的数据,来自于仓库中映射的结果
this.state = mapStateToProps(this.store.getState(), this.props)
// 监听仓库中的数据变化
this.unlisten = this.store.subscribe(() => {
this.setState(mapStateToProps(this.store.getState(), this.props))
})
}
// 映射dispatch处理函数
if (mapDispatchToProps) {
this.handlers = this.getEventHandlers();
}
}

componentWillUnmount() {
if (this.unlisten) {
//当组件卸载时,取消监听
this.unlisten();
}
}

render() {
// 渲染传入的组件,并把数据传入
return <Comp {...this.state} {...this.handlers} {...this.props} />;
}
}
// 类组件的名称和传入的组件一致
Container.displayName = Comp.displayName || Comp.name;
return Container;
}
}

还是比较简单的,就是处理一下数据,进行转发而已

实现Connect-使用Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { useContext, useState, useEffect } from "react"
import { bindActionCreators } from "redux"
import ctx from "./ctx";

function compare(obj1, obj2) {
for (const key in obj1) {
if (obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}

export default function (mapStateToProps, mapDispatchToProps) {
// 高阶组件
return function (Comp) {
// 对于Container组件,只有它需要的数据发生变化时才会重新渲染
function Container(props) {
const store = useContext(ctx); //从上下文中拿到仓库
const [state, setState] = useState(mapStateToProps && mapStateToProps(store.getState()))

useEffect(() => {
return store.subscribe(() => {
let newState = mapStateToProps && mapStateToProps(store.getState());
// store更新时, 重新渲染组件
setState(prevState => {
if (compare(prevState, newState)) {
return prevState;
}
else {
return newState;
}
})
})
}, [store])

// 得到需要传递的事件处理属性
function getEventHandlers() {
if (typeof mapDispatchToProps === "function") {
return mapDispatchToProps(store.dispatch, props);
}
else if (typeof mapDispatchToProps === "object") {
return bindActionCreators(mapDispatchToProps, store.dispatch)
}
}
let handlers = {};
if (mapDispatchToProps) {
handlers = getEventHandlers();
}
return <Comp {...state} {...handlers} {...props} />
}
// 设置一下在调试面板里显示的名称,设置成和传入的组件名一致
Container.displayName = Comp.displayName || Comp.name;
return Container;
}
}

连接router和redux

有的时候,我们希望router中的一些数据要同步到redux

这时候我们就可以使用connected-react-router

https://www.npmjs.com/package/connected-react-router

使用这个库的步骤比较繁琐,如下

修改reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在combineReducers中进行配置
import {connectRouter} from "connected-react-router";
import {createBrowserHistory} from "history";
const browserHistory = createBrowserHistory();
export default combineReducers({
loginUser: loginUserReducer,
users: usersReducer,
num : numReducer,
counter : counterReducer,
// 指定redux中的字段
router : connectRouter(browserHistory)
})

export {browserHistory}

添加中间件

1
2
3
4
// 使用connected-react-router提供的中间件
import reducer, {browserHistory} from "./reducer"
import {routerMiddleware} from "connected-react-router";
const store = createStore(reducer, composeWithDevTools(applyMiddleware(routerMiddleware(browserHistory), thunk, reduxPromise, sagaMid, logger)));

修改路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用ConnectedRouter代替Router
import {ConnectedRouter} from "connected-react-router";
import {browserHistory} from "../../store/reducer";
function App(props) {
return (
<ConnectedRouter history={browserHistory}>
<Switch>
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
</Switch>
<ul>
<li>
<Link to="/page1">页面1</Link>
</li>
<li>
<Link to="/page2">页面2</Link>
</li>
</ul>
</ConnectedRouter>
)
}

手动跳转的方法

如果你不使用Link或者NavLink,而是要手动跳转,要使用connected-react-router提供的push, replace函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React, {useCallback} from 'react'
import {BrowserRouter as Router, Link, Route, Switch} from "react-router-dom"
import RouteGuard from "../RouteGuard";
// 使用ConnectedRouter
import {ConnectedRouter} from "connected-react-router";
import {browserHistory} from "../../store/reducer";
// 如果不使用Link和NavLink,而是要要手动跳转的话
// 使用connected-react-router库提供的方法
// 但是这个push, replace只是创建action, 还要进行分发
// 所以要用connect来关联redux和这个组件
import {push, replace} from "connected-react-router";
import {connect} from "react-redux";

function Page1() {
return <h1>Page1</h1>
}

function Page2() {
return <h1>Page2</h1>
}



function App(props) {
let handleClick = useCallback(() => {
props.toPage1();
}, [props.toPage1])
return (
<ConnectedRouter history={browserHistory}>
<Switch>
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
</Switch>
<ul>
<li>
<Link to="/page1">页面1</Link>
</li>
<li>
<Link to="/page2">页面2</Link>
</li>
<li>
<button onClick={handleClick}>页面1</button>
</li>
</ul>
</ConnectedRouter>
)
}

const mapDispatchToProps = (dispatch) => {
return {
toPage1() {
dispatch(push("/page1"))
}
}
}

export default connect(null, mapDispatchToProps)(App)

FAQ & Ref

图解Redux中middleware的洋葱模型