「React」一文带你了解 Redux
目录
- Redux 核心概念
- Redux 数据管理
- Redux 适用场景
- Redux 代码组织方式
- Redux API
- (1)createStore
- (2)Store
- (3)State
- (4)Action
- (5)Action Creator
- (6)store.dispatch()
- (7)Reducer
- (8)store.subscribe()
- Redux 最佳实践
- Flux 架构思想
- Redux 源码解读
- (1)目录结构
- (2)createStore源码
1. Redux 核心概念
先来看一下官方对 Redux 的描述:
Redux 是 JavaScript 状态容器,它提供可预测的状态管理。
来看一下这句话背后的深意:
- Redux 是为JavaScript应用而生的,也就是说它不是 React 的专利,React 可以用,Vue 可以用,原生 JavaScript 也可以用;
- Redux 是一个状态容器,什么是状态容器?来看个例子:
假如把一个 React 项目里面的所有组件拉进一个群,那么 Redux 就充当了这个群里的“群文件”角色,所有的组件都可以把需要在组件树里流动的数据存储在群文件里。当某个数据改变的时候,其他组件都能够通过下载最新的群文件来获取到数据的最新值。这就是“状态容器”的含义——存放公共数据的仓库。
应用的状态往往十分复杂,如果应用状态就是一个普通 JavaScript 对象,而任何能够访问到这个对象的代码都可以修改这个状态,就很容易乱了套。当 bug 发生的时候,我们发现是状态错了,但是也很难理清到底谁把状态改错了,到底是如何走到出 bug 这一步。Redux 的主要贡献,就是限制了对状态的修改方式,让所有改变都可以被追踪。
Redux 基本使用:
(1)在使用Redux之前,我们需要安装稳定版的Redux包。
因为 Redux 是一个中立的状态管理工具,和 React 没有直接联系。所以,如果在 React 应用中使用 Redux,除了要引入 Redux,还需要导入 react-redux 这个安装包,安装方法如下:
npm install --save redux react-redux
(2)在使用Redux时,需要将其引入组件:
import { createStore } from 'redux';
3. Redux 数据管理
Redux 主要由三部分组成:store、reducer 和 action。先来看看它们各自代表什么:
- store:就好比组件群里的“群文件”,它是一个单一的数据源,而且是只读的;
- action:就是“动作”的意思,它是对变化的描述。
举个例子,下面这个对象就是一个 action:
const action = {
type: "ADD_ITEM",
payload: '<li>text</li>'
}
- reducer 是一个函数,它负责对变化进行分发和处理,最终将新的数据返回给 store。
store、action 和 reducer 三者紧密配合,便形成了 Redux 独树一帜的工作流:
从上图可以看出,在 Redux 的整个工作过程中,数据流是严格单向的。
对于一个 React 应用来说,视图(View)层面的所有数据(state)都来自 store(再一次诠释了单一数据源的原则)。如果想对数据进行修改,只有一种途径:派发 action。action 会被 reducer 读取,进而根据 action 内容的不同对数据进行修改、生成新的 state(状态),这个新的 state 会更新到 store 对象里,进而驱动视图层面做出对应的改变。
对于组件来说,任何组件都可以通过约定的方式从 store 读取到全局的状态,任何组件也都可以通过合理地派发 action 来修改全局的状态。Redux 通过提供一个统一的状态容器,使得数据能够自由而有序地在任意组件之间穿梭, 这就是 Redux 实现组件间通信的思路。
3. Redux 适用场景
当一个 React 应用采用 Redux 之后,对于某个状态,到底是放在 Redux 的 Store 中呢,还是放在 React 组件自身的状态中呢?
- 如果所有状态全都放在 Redux 的 Store 上,那就要对应增加 reducer 和 action 的代码,虽然拥有了可以跟踪的好处,但是对一些很细小的状态也要增加 reducer 和 action,会感觉得不偿失。
- 如果状态放在 React 组件中,感觉又白白浪费了 Redux 的优势,回到了 React 原生管理状态的老路上去。
面对这种左右为难的纠结状况,我们可以通过以下三步来确定:
(1)第一步,看这个状态是否会被多个 React 组件共享。
所谓共享,就是多个组件需要读取或者修改这个状态,如果是,那不用多想,应该放在 Store 上,因为 Store 上状态方便被多个组件共用,避免组件之间传递数据;如果不是,继续看第二步。
(2)第二步,看这个组件被 unmount 之后重新被 mount,之前的状态是否需要保留。
举个例子,一个对话框组件。用户在对话框打开的时候输入了一些内容,不做提交直接关闭这个对话框,这时候对话框就被 unmount 了,然后重新打开这个对话框(也就是重新 mount),需求是否要求刚才输入的内容依然显示?如果是,那么应该把状态放在 Store 上,因为 React 组件在 unmount 之后其中的状态也随之消失了,要想在重新 mount 时重获之前的状态,只能把状态放在组件之外,Store 当然是一个好的选择;如果需求不要求重新 mount 时保持 unmount 之前的状态,继续看第三步。
(3)第三步,到这一步,基本上可以确定,这个状态可以放在 React 组件中了。
不过,如果你觉得这个状态很复杂,需要跟踪修改过程,那看你个人喜好,可以选择放在 Store 上;如果你想简单处理,可以心安理得地让这个状态由 React 组件自己管理。
当然,对于简单状态,尽量还是用 React 自己来搞定,只有那些适用场合不限于一个组件的,才有足够理由让 Redux 来管理。
4. Redux 代码组织方式
在应用中引入 Redux 之后,就会引入 action 和 reducer。从方便管理的角度出发,和 React 组件一样,action 和 reducer 都有自己独立的源代码文件,很自然,我们需要决定如何组织这些代码。
更好的方法,是把源代码文件分类放在不同的目录中,根据分类方式,可以分为两种:
- 基于角色的分类(role based)
- 基于功能的分类(feature based)
(1)基于角色的分类
在MVC 应用中,在一个目录下放所有的 controller,在另一个目录下放所有的 view,在第三个目录下放所有的 model,每个目录下的文件都是同样的“角色”,这就是基于角色的分类。对应到使用 React 和 Redux 的应用,做法就是把所有 reducer 放在一个目录(通常就叫做 reducers),把所有 action 放在另一个目录(通常叫 actions)。最后,把所有的纯 React 组件放在另一个目录。
(2)基于功能的分类
基于功能的分类方式,是把一个模块相关的所有源代码放在一个目录。例如,对于博客系统,有 Post(博客文章)和 Comment(注释)两个基本模块,建立两个目录 Post 和 Comment,每个目录下都有各自的 action.js
和 reducer.js
文件,如下所示,每个目录都代表一个模块功能,这就是基于功能的分类方式。
Post -- action.js
|_ reucer.js
|_ view.js
Comment -- action.js
|_ reucer.js
|_ view.js
一般说来,基于功能的分类方式更优。因为每个目录是一个功能的封装,方便共享。具体用哪种方式来组织代码,主要就看是否预期这些模块会被共享,如果会,那采用基于功能的方式就是首选。
5. Redux API
(1)createStore
const store = createStore(
reducer,
initial_state,
applyMiddleware(middleware1, middleware2, ...)
);
createStore 方法是一切的开始,它接收三个入参:
- reducer;
- 初始状态内容;
- 指定中间件;
一般来说,只有 reducer 是必须有的。
(2)Store
Store 就是保存所有状态数据的地方,整个应用只能有一个 Store。使用 createStore
函数来创建 Store:
import { createStore } from 'redux'
const reducer = (state, action) => {
// ...
return new_state
}
const store = createStore(reducer)
使用 createStore
函数接收了一个 reducer 函数,这里 createStore
接收了一个函数,并返回了 store。
(3)State
Store 对象包含了所有数据,如果想获取某个时点的数据,就要对 Store 生成快照,这种时点的数据集合,就叫做 State。
对于 state,有三大原则:
- 唯一数据源:所有的状态值保存在 Redux 的
store
中,整个应用只有一个store
,状态是一个树形对象,每个组件使用状态树上的一部分数据; - 保持状态只读:在任何时候都不能直接修改应用状态。只能通过发送一个
Action
,由这个Action
描述如何去修改应用状态; 只有纯函数能改变数据:这里的函数指的就是
reducer
,它接收两个参数,第一个参数是state
,也就是当前状态,第二个参数是action
。reducer
根据这两个参数的值,创建并返回一个新的对象。import { createStore } from ‘redux’
const reducer = (state, action) => {// ...
return new_state
}
const store = createStore(reducer)
// 获取state
const state = store.getState()
注意:这里说的纯函数是指不依赖于且不改变它作用域之外的变量的函数,也就是说函数返回的结果必须完全由传入的参数决定
(4)Action
State 的变化,会导致 View 的变化。用户只能接触到 View 视图层,所以,我们 State 的变化必然是由于 View 导致的。视图层 View 通过 Action 发出通知,告知 State 它该上场了,需要发生改变了。
那么 Action 是什么呢?它是一个对象,type 属性是其必须的,它标识 Action 的名称,可能会有的属性有三个:error
、payload
和 meta
:
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
}
上面代码中,给 Action 定义了一个名称: “ADD_TODO”,它携带的是字符串 “Learn Redux”。
注意: Action 是改变 State 的唯一方式。
(5)Action Creator
View 会发送多种消息,这就需要定义多种 Action,如果每个都手写,会重复工作。可以定义一个函数来生成 Action,这个函数就称为 Action Creator。
const ADD_TODO = 'ADD_TODO'
function addTodo(type, text) {
return {
type,
payload: text
}
}
const action = addTodo(ADD_TODO, 'Learn Redux')
函数 addTodo
就是一个 Action Creator。
(6)store.dispatch()
Action 是改变 state 的唯一方式,Action 是 View 发出的,那么 View 是怎样发出 Action 呢?这时就需要用到 store.dispatch()
。
store.dispatch()
接收一个 Action 对象作为参数,并将它发送出去。
import { crateStore } from 'redux'
const reducer = (state, action) => {
// ...
return new_state
}
const store = createStore(reducer)
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
})
注意:store.dispatch()
是 View 发出 Action 的唯一方法。
(7)Reducer
Store 收到 Action 之后,会返回一个新的 State,此时 View 会更新。State 的计算过程就称为 Reducer。Reducer 是一个函数,接收两个参数 Action 和当前的 State,并返回一个新的 State:
const reducer = (state, action) => {
// ...
return new_state
}
Reducer 不需要手动调用,store.dispatch()
方法会触发 Reducer 的自动执行,就是在生成 Store 的时候,将 Reducer 传入 createStore
方法,上面的 Store、State 就一直在使用。
(8)store.subscribe()
Store 允许使用 store.subscribe()
方法设置监听函数,State 发生变化时会自动执行这个函数。
import { createStore } from 'redux'
const reducer = (state, action) => {
// ...
return new_state
}
const store = createStore(reducer)
store.subscribe(listener)
store.subscribe
方法返回一个函数,调用这个函数就可以解除监听。
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
unsubscribe()
通过上面的API,总结出Redux的工作流如下:
6. Redux 最佳实践
应用 Redux 的时候,有这些业界已经证明的最佳实践:
(1)Store 上的数据应该范式化。
所谓范式化,就是尽量减少冗余信息,像设计 MySQL 这样的关系型数据库一样设计数据结构。
(2)使用 selector
对于 React 组件,需要的是『反范式化』的数据,当从 Store 上读取数据得到的是范式化的数据时,需要通过计算来得到反范式化的数据。你可能会因此担心出现问题,这种担心不是没有道理,毕竟,如果每次渲染都要重复计算,这种浪费积少成多可能真会产生性能影响,所以,我们需要使用 seletor。业界应用最广的 selector 就是 reslector 。
reselector 的好处,是把反范式化分为两个步骤,第一个步骤是简单映射,第二个步骤是真正的重量级运算,如果第一个步骤发现产生的结果和上一次调用一样,那么第二个步骤也不用计算了,可以直接复用缓存的上次计算结果。
绝大部分实际场景中,总是只有少部分数据会频繁发生变化,所以 reselector 可以避免大量重复计算。
(3)只 connect 关键点的 React 组件
当 Store 上状态发生改变的时候,所有 connect 上这个 Store 的 React 组件会被通知:状态改变了!
然后,这些组件会进行计算。connect 的实现方式包含 shouldComponentUpdate
的实现,可以阻挡住大部分不必要的重新渲染,但是,毕竟处理通知也需要消耗 CPU,所以,尽量让关键的 React 组件 connect 到 store 就行。
一个实际的例子就是,一个列表种可能包含几百个项,让每一个项都去 connect 到 Store 上不是一个明智的设计,最好是只让列表去 connect,然后把数据通过 props 传递给各个项。
7. Flux 架构思想
Redux 的设计在很大程度上受益于 Flux 架构,可以说 Redux 是 Flux 的一种实现形式。Redux 结合了 Flux 架构和函数式编程,Flux 是一种架构思想,专门解决软件的结构问题。它跟 MVC 架构是同一类东西,但是更加简单和清晰。
Flux 并不是一个具体的框架,它是一套由 Facebook 技术团队提出的应用架构,这套架构约束的是应用处理数据的模式。在 Flux 架构中,一个应用将被拆分为以下 4 个部分:
- View(视图层):用户界面。该用户界面可以是以任何形式实现出来的,React 组件是一种形式,Vue、Angular 也完全 OK。Flux 架构与 React 之间并不存在耦合关系。
- Action(动作):也可以理解为视图层发出的“消息”,它会触发应用状态的改变。
- Dispatcher(派发器):它负责对 action 进行分发。
- Store(数据层):它是存储应用状态的“仓库”,此外还会定义修改状态的逻辑。store 的变化最终会映射到 view 层上去。
这 4 个部分之间的协作将通过下图所示的工作流规则来完成配合:
从上图可以看出,Flux 的最大特点,就是数据的”单向流动”,保证了整个执行流程的清晰。
Flux 在运行中,会按如下过程执行:
- 用户访问 View 视图层;
- 触发了用户的 Action;
- Dispatcher 收到 Action 的动作,通知 Store 需要更新数据;
- Store 更新数据后,发出 “change” 事件通知,通知 View 数据发生了变化,需要更新 UI;
- View 在接收到 “change” 事件通知后,会去更新页面。
Flux 最核心的地方在于严格的单向数据流,在单向数据流下,状态的变化是可预测的。如果 store 中的数据发生了变化,那么有且仅有一个原因,那就是由 Dispatcher 派发 Action 来触发的。这样一来,就从根本上避免了混乱的数据关系,使整个流程变得清晰简单。
不过这并不意味着 Flux 是完美的。事实上,Flux 对数据流的约束背后是不可忽视的成本:除了开发者的学习成本会提升外,Flux 架构还意味着项目中代码量的增加。
Flux 架构往往在复杂的项目中才会体现出它的优势和必要性。如果项目中的数据关系并不复杂,其实完全轮不到 Flux 登场,这一点对于 Redux 来说也是一样的。
结合 Flux 架构的特性,来看 Redux 官方给出的这个定义,就更容易理解可预测的深意了:
Redux 是 JavaScript 状态容器,它提供可预测的状态管理。
8. Redux 源码解读
(1)目录结构
下面是Redux源码的目录结构:
其中,utils 是工具方法库;index.js 作为入口文件,用于对功能模块进行收敛和导出。真正工作的是功能模块本身,也就是下面这几个文件:
- applyMiddleware.js: 中间件模块
- bindActionCreators.js: 用于将传入的 actionCreator 与 dispatch 方法相结合,合并一个新的方法
- combineReducers.js: 用于将多个 reducer 合并起来
- compose.js: 用于把接收到的函数从右向左进行组合
- createStore.js: 整个流程的入口,也是 Redux 中最核心的 API
(2)createStore源码
下面主要来看下createStore.js的源码(目前github上的源码是TS版的,这里以JS版为例)。
使用 Redux 的第一步,就是调用 createStore 方法。这个方法做的事情似乎就是创建一个 store 对象出来,像这样:
// 引入 redux
import { createStore } from 'redux'
// 创建 store
const store = createStore(
reducer,
initial_state,
applyMiddleware(middleware1, middleware2, ...)
);
从拿到入参到返回出 store 的过程中,到底都发生了什么呢?我们来看一下 createStore 中主体逻辑的源码:
function createStore(reducer, preloadedState, enhancer) {
// 这里处理的是没有设定初始状态的情况,也就是第一个参数和第二个参数都传 function 的情况
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
// 此时第二个参数会被认为是 enhancer(中间件)
enhancer = preloadedState;
preloadedState = undefined;
}
// 当 enhancer 不为空时,便会将原来的 createStore 作为参数传入到 enhancer 中
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState);
}
// 记录当前的 reducer,因为 replaceReducer 会修改 reducer 的内容
let currentReducer = reducer;
// 记录当前的 state
let currentState = preloadedState;
// 声明 listeners 数组,这个数组用于记录在 subscribe 中订阅的事件
let currentListeners = [];
// nextListeners 是 currentListeners 的快照
let nextListeners = currentListeners;
// 该变量用于记录当前是否正在进行 dispatch
let isDispatching = false
// 该方法用于确认快照是 currentListeners 的副本,而不是 currentListeners 本身
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice();
}
}
// 我们通过调用 getState 来获取当前的状态
function getState() {
return currentState;
}
// subscribe 订阅方法,它将会定义 dispatch 最后执行的 listeners 数组的内容
function subscribe(listener) {
// 校验 listener 的类型
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
// 禁止在 reducer 中调用 subscribe
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
// 该变量用于防止调用多次 unsubscribe 函数
let isSubscribed = true;
// 确保 nextListeners 与 currentListeners 不指向同一个引用
ensureCanMutateNextListeners();
// 注册监听函数
nextListeners.push(listener);
// 返回取消订阅当前 listener 的方法
return function unsubscribe() {
if (!isSubscribed) {
return;
}
isSubscribed = false;
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
// 将当前的 listener 从 nextListeners 数组中删除
nextListeners.splice(index, 1);
};
}
// 定义 dispatch 方法,用于派发 action
function dispatch(action) {
// 校验 action 的数据格式是否合法
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
// 约束 action 中必须有 type 属性作为 action 的唯一标识
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
// 若当前已经位于 dispatch 的流程中,则不允许再度发起 dispatch(禁止套娃)
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 执行 reducer 前,先"上锁",标记当前已经存在 dispatch 执行流程
isDispatching = true
// 调用 reducer,计算新的 state
currentState = currentReducer(currentState, action)
} finally {
// 执行结束后,把"锁"打开,允许再次进行 dispatch
isDispatching = false
}
// 触发订阅
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
return action;
}
// replaceReducer 可以更改当前的 reducer
function replaceReducer(nextReducer) {
currentReducer = nextReducer;
dispatch({ type: ActionTypes.REPLACE });
return store;
}
// 初始化 state,当派发一个 type 为 ActionTypes.INIT 的 action,每个 reducer 都会返回
// 它的初始值
dispatch({ type: ActionTypes.INIT });
// observable 方法可以忽略,它在 redux 内部使用,开发者一般不会直接接触
function observable() {
// observable 方法的实现
}
// 将定义的方法包裹在 store 对象里返回
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
通过上面源码会发现,createStore 从外面看只是一个简单的创建动作,但在内部却涵盖了所有 Redux 主流程中核心方法的定义。
createStore 内部逻辑如下:
还没有评论,来说两句吧...