前言
最近看项目中使用了 Redux, 便尝试了解一波 Redux 的设计思想与工作原理,通过写文章的形式沉淀下来。 结尾有彩蛋~
设计思想
在开始了解之前,我们需要先了解 Redux 解决了什么问题?
Redux 解决了什么问题
在没有 Redux 之前, 如果组件之间存在大量通信,甚至有些通信跨越多个组件,或者多个组件之间共享一套数据,简单的父子组件间传值不能满足我们的需求,自然而然地,我们需要有一个地方存取和操作这些公共状态。而 redux 就为我们提供了一种管理公共状态的方案,便于管理比较复杂的通信场景。
Redux 的设计理念
Redux 的设计采用了 Facebook 提出的 Flux 数据处理理念
在 Flux 中通过建立一个公共集中数据仓库 Store 进行管理,整体分成四个部分即: View (视图层)、Action (动作)、Dispatcher (派发器)、Store (数据层)
如下图所示,当我们想要修改仓库的数据时,需要从 View 中触发 Action,由 Dispatcher 派发到 Store 修改数据,从而驱动视图更新
这种设计的好处在于其数据流向是单一的,数据的修改一定是会经过 Action、Dispatcher 等动作才能实现,方便预测、维护状态的流向。
当我们了解了 Flux 的设计理念后,便可以照葫芦画瓢了。
如下图所示,在 Redux 中同样需要维护一个公共数据仓库 Store, 而数据流向只能通过 View 触发 Action、 Reducer更新派发, Store 改变从而驱动视图更新
工作原理
当我们了解了 Redux 的设计理念后,趁热打铁炫一波 Redux 的工作原理,我们知道使用 Redux 进行状态管理的第一步就是需要先创建数据仓库 Store, 也就会需要调用 createStore 方法。那我们就先拿 createStore 开炫。
createStore
从 Redux 源码中我们不难看出,createStore 接收 reducer 、初始化state 、中间件三个参数,当执行 createStore 时会记录当前的 state 状态,并返回 store 对象,包含 dispatch、subscribe、getState 等属性。
其中
- dispatch: 用来触发 Action
- subscribe: 当 store 值的改变将触发 subscribe 的回调
- getState: 用来获取当前的 state 状态。
getState 比较简单,直接返回当前的 state 状态,接下来我们将着重了解 dispatch 与 subscribe 的实现。
js
function createStore(reducer, preloadedState, enhancer) {
let currentReducer = reducer // 记录当前的 reducer
let currentState = preloadedState // 记录当前的 state
let isDispatching = false // 是否正在进行 dispatch
function getState() {
return currentState // 通过 getState 获取当前的 state
}
// 触发 action
function dispatch(action: A) {}
function subscribe(listener: () => void) {}
// 初始化 state
dispatch({ type: ActionTypes.INIT } as A)
// 返回一个 sttore
const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState
}
return store
}
dispatch
在 Redux 中, 修改数据的唯一方式就是通过 dispatch,而 dispatch 接受一个 action 对象作为参数,执行 dispatch 方法,将生成新的 state,并触发监听事件。
js
function dispatch(action) {
// 如果已经在触发中,则不允许再次出发 dispatch (禁止套娃)
// 例如:在 reducer 中触发 dispatch
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 上锁
isDispatching = true
// 调用 reducer,获取新的 state
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 触发订阅事件
const listeners = (currentListeners = nextListeners)
listeners.forEach(listener => {
listener()
})
return action
}
subscribe
在 Redux 中, 可以通过 subscribe 方法来订阅 store 的变化, 一旦 store 发生了变化, 就会执行订阅的回调函数
可以看到 subscribe 方法接收一个回调函数作为参数, 执行 subscribe 方法将会返回一个 unsubscribe 函数, 用于取消订阅
js
function subscribe(listener: () => void) {
if (isDispatching) {
throw new Error()
}
let isSubscribed = true // 防止调用多次 unsubscribe
ensureCanMutateNextListeners() // 确保 nextListeners 是 currentListeners 的快照,而不是同一个引用
const listenerId = listenerIdCounter++
nextListeners.set(listenerId, listener) //nextListeners 添加订阅事件
// 取消订阅事件
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error()
}
isSubscribed = false
ensureCanMutateNextListeners(); // 如果某个订阅事件执行了 unsubscribe, nextListeners 创建了新的内存地址,而原先的listeners 依然保持不变 (dispatch 方法中的312 行)
nextListeners.delete(listenerId)
currentListeners = null
}
}
ensureCanMutateNextListeners 与 currentListeners 的作用
承接上文,在 subscribe 中不管是注册监听还是取消监听都会调用 ensureCanMutateNextListeners 的方法,那么这个方法是做什么的呢?
从函数的逻辑上不难得出答案:
ensureCanMutateNextListeners 确保 nextListeners 是 currentListeners 的快照,而不是同一个引用
js
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) { // currentListeners 用来确保循环的稳定性
nextListeners = new Map()
currentListeners.forEach((listener, key) => {
nextListeners.set(key, listener)
})
}
}
在 dispatch 或者 subscribe 函数中,都是通过 nextListeners 触发监听,那为何还需要使用 currentListeners?
这里就不卖关子了,这里的 currentListeners 用于确保在 dispatch 中 listener 的数量不会发生变化, 确保当前循环的稳定性。
请看下面的例子👇
js
const a = store.subscribe(() => {
/* a */
});
const b = store.subscribe(() => a());
const c = store.subscribe(() => {
/*/ c */
});
store.dispatch(action);
上面的代码在 Redux 中是被允许的, 通过 subscribe 注册监听函数 a、b、c,此时 nextListeners 指向 [a, b, c]
当执行 dispatch 时, listener、currentListeners、nextListeners 将指向地址 [a, b, c];
js
// dispatch 触发监听事件的逻辑
// 触发订阅事件
const listeners = (currentListeners = nextListeners)
listeners.forEach(listener => { listener() })
当执行到 b 监听函数时,将解绑 a 函数的监听事件,如果直接修改 nextListeners, 在循环中操作数组是非常危险的事情, 因此借助 ensureCanMutateNextListeners、currentListeners 为 nextListeners 开辟了新的内存地址,对 nextListeners 的操作将不影响 listener。
实现一个 mini react-redux
上文我们说到,一个组件如果想从 store 存取公用状态,需要进行四步操作:
- import引入store
- getState获取状态
- dispatch修改状态
- subscribe订阅更新
代码相对冗余,我们想要合并一些重复的操作,而 react-redux 就提供了一种合并操作的方案:react-redux提供 Provider
和 connect
两个API, Provider 将 store 放进 this.context 里,省去了 import 这一步, connect将 getState、dispatch 合并进了this.props,并自动订阅更新,简化了另外三步,下面我们来看一下如何实现这两个API:
Provider
Provider
组件比较简单,接收 store 并放进全局的 context
对象,使 store
可用于任何需要访问 Redux store 的嵌套组件
js
import React, { createContext } from 'react';
let StoreContext;
const Provider = (props) => {
StoreContext = createContext(props.store);
return <StoreContext.Provider value={props.store}>{ props.children }</StoreContext.Provider>
}
connect
下面我们来思考一下如何实现 connect
,我们先回顾一下connect的使用方法
js
connect(mapStateToProps, mapDispatchToProps)(App)
connect 接收 mapStateToProps、mapDispatchToProps 两个函数,然后返回一个高阶函数, 最终将 mapStateToProps、mapDispatchToProps 函数的返回值通过 props 形式传递给 App 组件
我们直接放出connect的实现代码,并不复杂:
js
import React, { createContext, useContext, useEffect } from 'react';
export function connect(mapStateToProps, mapDispatchToProps) {
return function (Component) {
const connectComponent: React.FC = (props) => {
const store = useContext(StoreContext);
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const handleStoreChange = () => {
// 强制刷新
forceUpdate();
}
useEffect(() => {
store.subscribe(handleStoreChange)
}, [])
return (
<Component
// 传入该组件的props,需要由connect这个高阶组件原样传回原组件
{ ...(props) }
// 根据 mapStateToProps 把 state 挂到 this.props 上
{ ...(mapStateToProps(store.getState())) }
// 根据mapDispatchToProps把dispatch(action)挂到this.props上
{ ...(mapDispatchToProps(store.dispatch)) }
/>
)
}
return connectComponent;
}
}
可以看出 connect 通过 useContext 实现和 store 的链接,将 state 作为第一个参数传给 mapStateToProps、将 dispatch 作为第一个参数传递给 mapDispatchToProps,最终将结果通过 props 形式传递给子组件。
其实 connect 这种设计,是装饰器模式的实现,所谓装饰器模式,简单地说就是对类的一个包装,动态地拓展类的功能。这里的 connect 以及 React 中的高阶组件(HoC)都是这一模式的实现。
对类的装饰常用于拓展类的功能,对类中函数的装饰常用于 AOP 切面
js
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。 如果一定要装饰函数,可以使用高阶函数
mini react-redux
通过上文,我们了解了 Provider 与 connect 的实现,我们可以写个 mini react-redux 来测试一下
1 创建如下目录结构
2 实现 createStore 函数 创建一个 createStore.ts 文件,createStore 最终将返回 store 对象,包含 getState、dispatch、subscribe
js
export const createStore = (reducer: Function) => {
let currentState: undefined = undefined;
const obervers: Array<Function> = [];
function getState() {
return currentState;
}
function dispatch(action: { type: string}) {
currentState = reducer(currentState, action);
obervers.forEach(fn => fn());
}
function subscribe(fn: Function) {
obervers.push(fn);
}
dispatch({ type: '@@REDUX/INIT' }); // 初始化 state
return {
getState,
dispatch,
subscribe
}
}
3 实现 reducer
createStore 函数接收一个 reducer 方法,reducer 常用来分发 action, 并返回新的 state
js
// reducer.ts
const initialState = {
count: 0
}
export function reducer(state = initialState, action: { type: string}) {
switch (action.type) {
case 'add':
return {
...state,
count: state.count + 1
}
case 'reduce':
return {
...state,
count: state.count - 1
}
default:
return initialState;
}
}
4 实现 Provider 与 connect
js
/* eslint-disable react-hooks/rules-of-hooks */
//@ts-nocheck
import React, { createContext, useContext, useEffect } from 'react';
let StoreContext;
const Provider = (props) => {
StoreContext = createContext(props.store);
return <StoreContext.Provider value={props.store}>{ props.children }</StoreContext.Provider>
}
export default Provider;
export function connect(mapStateToProps, mapDispatchToProps) {
return function (Component) {
const connectComponent: React.FC = (props) => {
const store = useContext(StoreContext);
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
const handleStoreChange = () => {
// 强制刷新
forceUpdate();
}
useEffect(() => {
store.subscribe(handleStoreChange)
}, [])
return (
<Component
// 传入该组件的props,需要由connect这个高阶组件原样传回原组件
{ ...(props) }
// 根据 mapStateToProps 把 state 挂到 this.props 上
{ ...(mapStateToProps(store.getState())) }
// 根据mapDispatchToProps把dispatch(action)挂到this.props上
{ ...(mapDispatchToProps(store.dispatch)) }
/>
)
}
return connectComponent;
}
}
5 修改 main.tsx
js
// main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import Provider from './react-redux/index.tsx';
import { createStore } from './react-redux/createStore.ts';
import { reducer } from './react-redux/reducer.ts';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={createStore(reducer)}>
<App />
</Provider>
</React.StrictMode>,
)
6 修改 App.tsx
js
// App.tsx
import { useState } from 'react';
import { connect } from './react-redux';
const addAction = {
type: 'add'
}
const mapStateToProps = (state: { count: number }) => {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
addCount: () => {
dispatch(addAction)
}
}
}
interface Props {
count: number;
addCount: () => void;
}
function App(props: Props): JSX.Element {
const { count, addCount } = props;
return (
<div className="App">
{ count }
<button onClick={ () => addCount() }>增加</button>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
运行项目,点击增加按钮,如能正确计数,我们整个redux、react-redux的流程就走通了。
中间件
在大部分场景下, 我们需要自定义 dispatch 的行为, 在 Redux 中, 我们可以使用 中间件来拓展 dispatch 的功能
类似于 Express 或者 Koa, 在这些框架中,我们可以使用中间件来拓展 请求 和 响应 之间的功能
而 Redux 中间件的作用是在 action 发出之后, 到达 reducer 之前, 执行一系列的任务
在 Redux 中我们可以通过 applyMiddleware 生成一个强化器 enhancer 作为 createStore 的第二个参数传递。
js
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'
const middlewareEnhancer = applyMiddleware(print1, print2, print3)
// Pass enhancer as the second arg, since there's no preloadedState
const store = createStore(rootReducer, middlewareEnhancer)
export default store
正如它们的名称所示,每个中间件在调度操作时都会打印一个数字
js
import store from './store'
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: '1'
// log: '2'
// log: '3'
在这个例子中,当触发 dispatch 的内部执行顺序如下:
- The
print1
middleware (which we see asstore.dispatch
) - The
print2
middleware - The
print3
middleware - The original
store.dispatch
- The root reducer inside
store
实现一个中间件
从上文得知, 我们了解了如何使用中间件, 接下来我们将实现一个中间件。
在 Redux 中,中间件其实是由三个嵌套函数组成
js
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// Do anything here: pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
// Can also use storeAPI.getState() here
return next(action)
}
}
}
最外层函数 exampleMiddleware 将会被 applyMiddleware 调用,并传入 storeAPI 对象( 形如 {dispatch, getState} ),
中间层函数 wrapDispatch 接收一个 next 参数,next 实际上就是中间管道的下一个中间件函数,如果是最后一个 next,那么他的下一个中间件函数就是 dispatch
最内层函数 handleAction 接收一个 Action 对象
此时,我们知道了如何编写一个中间件,接下来我们将实现一个 logger 中间件
js
const loggerMiddleware = storeAPI => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}
写完 logger 中间件后,我们尝试在 Redux 中使用,如下
js
import { createStore, applyMiddleware } from "redux";
const initialState = {
count: 0
}
function reducer(state = initialState, action: { type: string}) {
switch (action.type) {
case 'add':
return {
...state,
count: state.count + 1
}
case 'reduce':
return {
...state,
count: state.count - 1
}
default:
return initialState;
}
}
const logger1 = storeAPI => next => action => {
console.log('logger1 开始');
const result = next(action)
console.log('logger1 结束');
return result
}
const logger2 = storeAPI => next => action => {
console.log('logger2 开始');
const result = next(action)
console.log('logger2 结束');
return result
}
const logger3 = storeAPI => next => action => {
console.log('logger3 开始');
const result = next(action)
console.log('logger3 结束');
return result
}
const middlewares = applyMiddleware(logger1, logger2, logger3);
const store = createStore(reducer, middlewares);
store.dispatch({ type: 'add' });
最终将打印
从打印的记过来看,如果之前有接触过 Express 或者 Koa 的同学,应该可以很快发现,这个是一个洋葱模型
applyMiddleware 的实现原理
从上可知,Redux 提供了一个 applyMiddleware 方法用于将中间件拓展到 dispatch 上
具体是如何拓展的呢?
从源码我们不难看出,最终是通过 compose 也就是利用 reduce 方法,将下一个的中间件函数作为参数,在上一个中间件的函数体内执行。
注意这里传入 compose 内的每一个函数都是一个双层嵌套函数。
js
// applyMiddleware 源码
export default function applyMiddleware(
...middlewares
) {
// 返回一个接收 createStore为入参的函数
return createStore => (reducer, preloadedState) => {
// 创建 store
const store = createStore(reducer, preloadedState)
let dispatch: Dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
/**
* middleware 形如:
* ({dispatch, getState}) => next => action => { ... return next(action) }
*/
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
js
function compose(...funcs) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return (arg:) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
)
}
模拟洋葱模型
承接上文,我们大概了解了什么是洋葱模型,接下来我们将模拟一波洋葱模型的实现。
js
const func1 = (fn) => () => {
console.log('进入func1', fn);
const res = fn();
console.log('离开func1');
return res;
}
const func2 = (fn) => () => {
console.log('进入func2', fn);
const res = fn();
console.log('离开func2');
return res;
}
const func3 = (fn) => () => {
console.log('进入func3', fn);
const res = fn();
console.log('离开func3');
return res;
}
const composeB = (...fns) => {
if (fns.length === 0) return arg => arg
if (fns.length === 1) return fns[0]
return fns.reduce((res, cur) => {
return (...args) => res(cur(...args))
});
}
// (...args) => func1((...args) => func2((...args) => func3(...args))) // 从左到右入栈
const dispatch = () => void 0;
const c = composeB(func1, func2, func3)(dispatch);
c();
总结
书写至此,突然有一丝煽情,之前在下对于 redux 充满了未知与恐惧,刚开始特别害怕学不懂,便迟迟不敢尝试,不断地摆烂,破罐子破摔。可当静下心来,接纳自己的愚蠢,慢慢地一遍又一遍地读每一行代码与一些很 nice 的文章时,似乎恐惧是自己事前设定好的。而生活里也并不是只有成功 or 失败,失败也不应该判定一个人的价值,所以不需要惧怕失败。
最后想多说的一句就是: 就算失败了,我依然觉得你是最棒的!