redux 原理探秘:从 redux 到 react-redux 和 redux-toolkit

上一篇文章中介绍了 redux 和核心概念、核心原则和相关技术栈架构,下面这篇文章我们开始深入研究 redux 和相关工具 react-redux、redux-toolkit 的实现原理

redux 实现原理

在介绍 redux 核心方法之前,先整体回顾一下 redux 的核心方法

  • createStore 是创建 Store 的入口,也是实现发布-订阅模式的核心
  • 通过 getState 方法能够获取到当前的状态
  • 通过 subscribe 方法注册订阅回调函数,通过 dispatch 执行 Reducer 更新状态并触发订阅者函数执行,实现发布-订阅模式
  • 通过 Observable 方法将 Store 转化为一个可观察对象

createStore 方法

createStore 方法核心实现步骤如下

  • 定义了 getState、subscribe、dispatch、observable 方法,用于实现获取 Store 的状态和发布-订阅模式
  • 通过 dispatch 方法初始化所有状态
  • 最后返回包含 getState、subscribe、dispatch、observable 方法的 store 对象
1export function createStore( 2 reducer: Reducer<S, A, PreloadedState>, 3 preloadedState?: PreloadedState | StoreEnhancer<Ext, StateExt> | undefined, 4 enhancer?: StoreEnhancer<Ext, StateExt> 5): Store<S, A, StateExt> & Ext { 6 // 定义状态变量 7 let currentReducer = reducer 8 let currentState: S | PreloadedState | undefined = preloadedState as 9 | PreloadedState 10 | undefined 11 12 // 获取当前状态快照 13 function getState() {} 14 15 // 注册回调函数 16 function subscribe(listener: () => void) {} 17 18 // 派发 action,触发状态更新 19 function dispatch(action: A) {} 20 21 // 实现了 Observable 协议的函数,使得 Store 可以被观察 22 function observable() {} 23 24 // 创建 store 后初始化所有状态 25 dispatch({ type: ActionTypes.INIT } as A) 26 27 const store = { 28 dispatch: dispatch as Dispatch<A>, 29 subscribe, 30 getState, 31 [$$observable]: observable, 32 } as unknown as Store<S, A, StateExt> & Ext 33 return store 34}

getState 方法

getState 方法比较简单,就是返回了当前的状态 currentState

1export function createStore() { 2 function getState(): S { 3 return currentState as S 4 } 5}

subscribe 方法

subscribe 方法用于订阅者注册回调函数,有几个关键点

  • 回调函数被存储在一个 Map 对象中,但是在方法中却定义了两个 Map 对象: currentListeners 和 nextListeners,这是为了防止在 dispatch 过程中对订阅者列表进行修改引起的潜在问题
    • 在添加或删除订阅方法前,都会通过 ensureCanMutateNextListeners 方法创建一份 nextListeners 副本,添加和删除操作都是在 nextListeners 副本上进行
    • 当需要通知订阅者时(即执行 dispatch 方法),通过将 nextListeners 复制到 currentListeners,再遍历 currentListeners 触发所有订阅者执行
  • 在 subscribe 方法执行时,为每个订阅者分配了一个唯一的 id(即 listenerId),在取消订阅的 unsubscribe 方法中,通过闭包能够访问到定义的 listenerId,从而实现移除监听器的效果
1export function createStore() { 2 // 定义状态变量 3 let currentReducer = reducer 4 let currentState: S | PreloadedState | undefined = preloadedState as 5 | PreloadedState 6 | undefined 7 let currentListeners: Map<number, ListenerCallback> | null = new Map() 8 let nextListeners = currentListeners 9 10 function subscribe(listener: () => void) { 11 // 标记为已订阅状态 12 let isSubscribed = true 13 14 // 避免直接修改 currentListeners 15 ensureCanMutateNextListeners() 16 // 为每个订阅者分配一个唯一的 id 17 const listenerId = listenerIdCounter++ 18 // 将监听器添加到 nextListeners 19 nextListeners.set(listenerId, listener) 20 21 // 返回一个取消订阅函数 22 // 实现逻辑就是通过 id 从 nextListeners 移除监听器 23 return function unsubscribe() { 24 if (!isSubscribed) return 25 26 isSubscribed = false 27 28 ensureCanMutateNextListeners() 29 nextListeners.delete(listenerId) 30 currentListeners = null 31 } 32 } 33 34 // 保护 currentListeners 数组不被意外修改 35 function ensureCanMutateNextListeners() { 36 // 创建一份 nextListeners 副本,避免直接修改 currentListeners 37 if (nextListeners === currentListeners) { 38 nextListeners = new Map() 39 currentListeners.forEach((listener, key) => { 40 nextListeners.set(key, listener) 41 }) 42 } 43 } 44 45 return store 46}

dispatch 方法

dispatch 方法用于执行 Reducer 更新状态并通知所有订阅者执行,具体实现有两个关键点

  • 在执行 reducer 更新状态前,会通过 isDispatching 设置为 true 的方式加锁,确保不会执行其他 action,reducer 更新状态结束后,再关闭锁
  • 在遍历通知订阅者前,将 nextListeners 复制到 currentListeners 再遍历执行,避免对于 nextListeners 的操作造成订阅者执行错误
1export function createStore() { 2 // 定义状态变量 3 let currentReducer = reducer 4 let currentState: S | PreloadedState | undefined = preloadedState as 5 | PreloadedState 6 | undefined 7 let currentListeners: Map<number, ListenerCallback> | null = new Map() 8 let nextListeners = currentListeners 9 let isDispatching = false 10 11 function dispatch(action: A) { 12 // 确保不会执行其他 action 13 if (isDispatching) { 14 throw new Error('Reducers may not dispatch actions.') 15 } 16 17 try 18 // isDispatching 设置为 true,相当于加锁 19 isDispatching = true 20 // 执行 reducer 函数更新状态 21 currentState = currentReducer(currentState, action) 22 } finally { 23 // 结束 dispatch 执行关闭锁 24 isDispatching = false 25 } 26 27 // 修改状态后,触发所有订阅者执行 28 // 注意这里将 nextListeners 复制到 currentListeners 再遍历执行, 29 // 避免对于 nextListeners 的操作造成订阅者执行错误 30 const listeners = (currentListeners = nextListeners) 31 listeners.forEach((listener) => { 32 listener() 33 }) 34 35 // 返回 action,符合 dispatch 的标准行为 36 return action 37 } 38 39 return store 40}

observable 方法

observable 方法将 Store 转换为一个符合 Observable 提案的可观察对象,可以应用在到响应式编程中或者与 RxJS 集成

可以这样理解可观察对象:Store 就是一个黑盒,内部的状态如何变化是无法知道的,而转变为可观察对象(observable)之后,就像带上了一副透视眼镜,每当状态变化时,都能够通过透视眼镜(observable 的 next 方法),获取到最新的状态

observable 方法实现步骤如下

  1. 定义 observeState 方法观察 observer 对象状态的变化,通过 observer 的 next 属性传入当前的状态
  2. 立即执行一次 observeState 方法确保当前状态被观察,然后调用 subscribe 方法注册当前观察者,这样当后续的状态变化时,都能够通过发布-订阅模式获取到最新的状态
1export function createStore() { 2 function observable() { 3 // 引用 store 的 subscribe 方法 4 const outerSubscribe = subscribe 5 6 return { 7 // 实现 subscribe 方法,接收一个 observer 对象 8 subscribe(observer) { 9 // 定义一个函数来观察状态变化 10 function observeState() { 11 // 确保 observer 有 next 方法 12 if (observer.next) { 13 // 调用 next 方法并传入当前状态 14 observer.next(getState()) 15 } 16 } 17 18 // 立即执行一次以确保当前状态被观察 19 observeState() 20 // 调用外部的 subscribe 方法注册观察者 21 const unsubscribe = outerSubscribe(observeState) 22 // 返回一个取消订阅的方法 23 return { unsubscribe } 24 }, 25 26 // 返回 observable 对象本身,符合 Observable 协议 27 [$$observable]() { 28 return this 29 }, 30 } 31 } 32}

react-redux 实现原理

react-redux 是 react 组件获取和操作 redux 状态的桥梁,提供了三类 api 用来操作状态

  • Provider 组件:包裹在应用最外层,使用 react 的 context 来传递 Store
  • connect:一个高阶函数,用于将 Store 和 Action 映射到 react 组件的 props
  • hooks:用于在 react 组件中使用 Store 和 Action,简化 connect 的操作

因为现在更推荐使用 hooks 操作 Store,所以接下来我们分析 react-redux 中更为常用 Provider 组件和 hooks 的实现原理

Provider 组件

首先先看 Provider 组件的实现原理,实现步骤如下

  • 接收 Store 和可选的 context 作为参数
  • 定义包含 store 和订阅逻辑的 contextValue
  • 使用 react 的 useIsomorphicLayoutEffect hook 监听 store 状态变化,进行订阅和通知
  • 通过 Context.Provider 将 contextValue 传递给子组件,保证 react 的子组件能够访问 Store
1import { useMemo } from 'react' 2 3function Provider<A extends Action<string> = UnknownAction, S = unknown>({ 4 store, 5 context, 6 children, 7}: ProviderProps<A, S>) { 8 // Redux store 和订阅逻辑 9 const contextValue = useMemo(() => { 10 // 创建一个订阅对象,它负责监听 store 的变化 11 const subscription = createSubscription(store) 12 return { 13 store, 14 subscription, 15 } 16 }, [store]) 17 18 // 获取当前的 store 状态 19 const previousState = useMemo(() => store.getState(), [store]) 20 21 // 客户端渲染优先使用 useLayoutEffect,否则使用 useEffect 22 useIsomorphicLayoutEffect(() => { 23 const { subscription } = contextValue 24 // 设置订阅对象的状态变化时的回调函数,尝试订阅 store 的变化 25 subscription.onStateChange = subscription.notifyNestedSubs 26 subscription.trySubscribe() 27 28 // 如果上次渲染后 store 状态发生了变化,通知所有子订阅者 29 if (previousState !== store.getState()) { 30 subscription.notifyNestedSubs() 31 } 32 33 // 组件卸载时的清理逻辑,取消订阅并清理回调函数 34 return () => { 35 subscription.tryUnsubscribe() 36 subscription.onStateChange = undefined 37 } 38 }, [contextValue, previousState]) 39 40 // 优先使用自定义的 context,否则使用默认的 ReactReduxContext 41 const Context = context || ReactReduxContext 42 43 // 使用 Context.Provider 包裹子组件,并传递 contextValue 44 // 允许子组件通过 context 访问到 Redux store 和订阅逻辑 45 return <Context.Provider value={contextValue}>{children}</Context.Provider> 46}

hooks 简化操作

使用 react-redux 提供的 hooks 在 react 组件中直接操作 Store,相比于使用 connect 更加简单,下面就分析两个最常用的的 hooks:useSelector 和 useDispatch

useSelector 用于在 react 组件中,通过回调函数获取某一部分自己需要的 Store,核心实现原理如下

  1. 从 react 组件的上下文 Context 中获取 Store
  2. 通过传入的回调函数 selector 获取需要的 Store
1import { useCallback, Context } from 'react' 2 3export function createSelectorHook(context = ReactReduxContext): UseSelector { 4 const useReduxContext = useDefaultReduxContext 5 6 return function useSelector<TState, Selected extends unknown>( 7 selector: (state: TState) => Selected 8 ): Selected { 9 // 从 Context 中获取 Store 10 const { store } = useReduxContext() 11 12 // 使用 useCallback hooks 包裹提高性能 13 const wrappedSelector = useCallback<typeof selector>( 14 { 15 [selector.name](state: TState) { 16 // 通过传入的回调函数 selector 获取 Store 17 const selected = selector(state) 18 19 return selected 20 }, 21 }[selector.name], 22 [selector] 23 ) 24 25 // 返回选择的状态 26 return selectedState 27 } 28} 29 30export const useSelector = createSelectorHook()

useDispatch 用于在 react 组件中获取 dispatch 方法,实现原理也很简单,直接返回了 Store 的 dispatch 方法

1export function createDispatchHook(context = ReactReduxContext) { 2 const useStore = 3 context === ReactReduxContext ? useDefaultStore : createStoreHook(context) 4 5 return function useDispatch() { 6 // 本质就是返回了 Store 的 dispatch 方法 7 const store = useStore() 8 return store.dispatch 9 } 10} 11 12export const useDispatch = createDispatchHook()

redux-toolkit

redux-toolkit 提供了一系列简化 redux 配置和操作的方法,下面我们分析两个最常用的方法

  • configureStore:简化了 createStore 方法的配置
  • createSlice:将初始状态 Store、Action、Reducer 统一放在一个切片(Slice)集中管理

configureStore 方法

configureStore 方法最终会调用 redux 的 createStore 方法创建 Store,但是简化了如下部分的配置

  • 中间件配置:包含了一些常用的默认中间件(如:redux-trunk),并且不用手动配置 applyMiddleware 就可以添加中间件
  • DevTools 配置:自动集成 Redux DevTools,并且支持自定义配置,并且生产环境时可以自动禁用 DevTools
  • Reducer 组合:可以接受一个 Reducer 对象,将多个 Reducer 组合起来
  • 增强器配置,简化了增强器的操作,无需手动组合
1export function configureStore< 2 S = any, 3 A extends Action = UnknownAction, 4 M extends Tuple<Middlewares<S>> = Tuple<[ThunkMiddlewareFor<S>]>, 5 E extends Tuple<Enhancers> = Tuple< 6 [StoreEnhancer<{ dispatch: ExtractDispatchExtensions<M> }>, StoreEnhancer] 7 >, 8 P = S, 9>(options: ConfigureStoreOptions<S, A, M, E, P>): EnhancedStore<S, A, E> { 10 // 构建获取默认中间件的函数 11 const getDefaultMiddleware = buildGetDefaultMiddleware<S>() 12 13 const { 14 reducer = undefined, 15 middleware, 16 devTools = true, 17 preloadedState = undefined, 18 enhancers = undefined, 19 } = options || {} 20 21 let rootReducer: Reducer<S, A, P> 22 23 // 根据 reducer 类型创建 rootReducer 24 if (typeof reducer === 'function') { 25 rootReducer = reducer 26 } else if (isPlainObject(reducer)) { 27 rootReducer = combineReducers(reducer) as unknown as Reducer<S, A, P> 28 } 29 30 // 定义最终的中间件数组 31 let finalMiddleware: Tuple<Middlewares<S>> 32 if (typeof middleware === 'function') { 33 finalMiddleware = middleware(getDefaultMiddleware) 34 } else { 35 finalMiddleware = getDefaultMiddleware() 36 } 37 38 // 配置 compose 函数,用于增强器的组合 39 let finalCompose = compose 40 41 // devTool 相关配置 42 if (devTools) { 43 finalCompose = composeWithDevTools({ 44 // Enable capture of stack traces for dispatched Redux actions 45 trace: !IS_PRODUCTION, 46 ...(typeof devTools === 'object' && devTools), 47 }) 48 } 49 50 // 创建中间件增强器 51 const middlewareEnhancer = applyMiddleware(...finalMiddleware) 52 53 // 构建获取默认增强器的函数 54 const getDefaultEnhancers = buildGetDefaultEnhancers<M>(middlewareEnhancer) 55 // 获取最终的增强器数组 56 let storeEnhancers = 57 typeof enhancers === 'function' 58 ? enhancers(getDefaultEnhancers) 59 : getDefaultEnhancers() 60 61 const composedEnhancer: StoreEnhancer<any> = finalCompose(...storeEnhancers) 62 63 // 最终调用 redux 的 createStore 创建 Store 64 return createStore(rootReducer, preloadedState as P, composedEnhancer) 65}

createSlice 方法

createSlice 方法目标是创建一个 slice 切片统一管理 Store、Action、Reducer 并简化配置,具体实现步骤如下

  • 定义 slice 名称和 reducer 路径
  • 配置 reducers,支持函数或对象
  • 创建 context 管理 reducers 和 action creators
  • 遍历并处理 reducers,生成 action types 和 reducers
  • 构建最终的 reducer 和 slice 对象,包括 reducer、actions、caseReducers 和其他实用方法
1export function buildCreateSlice({ creators }: BuildCreateSliceConfig = {}) { 2 return function createSlice(options) { 3 const { name, reducerPath = name } = options 4 5 // 将 reducer 配置为函数或对象形式 6 const reducers = 7 (typeof options.reducers === 'function' 8 ? options.reducers(buildReducerCreators<State>()) 9 : options.reducers) || {} 10 11 const reducerNames = Object.keys(reducers) 12 13 // 创建 context 管理 reducers 和 action creators 14 const context: ReducerHandlingContext<State> = { 15 sliceCaseReducersByName: {}, 16 sliceCaseReducersByType: {}, 17 actionCreators: {}, 18 sliceMatchers: [], 19 } 20 21 // 定义处理 reducers 的方法 22 const contextMethods: ReducerHandlingContextMethods<State> = { 23 // 添加一个 case reducer 和对应的 action 类型 24 addCase(typeOrActionCreator, reducer) {}, 25 // 添加一个 matche 和对应的 reducer,处理不基于特定 action 类型的通用逻辑 26 addMatcher(matcher, reducer) {}, 27 // 将一个 action creator 暴露在 slice 的 actions 对象中 28 exposeAction(name, actionCreator) {}, 29 // 将一个 case reducer 暴露在 slice 的 caseReducers 对象中 30 exposeCaseReducer(name, reducer) {}, 31 } 32 33 // 遍历 reducers 并应用 34 reducerNames.forEach((reducerName) => { 35 const reducerDefinition = reducers[reducerName] 36 const reducerDetails: ReducerDetails = { 37 reducerName, 38 type: getType(name, reducerName), 39 createNotation: typeof options.reducers === 'function', 40 } 41 handleNormalReducerDefinition<State>( 42 reducerDetails, 43 reducerDefinition, 44 contextMethods 45 ) 46 }) 47 48 // 构建最终的 reducer 函数 49 function buildReducer() { 50 const [ 51 extraReducers = {}, 52 actionMatchers = [], 53 defaultCaseReducer = undefined, 54 ] = 55 typeof options.extraReducers === 'function' 56 ? executeReducerBuilderCallback(options.extraReducers) 57 : [options.extraReducers] 58 59 const finalCaseReducers = { 60 ...extraReducers, 61 ...context.sliceCaseReducersByType, 62 } 63 64 return createReducer(options.initialState, (builder) => { 65 for (let key in finalCaseReducers) { 66 builder.addCase(key, finalCaseReducers[key] as CaseReducer<any>) 67 } 68 for (let sM of context.sliceMatchers) { 69 builder.addMatcher(sM.matcher, sM.reducer) 70 } 71 for (let m of actionMatchers) { 72 builder.addMatcher(m.matcher, m.reducer) 73 } 74 if (defaultCaseReducer) { 75 builder.addDefaultCase(defaultCaseReducer) 76 } 77 }) 78 } 79 80 // 定义和返回 slice 对象 81 let _reducer: ReducerWithInitialState<State> 82 83 const slice = { 84 // slice 的唯一标识名 85 name, 86 // 在 State 树中的路径,通常与 slice 名称相同 87 reducerPath, 88 // 定义如何根据接收到的 action 和当前 state,并计算新 state 的函数 89 reducer(state, action) {}, 90 // 自动生成的 action creators,与 slice 中的 reducers 相关联 91 actions: context.actionCreators, 92 // case reducers 集合,每个 case reducer 对应处理一种 action 93 caseReducers: context.sliceCaseReducersByName, 94 // 获取 slice 的初始状态函数 95 getInitialState() {}, 96 // 创建和缓存对应 slice 状态的选择器函数 97 getSelectors(selectState: (rootState: any) => State = selectSelf) { 98 const selectorCache = emplace(injectedSelectorCache, this, { 99 insert: () => new WeakMap(), 100 }) 101 102 return emplace(selectorCache, selectState, { 103 insert: () => { 104 const map: Record<string, Selector<any, any>> = {} 105 for (const [name, selector] of Object.entries( 106 options.selectors ?? {} 107 )) { 108 map[name] = wrapSelector( 109 this, 110 selector, 111 selectState, 112 this !== slice 113 ) 114 } 115 return map 116 }, 117 }) as any 118 }, 119 // 从全局状态中提取出当前 slice 的状态 120 selectSlice(state) {}, 121 // 获取包含 slice 的所有选择器 122 get selectors() { 123 return this.getSelectors(this.selectSlice) 124 }, 125 // 将 reducer 动态注入到 store 中(通常用在代码拆分或动态加载 reducer 场景) 126 injectInto(injectable, { reducerPath: pathOpt, ...config } = {}) {}, 127 } 128 129 return slice 130 } 131} 132 133export const createSlice = buildCreateSlice()