一、介绍
这篇文章来总结一下 Redux, 便于以后的知识回顾. 有了之前 Flux 知识学习, 应该对单向数据流的状态管理有比较清晰的认识了, 同样 Redux 的出现也是受到了 Flux 的启发, 这也是我们最好要先去了解一下 Flux 的原因. 同时 Redux 利用纯函数简单明了的特点, 在 Flux 架构的基础上进行了优化和功能增强 (支持中间件、异步等), 降低了复杂度, 同时还提供强大的工具库支持 (React-Redux、Redux-Toolkit、Redux-Thunk). 下面一起来看下其具体的实现逻辑. 详细内容可以直接在官网学习.
Redux 的宗旨还是通过集中式的、单向的方式对整个应用中使用的状态进行管理,确保了状态更新的可预测性, 让状态的变化可追踪. Flux 中有 Action、Dispatcher、Store、View 四个概念, 类似的 Redux 中也有 Actions、State、Reducer、Store 等概念, 注意这里并没有 dispatcher 的概念, 因为它利用纯函数替代了事件处理器, 后面会具体说到.
首先还是先入为主的看一下 Redux 的事件流更新动画, 有个大致的印象, 下面是官网的一张图
其实很清晰明了, Redux 在这个环节一共做了下面几件事
1、 接收响应视图 (UI) 的某个事件, 如点击
2、 Dispatch 一个 Action 给到 Store
3、 Store 结合当前 State 和 Action 运行 Reducer 生成新的 State
4、 Store 将新的 State 广播到 UI 层, 让所有订阅过 State 的组件都进行数据更新和视图渲染
下面还是一个个概念来介绍
1、 Actions
可以说几乎和 Flux 的 Actions 含义一样, 不过除了包括 action 和 action creator, Redux 还引入了 异步 action 的概念, 下面逐一说明下:
action 是一个对象, 用来描述发生的具体事件, 由事件类型和所带的 payload, 在用户事件触发后, action 会被 dispatch, 其 payload 是完全透明的传递, 所以使用者可以自定义参数.
代码语言:javascript复制const todoAdded = {
type: 'todos/todoAdded', // action类型, 用于确定是否执行当前action
payload: 'Buy milk' // 所需要传递的自定义参数
};
dispatch(todoAdded);
action creator 是一个纯函数, 是从 Flux 架构中出现的, 他是一种统一集中式管理 action 的思路, 为什么要使用 action creator ? 这篇文章总结的很好, 大家可以看一看, 大致归纳如下:
● 对于同一个 action type 来说, 也许其内部的大多数逻辑都相似, action creator 正好可以收拢这部分逻辑, 避免在多个组件中进行重复创建, 同时也提高了复用性
● 通过函数式的定义, 可以清晰的知道当前 action 需要传递的参数和后续会影响的状态
● 如果在返回 action 对象之前, 需要处理很多的其他逻辑, 包括同步、异步等逻辑, 那封装成函数是再好不过的. 使用者根本无需关心内部的执行逻辑, 只需当作黑盒调用即可
● 对于使用 action creator 的组件来说, 组件的测试性得以提升, 只要保证 creator 的测试正确, 使用到的组件可以直接对其进行函数级的 mock.
代码语言:javascript复制const addTodo = (text) => {
...
return {
type : "ADD_TODO",
text
}
};
dispatch(addTodo('test'));
异步 action 的出现主要是解决普通的 action 无法执行一些异步逻辑的问题, Flux 只支持同步的一些方法调用, 而在 Redux 中提供了相应的解决方案, 那就是通过引入中间件 middleware 的模式添加异步 action, 如 redux-thunk. 具体在 middleware 介绍时再详细说明.
2、 State
集中管理着 Redux 中的所有状态, 可以使用 store.getState 来获取当前的状态. 但是不能够直接去修改他, 必须通过 reducer 去修改他, 不过 Redux 并没有对 State 的修改做任何保护措施, 所以在我们代码中要严格避免直接修改 State 的这种情况.
3、 Reducer
与 Flux 中的 reduce 类似, 都是一个函数, 主要用来获取新的状态. 他接收当前的 state 和 触发的 action, 然后计算输出一个新的 state, 定义如 (state, action) => newState. 在 Redux 中, reducer 必须是一个纯函数, 不能有任何的副作用, 当然也不支持异步逻辑, 大概长下面这样.
代码语言:javascript复制const reducer = (state = initialState, action) => {
// reducer 通常会根据 action type 字段来决定发生什么
switch (action.type) {
// 根据不同 type 的 action 在这里做一些事情
default:
// 如果这个 reducer 不关心这个 action type,会返回原本的state
return state
}
};
一般我们会按类型去区分不同的 reducer, 以便于分类管理, Redux 也提供了 combineReducers 函数, 帮我们组合 reducer, 并统一输出
代码语言:javascript复制const rootReducer = combineReducers({
// 定义一个名为`todos`的顶级状态字段,由`todosReducer`处理
todos: todosReducer,
filters: filtersReducer
});
4、 Store
在上面, 我们虽然已经有了 Actions、State、Reducer 的定义, 但他们都是分散了, 现在是时候把他们整合起来, 组成完整的状态管理功能. Store 就是为了达到此目的而生的, 通过 createStore 方法生成 store 实例, 然后就可以在各个组件中使用实例的相应方法.
代码语言:javascript复制const store = createStore(rootReducer);
store.getState(); // 获取当前 state;
store.dispatch(action); // 触发状态更新;
store.subscribe(listener); // 注册监听回调
二、源码分析
Redux 的实现整体采用函数式编程的方式, 所以读起来要比 Flux 的源码轻松很多, 逻辑走向比较清晰, 可以学习学习其编程思维, 他导出的函数有如下几个:
代码语言:javascript复制export {
createStore, // 整体的Store创建函数, 不过现在推荐使用redux-toolkit
legacy_createStore, // 跟createStore一样
combineReducers, // 不同reducer的组合函数
bindActionCreators, // 直接绑定action creator, 使调用更简单
applyMiddleware, // 中间件引入
compose, // 生成链式调用的middleware
__DO_NOT_USE__ActionTypes, // 内置的action type
}
1、__DO_NOT_USE__ActionTypes
首先来说下 Redux 内置的几种 action 类型, 因为在读其他源码时会用到, Redux 内置了三种类型的 action, 使用者可以直接在自己定义的 reducer 中使用
代码语言:javascript复制const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`, // 初始化, 当调用createStore时会触发一次
REPLACE: `@@redux/REPLACE${randomString()}`, // 替换, 当使用者动态更新reducer的时候会调用一次
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`,
}
2、createStore
代码中引用的 store 就是通过该函数创建了, 是 Redux 中的核心函数, 函数中对主要的功能函数进行的定义, 并对一些属性进行初始化, 他定义如下
代码语言:javascript复制/**
* @param {Function} reducer, 形如(state,action) => newState的函数
* @param {any} preloadedState, 可选参数, 用于初始化的state
* @param {Function} enhancer, 可选参数, 用于功能增强, 扩展第三方功能, 如miidleware等
* @returns {Store} 返回store对象用于状态处理
*/
createStore(reducer, preloadedState, enhancer) {
...
let currentReducer = reducer // 初始化reducer
let currentState = preloadedState // 初始化当前状态
let currentListeners = [] // 初始化订阅列表
let nextListeners = currentListeners // 对订阅列表做备份
let isDispatching = false // 初始化dispatch执行状态
...
dispatch({ type: ActionTypes.INIT }) // 触发一下初始化类型的action, 使用者可以在reducer中响应该事件
return {
dispatch, // 用于触发action
subscribe, // 通过listener函数订阅state的变化
getState, // 直接返回当前state
replaceReducer, // 替换当前正在使用的reducer函数
[$$observable]: observable,
}
}
createStore 在用户没有使用 enhaner 的情况下, 其采用了闭包的方式来管理 reducer、state、listeners 和 dispatch 的状态. 我们来看看几个关键函数的定义, 以及 state 的整体控制思路.
dispatch
首先来看下核心函数 dispatch, 是唯一接收 action 触发事件, 改变 state 状态的方式. dispatch 函数只能接收纯对象作为参数, 如果要触 action 是 Promise、Observable、thunk 或者其他类型, 需要引入对应的中间件来进行处理, 函数的执行流程大致如下
代码语言:javascript复制function dispatch(action) {
...
try {
isDispatching = true // 上锁, 表示当前正在执行reducer
currentState = currentReducer(currentState, action) // 执行reducer, 更新当前状态
} finally {
isDispatching = false // 解锁, 表示已经执行完reducer
}
// 更新当前的订阅数组, 并轮训监听函数, 告诉订阅者当前状态已更新
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i ) {
const listener = listeners[i]
listener()
}
// 返回输入的action对象
return action
}
subscribe
用于监听状态的更新, 他接收一个 listener 函数作为入参. 如上面 disptach 流程所示, subscribe 将在任何一个 action 被执行完后调用, 虽然 Redux 没有传递任何参数给到 subscribe 的 listener, 但是在监听器中可以调用 store.getState 来获取所有状态.
代码语言:javascript复制function subscribe(listener) {
...
let isSubscribed = true // 采用闭包的方式缓存已订阅状态
ensureCanMutateNextListeners() // 拷贝当前订阅列表
nextListeners.push(listener) // 向拷贝到列表中添加新的监听器
// 返回解除订阅函数
return function unsubscribe() {
...
isSubscribed = false // 恢复未订阅状态
ensureCanMutateNextListeners() // 拷贝当前订阅列表
const index = nextListeners.indexOf(listener) // 删除监听器
nextListeners.splice(index, 1)
currentListeners = null // 清空当前订阅列表
}
}
说明:
● ensureCanMutateNextListeners 函数是用于生成当前订阅列表 (currentListeners) 的副本 (nextListeners), 所有的订阅列表的更新删除操作都在副本进行, 然后每次触发 dispatch 的时候都会用副本去更新当前的订阅列表.
● 正因为第一点, 所以当你调用 subscribe 或者 unsubscribe 时, 不会对当前正在执行的 diapatch 轮训监听器产生任何影响, 而是在下一个 dispatch 调用时使用新的订阅列表
● 在 listener 中你也可以调用 dispatch 来更新当前的 state, 从而出现前套 dispatch 执行的情况, 正式因为存在这种情况的可能, 所有 listener 中调用 store.getState 并不是总能够拿到最新的状态.
replaceReducer
该函数允许你热更新 reducer, 函数逻辑很简单, 直接替换当前的 reducer 函数, 并触发替换 action.
代码语言:javascript复制function replaceReducer(nextReducer) {
currentReducer = nextReducer // 直接干脆的替换掉
dispatch({ type: ActionTypes.REPLACE }) // 还记得上面说的__DO_NOT_USE__ActionTypes吗, 这里会触发他的REPLACE事件, 使用者可以在reducer里响应该事件
}
observable
这个函数通常情况下不会使用, 是为了配置具有 observable/reactive 特性的三方库来使用的, 其返回一个对象, 对象中包括订阅方法, 该类似 subscribe 方法
代码语言:javascript复制function observable() {
const outerSubscribe = subscribe // 对subscribe函数的引用
return {
// observer是个对象, 必须包含next函数
subscribe(observer) {
...
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState() // 先更获取一下当前状态
const unsubscribe = outerSubscribe(observeState) // 注册监听器
return { unsubscribe } // 返回解除订阅函数
},
...
}
}
3、combineReducers
上面说的 createStore 仅仅支持传入一个 reducer 函数, 但是在实际中随着业务复杂度增加, 状态会变的越来越多, 虽然可以通过一个 reducer 都进行管理, 但会使得 reducer 变的过于冗长、逻辑堆叠、难于维护. 为了更方便的管理各种不同类型的状态, 我们常常会对状态进行分组, 然后再通过 combineReducers 进行组合, 传入 createStore 中进行初始化
借助于上面的思路, 很容易想到 combineReducers 的实现逻辑, 即接收不同的 reducers, 返回一个总体控制的 combination 函数, 该函数中会轮训 reducers 的所有属性, 分别触发他们的 reducer 函数, 下面来看一下他的具体源码实现
代码语言:javascript复制export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers) // 获取对象key值列表
const finalReducers = {} // 缓存符合要求的reducer
for (let i = 0; i < reducerKeys.length; i ) {
const key = reducerKeys[i]
...
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key] // reducer只能是函数
}
}
const finalReducerKeys = Object.keys(finalReducers)
...
let shapeAssertionError
try {
assertReducerShape(finalReducers) // reducer拦截内置的action type
} catch (e) {
shapeAssertionError = e
}
// 返回reducers的组合函数, 用于传递给createStore
return function combination(state = {}, action) {
...
}
}
我们再来看下 combination 的实现
代码语言:javascript复制function combination(state = {}, action) {
...
let hasChanged = false // 状态变化标识位
const nextState = {} // 已更新的状态
// 循环执行 reducers 中的 reducer 函数
for (let i = 0; i < finalReducerKeys.length; i ) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key] // 找到待执行的reducer
const previousStateForKey = state[key] // 找到对应的state
const nextStateForKey = reducer(previousStateForKey, action)
...
nextState[key] = nextStateForKey // 更新对应的state值
hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 判断状态是否改变, 只要有一个reducer关联的状态改变就算有变化
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length // 如果有状态的删减, 也算变化
return hasChanged ? nextState : state // 返回新的状态
}
注意
● 使用 combineReducers 时, 所有待组合的 reducer 都不允许去拦截 __DO_NOT_USE__ActionTypes 中定义的 type, 所有这些 type 都要返回当前的状态, 否则会抛出异常.
● 可以看出 state 集合的管理 与 reducer 集合的管理要相互呼应, 对象的key值要一直, 不然 combineReducers 中无法找到相应的状态, 类似如下
代码语言:javascript复制const state = {
counter: {},
todos: {}
}
const reducers = {
counter: (state, action) => newState,
todos: (state, action) => newState
}
4、bindActionCreators
我们通常都是 dispatch 一个 action 去更新状态, 例如 store.dispatch(action), 其中 action 是一个包含 type 类型的对象, 但如之前所说, 我们往往会使用 action creator 来优化对跨组件 action 的管理, 而 action creator 是一个带有入参为 payload 的函数, 通常调用方式如下:
代码语言:javascript复制const addTodo = (text) => ({ type: 'ADD_TODO', text });
dispatch(addTodo('Use Redux'));
而 bindActionCreator 提供了一种更为优雅、兼容性更好的调用方式, 他允许你传入的actionCreators是函数或者对象, 返回一个可以直接 dispatch 的函数或者对象
代码语言:javascript复制// 对于每个actionCreator方法,返回一个自动执行dispatch的方法, 简化调用
function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(this, arguments))
}
}
// 根据不同的actionCreators输入类型, 返回对应的bindActionCreator函数
export default function bindActionCreators(actionCreators, dispatch) {
// 如果是函数, 直接返回bindActionCreator的封装函数
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
...
const boundActionCreators = {}
// 如果是对象, 则遍历对象, 返回包含bindActionCreator函数执行结果的对象
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}
通过上面的使用, 我们的调用方式变为
代码语言:javascript复制const addTodoCreator = bindActionCreators(addTodo, dispatch)
addTodoCreator('Use Redux') // 这里会自动执行dispatch
其实也没什么奥秘, 就是多封装了一层, 包装了一个语法糖, 免去了每次都要手动dispatch的过程
5、 applyMiddleware
上面说 createStore 可以传入第三个参数 enhancer, 用来扩展一些包含自定义功能的中间件, 进行功能增强. 中间件可以进行各种异步操作、日志记录等等, 比如说用的最多的中间件应该就是 redux-thunk, 这是与 Flux 的重要区别之一. 让我们来看看其实现:
在 createStore 的实现中有一段这样的代码, 当传入符合要求的 enhancer 时, creatStore 会直接返回 enhancer 的函数执行结果, 而这个 enhancer 就是 applyMiddleware 的返回值.
代码语言:javascript复制export function createStore(reducer, preloadedState, enhancer) {
...
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error(...)
}
// 首先判断enhancer是否存在, 存在的话则直接交给enhancer处理, 不走后续逻辑
return enhancer(createStore)(reducer, preloadedState)
}
...
}
applyMiddleware 接收中间件列表作为参数, 每个 middleware 接受 Store 的 dispatch 和 getState 函数作为命名参数,并返回一个函数, applyMiddleware 会将所有中间件返回的函数做链式调用, middleware 函数的定义如下:
代码语言:javascript复制function middleware({ getState, dispatch }) {
return next => action => {
// 调用 middleware 链中下一个 middleware 的 dispatch。
const returnValue = next(action)
// 一般会是 action 本身,除非后面的 middleware 修改了它。
return returnValue
}
}
applyMiddleware 的具体实现代码如下
代码语言:javascript复制export default function applyMiddleware(...middlewares) {
// 这里的args其实就是createStore的三个入参数, 在这里拦截createStore, 对其进行了逻辑增强
return (createStore) => (...args) => {
const store = createStore(...args) // 先生成默认的store
let dispatch = () => { ... }
// 使用默认的store构造middleware函数的入参
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
}
// 生成链式调用列表, 即形如next => action => {}的函数列表
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
// 构造新的dispatch, 链式调用middleware, chain列表按从右到左组合
dispatch = compose(...chain)(store.dispatch)
// 返回新的store,并将新的dispatch方法覆盖原有的dispatch方法
return {
...store,
dispatch,
}
}
}
可以看到 applyMiddleware 的核心是改写了 dispatch 方法, 使用链式调用(compose)方式逐一执行中间件函数, 采用了类似 koa 中的洋葱模型来运行代码逻辑, 由外到里触发, 再由里到外返回.
以上的这些就差不多是 Redux 源码的基本部分, 可以看出要比 Flux 来的简洁, 运用了比较多的函数式编程思维, 使得逻辑清晰简单. 当然, 现在官方已经开始推荐使用 redux-toolkit, 他是基于 Redux 的最佳实践, 简化了 Redux 的编写调用, 他采用了函数式、柯里化等编程思维, 具体差异可以参考官方说明.
三、总结
现在我们可以来对比一下 Flux 和 Redux 之间的差异
实现思路 | 实现方式 | 定位 | 使用范围 | Store | Dispatcher | State | 状态更新 | 异步逻辑 | |
---|---|---|---|---|---|---|---|---|---|
Flux | 单向数据流 | 响应式编程 | 一种架构方案 | react组件 | 可以有多个Store | 有唯一的Dispatcher | State是可变的, 未做保护 | 在Store中执行状态更新 | 不支持异步操作 |
Redux | 单向数据流 | 函数式编程 | Flux架构的具体实现 | 无技术栈限制 | 只有一个Store | 没有Dispatcher的概念 | State不可以直接改变 | 由reducer执行状态更新 | 可以使用middleware来处理异步 |