# Redux 核心概念
action reducer store
# MVC
它是一个 UI 的解决方案,用于降低 UI,以及 UI 关联的数据的复杂度。
传统的服务器端的 MVC
环境:
- 服务端需要响应一个完整的 HTML
- 该 HTML 中包含页面需要的数据
- 浏览器仅承担渲染页面的作用
以上的这种方式叫做服务端渲染,即服务器端将完整的页面组装好之后,一起发送给客户端。
服务器端需要处理 UI 中要用到的数据,并且要将数据嵌入到页面中,最终生成一个完整的 HTML 页面响应。
为了降低处理这个过程的复杂度,出现了 MVC 模式。
Controller: 处理请求,组装这次请求需要的数据 Model:需要用于 UI 渲染的数据模型 View:视图,用于将模型组装到界面中
前端 MVC 模式的困难
React 解决了 数据 -> 视图 的问题
- 前端的 controller 要比服务器复杂很多,因为前端中的 controller 处理的是用户的操作,而用户的操作场景是复杂的。
- 对于那些组件化的框架(比如 vue、react),它们使用的是单向数据流。若需要共享数据,则必须将数据提升到顶层组件,然后数据再一层一层传递,极其繁琐。 虽然可以使用上下文来提供共享数据,但对数据的操作难以监控,容易导致调试错误的困难,以及数据还原的困难。并且,若开发一个大中型项目,共享的数据很多,会导致上下文中的数据变得非常复杂。
比如,上下文中有如下格式的数据:
value = {
users: [{}, {}, {}],
addUser: function (u) {},
deleteUser: function (u) {},
updateUser: function (u) {},
};
2
3
4
5
6
# 前端需要一个独立的数据解决方案
Flux
Facebook 提出的数据解决方案,它的最大历史意义,在于它引入了 action 的概念
action 是一个普通的对象,用于描述要干什么。action 是触发数据变化的唯一原因
store 表示数据仓库,用于存储共享数据。还可以根据不同的 action 更改仓库中的数据
示例:
var loginAction = {
type: "login",
payload: {
loginId: "admin",
loginPwd: "123123",
},
};
var deleteAction = {
type: "delete",
payload: 1, // 用户id为1
};
2
3
4
5
6
7
8
9
10
11
12
Redux
在 Flux 基础上,引入了 reducer 的概念
reducer:处理器,用于根据 action 来处理数据,处理后的数据会被仓库重新保存。
# Action
- action 是一个 plain-object(平面对象)
- 它的proto指向 Object.prototype
- 通常,使用 payload 属性表示附加数据(没有强制要求)
- action 中必须有 type 属性,该属性用于描述操作的类型
- 但是,没有对 type 的类型做出要求
- 在大型项目,由于操作类型非常多,为了避免硬编码(hard code),会将 action 的类型存放到一个或一些单独的文件中(样板代码)。
- 为了方面传递 action,通常会使用 action 创建函数(action creator)来创建 action
- action 创建函数应为无副作用的纯函数
- 不能以任何形式改动参数
- 不可以有异步
- 不可以对外部环境中的数据造成影响
- action 创建函数应为无副作用的纯函数
- 为了方便利用 action 创建函数来分发(触发)action,redux 提供了一个函数
bindActionCreators,该函数用于增强 action 创建函数的功能,使它不仅可以创建 action,并且创建后会自动完成分发。
# Reducer
Reducer 是用于改变数据的函数
- 一个数据仓库,有且仅有一个 reducer,并且通常情况下,一个工程只有一个仓库,因此,一个系统,只有一个 reducer
- 为了方便管理,通常会将 reducer 放到单独的文件中。
- reducer 被调用的时机
- 通过 store.dispatch,分发了一个 action,此时,会调用 reducer
- 当创建一个 store 的时候,会调用一次 reducer
- 可以利用这一点,用 reducer 初始化状态
- 创建仓库时,不传递任何默认状态
- 将 reducer 的参数 state 设置一个默认值
- reducer 内部通常使用 switch 来判断 type 值
- reducer 必须是一个没有副作用的纯函数
- 为什么需要纯函数
- 纯函数有利于测试和调式
- 有利于还原数据
- 有利于将来和 react 结合时的优化
- 具体要求
- 不能改变参数,因此若要让状态变化,必须得到一个新的状态
- 不能有异步
- 不能对外部环境造成影响
- 为什么需要纯函数
- 由于在大中型项目中,操作比较复杂,数据结构也比较复杂,因此,需要对 reducer 进行细分。
- redux 提供了方法,可以帮助我们更加方便的合并 reducer
- combineReducers: 合并 reducer,得到一个新的 reducer,该新的 reducer 管理一个对象,该对象中的每一个属性交给对应的 reducer 管理。
# Store
Store:用于保存数据
通过 createStore 方法创建的对象。
该对象的成员:
- dispatch:分发一个 action
- getState:得到仓库中当前的状态
- replaceReducer:替换掉当前的 reducer
- subscribe:注册一个监听器,监听器是一个无参函数,该分发一个 action 之后,会运行注册的监听器。该函数会返回一个函数,用于取消监听
# Redux 中间件(Middleware)
中间件:类似于插件,可以在不影响原本功能、并且不改动原本代码的基础上,对其功能进行增强。在 Redux 中,中间件主要用于增强 dispatch 函数。
实现 Redux 中间件的基本原理,是更改仓库中的 dispatch 函数。
Redux 中间件书写:
中间件本身是一个函数,该函数接收一个 store 参数,表示创建的仓库,该仓库并非一个完整的仓库对象,仅包含 getState,dispatch。该函数运行的时间,是在仓库创建之后运行。
- 由于创建仓库后需要自动运行设置的中间件函数,因此,需要在创建仓库时,告诉仓库有哪些中间件
- 需要调用 applyMiddleware 函数,将函数的返回结果作为 createStore 的第二或第三个参数。
中间件函数必须返回一个 dispatch 创建函数
applyMiddleware 函数,用于记录有哪些中间件,它会返回一个函数
- 该函数用于记录创建仓库的方法,然后又返回一个函数
# redux-logger
一个专门为 Redux 设计的日志打印中间件,它将每一个 dispatch 的动作和当前 state 的变化以日志的形式展示在浏览器的控制台中,帮助开发者可视化地理解和追踪应用状态流。
redux-logger (opens new window)
import { createStore, applyMiddleware } from "redux";
// 默认选项的日志记录器
import logger from "redux-logger";
const store = createStore(reducer, applyMiddleware(logger));
2
3
4
5
# 利用中间件进行副作用处理
# redux-thunk
thunk 允许 action 是一个带有副作用的函数,当 action 是一个函数被分发时,thunk 会阻止 action 继续向后移交。
thunk 会向函数中传递三个参数:
- dispatch:来自于 store.dispatch
- getState:来自于 store.getState
- extra:来自于用户设置的额外参数
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
import logger from "redux-logger";
import thunk from "redux-thunk";
export default createStore(reducer, applyMiddleware(thunk, logger));
2
3
4
5
6
import store from "./index";
import { fetchUsers } from "./action/usersAction";
store.dispatch(fetchUsers());
2
3
4
// action/usersAction.js
export function fetchUsers() {
//由于thunk的存在,允许action是一个带有副作用的函数
return async function (dispatch) {
dispatch(createSetLoadingAction(true)); //正在加载
const users = await getAllStudents();
const action = createSetUsersAction(users);
dispatch(action);
dispatch(createSetLoadingAction(false));
};
}
export const createSetUsersAction = (users) => ({
type: SETUSERS,
payload: users, //用户数组
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# redux-promise
# redux-promise
如果 action 是一个 promise,则会等待 promise 完成,将完成的结果作为 action 触发,如果 action 不是一个 promise,则判断其 payload 是否是一个 promise,如果是,等待 promise 完成,然后将得到的结果作为 payload 的值触发。
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
import logger from "redux-logger";
import promise from "../redux-promise";
const store = createStore(reducer, applyMiddleware(promise, logger));
export default store;
2
3
4
5
6
7
/**
* 由于使用了redux-promise中间件,因此,允许action是一个promise,在promise中,如果要触发action,则使用resolve
*/
export function fetchStudents() {
return new Promise((resolve) => {
setTimeout(() => {
const action = setStudentsAndTotal(
[
{ id: 1, name: "aaa" },
{ id: 2, name: "bbb" },
],
2,
);
resolve(action);
}, 3000);
});
}
export async function fetchStudents(condition) {
const resp = await searchStudents(condition);
return setStudentsAndTotal(resp.datas, resp.cont);
}
export async function fetchStudents(condition) {
return {
type: actionTypes.setStudentsAndTotal,
payload: searchStudents(condition).then((resp) => ({
datas: resp.datas,
total: resp.cont,
})),
};
}
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
# 迭代器和可迭代协议
解决副作用的 redux 中间件
redux-thunk:需要改动 action,可接收 action 是一个函数
redux-promise:需要改动 action,可接收 action 是一个 promise 对象,或 action 的 payload 是一个 promise 对象
以上两个中间件,会导致 action 或 action 创建函数不再纯净。
redux-saga 将解决这样的问题,它不仅可以保持 action、action 创建函数、reducer 的纯净,而且可以用模块化的方式解决副作用,并且功能非常强大。
redux-saga 是建立在 ES6 的生成器基础上的,要熟练的使用 saga,必须理解生成器。
要理解生成器,必须先理解迭代器和可迭代协议。
# 迭代
类似于遍历
遍历:有多个数据组成的集合数据结构(map、set、array 等其他类数组),需要从该结构中依次取出数据进行某种处理。
迭代:按照某种逻辑,依次取出下一个数据进行处理。
# 迭代器 iterator
JS 语言规定,如果一个对象具有 next 方法,并且 next 方法满足一定的约束,则该对象是一个迭代器(iterator)。
next 方法的约束:该方法必须返回一个对象,该对象至少具有两个属性:
- value:any 类型,下一个数据的值,如果 done 属性为 true,通常,会将 value 设置为 undefined
- done:bool 类型,是否已经迭代完成
通过迭代器的 next 方法,可以依次取出数据,并可以根据返回的 done 属性,判定是否迭代结束。
# 迭代器创建函数 iterator creator
它是指一个函数,调用该函数后,返回一个迭代器,则该函数称之为迭代器创建函数,可以简称位迭代器函数。
DETAILS
function createIterator(total) {
i = 1;
return {
next() {
var obj = {
//当前这一次迭代到的数据
value: i > total ? undefined : Math.random(),
done: i > total,
};
i++;
return obj;
},
};
}
var iterator = createIterator(5);
var next = iterator.next();
while (!next.done) {
//若当前迭代的数据不是迭代器的结束
//如果当前还有数据
console.log(next.value);
next = iterator.next();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 可迭代协议
ES6 中出现了 for-of 循环,该循环就是用于迭代某个对象的,因此,for-of 循环要求对象必须是可迭代的(对象必须满足可迭代协议)
可迭代协议是用于约束一个对象的,如果一个对象满足下面的规范,则该对象满足可迭代协议,也称之为该对象是可以被迭代的。
可迭代协议的约束如下:
- 对象必须有一个知名符号属性(
Symbol.iterator) - 该属性必须是一个无参的迭代器创建函数
DETAILS
//obj满足可迭代协议
//obj可被迭代
var obj = {
[Symbol.iterator]: function () {
var total = 3;
i = 1;
return {
next() {
var obj = {
//当前这一次迭代到的数据
value: i > total ? undefined : Math.random(),
done: i > total,
};
i++;
return obj;
},
};
},
};
//模拟for-of循环
var iterator = obj[Symbol.iterator]();
var result = iterator.next();
while (!result.done) {
//有数据
const item = result.value;
console.log(item); //执行循环体
result = iterator.next();
}
for (const item of obj) {
console.log(item);
}
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
# for-of 循环的原理
调用对象的[Symbol.iterator]方法,得到一个迭代器。不断调用 next 方法,只有返回的 done 为 false,则将返回的 value 传递给变量,然后进入循环体执行一次。
# 生成器 generator
# generator
生成器:由构造函数 Generator 创建的对象,该对象既是一个迭代器,同时,又是一个可迭代对象(满足可迭代协议的对象)
//伪代码
var generator = new Generator();
generator.next(); //它具有next方法
var iterator = generator[Symbol.iterator]; //它也是一个可迭代对象
for (const item of generator) {
//由于它是一个可迭代对象,因此也可以使用for of循环
}
2
3
4
5
6
7
8
注意:Generator 构造函数,不提供给开发者使用,仅作为 JS 引擎内部使用
# generator function
生成器函数(生成器创建函数):该函数用于创建一个生成器。
ES6 新增了一个特殊的函数,叫做生成器函数,只要在函数名与 function 关键字之间加上一个*号,则该函数会自动返回一个生成器
生成器函数的特点:
- 调用生成器函数,会返回一个生成器,而不是执行函数体(因为,生成器函数的函数体执行,受到生成器控制)
- 每当调用了生成器的 next 方法,生成器的函数体会从上一次 yield 的位置(或开始位置)运行到下一个 yield
- yield 关键字只能在生成器内部使用,不可以在普通函数内部使用
- 它表示暂停,并返回一个当前迭代的数据
- 如果没有下一个 yield,到了函数结束,则生成器的 next 方法得到的结果中的 done 为 true
- yield 关键字后面的表达式返回的数据,会作为当前迭代的数据
- 生成器函数的返回值,会作为迭代结束时的 value
- 但是,如果在结束过后,仍然反复调用 next,则 value 为 undefined
- 生成器调用 next 的时候,可以传递参数,该参数会作为生成器函数体上一次 yield 表达式的值。
- 生成器第一次调用 next 函数时,传递参数没有任何意义
- 生成器带有一个 throw 方法,该方法与 next 的效果相同,唯一的区别在于:
- next 方法传递的参数会被返回成一个正常值
- throw 方法传递的参数是一个错误对象,会导致生成器函数内部发生一个错误。
- 生成器带有一个 return 方法,该方法会直接结束生成器函数
- 若需要在生成器内部调用其他生成器,注意:如果直接调用,得到的是一个生成器,如果加入*号调用,则进入其生成器内部执行。如果是
yield* 函数()调用生成器函数,则该函数的返回结果,为该表达式的结果
DETAILS
function* g2() {
console.log("g2-开始");
let result = yield "g1";
console.log("g2-运行1");
result = yield "g2";
return 123;
}
//下面的函数是一个生成器函数,用于创建生成器
function* createGenerator() {
console.log("生成器函数的函数体 - 开始");
let result = yield 1; //将1作为第一次的迭代的值
result = yield* g2(); //result为g2函数的返回值
console.log("生成器函数的函数体 - 运行1", result);
result = yield 2; //将2作为第二次迭代的值
console.log("生成器函数的函数体 - 运行2", result);
result = yield 3;
console.log("生成器函数的函数体 - 运行3", result);
return "结束";
}
var generator = createGenerator(); //调用后,一定得到一个生成器
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# redux-saga
# redux-saga
中文文档地址:https://redux-saga-in-chinese.js.org/
- 纯净
- 强大
- 灵活
在 saga 任务中,如果 yield 了一个普通数据,saga 不作任何处理,仅仅将数据传递给 yield 表达式(把得到的数据放到 next 的参数中),因此,在 saga 中,yield 一个普通数据没什么意义。
saga 需要你在 yield 后面放上一些合适的 saga 指令(saga effects),如果放的是指令,saga 中间件会根据不同的指令进行特殊处理,以控制整个任务的流程。
每个指令本质上就是一个函数,该函数调用后,会返回一个指令对象,saga 会接收到该指令对象,进行各种处理
一旦 saga 任务完成(生成器函数运行完成),则 saga 中间件一定结束
- take 指令:【阻塞】监听某个 action,如果 action 发生了,则会进行下一步处理,take 指令仅监听一次。yield 得到的是完整的 action 对象
- all 指令:【阻塞】该函数传入一个数组,数组中放入生成器,saga 会等待所有的生成器全部完成后才会进一步处理
- takeEvery 指令:不断的监听某个 action,当某个 action 到达之后,运行一个函数。takeEvery 永远不会结束当前的生成器
# redux-actions
该库用于简化 action-types、action-creator 以及 reducer 官网文档:https://redux-actions.js.org/
# createAction
该函数用于帮助你创建一个 action 创建函数(action creator)
# createActions
该函数用于帮助你创建多个 action 创建函数
# handleAction(s)
# handleAction
简化针对单个 action 类型的 reducer 处理,当它匹配到对应的 action 类型后,会执行对应的函数
# handleActions
简化针对多个 action 类型的 reducre 处理
# combineActions
配合 createActions 和 handleActions 两个函数,用于处理多个 action-type 对应同一个 reducer 处理函数。