Redux 容器 | 原理解析
本文的源码可以在下面链接中找到:我自己实现的Redux,保留了主要实现细节https://github.com/Gravity2333/my-redux
Redux是一个Javascript的状态管理容器。提到Redux,往往会想到结合React做应用全局的状态管理,但是Redux本质上是一个独立的库,你可以将其用在任何需要集中管理的框架(甚至是nodejs)中,只是结合React是我们最常用的开发方式罢了。
注,Redux配合React通常需要使用react-redux库,其提供了Provider,connect方法将Redux与React做连接,由此可见Redux是独立的,并不是依附于React。
单向数据流
Redux采用单向数据流的方式管理状态,View通过派发(Dispatch) action给Reducer的方式,生成一个全新的State快照,从而修改全局的State。同时,View 通过subscibe的方式监听到state的变动,从而修改页面,整个过程数据流是单向的,如下所示:
需要注意,View无法直接修改State!只能通过派发Action的方式!
本文默认你已经了解Redux的基本使用,对其用法不做赘述,如果不了解,请看 Redux 中文官网
下面就简单说一下Redux的实现原理,很简单,没有任何的难点。
createStore 创建store仓库
先从createStore说起,其作用是创建一个Redux仓库,参数定义如下:
createStore(reducer , enhancer)
其中,reducer是一个用来接受action返回新的state快照的函数,其参数定义为:
reducer(currentState, action)
需要注意的是,reducer是一个纯函数,传入当前的state快照和当前更新的action,返回一个全新的state。返回值需要是一个全新的state,如果返回的是原先的state,会破环不可变性的原则,可能导致结合React时,react-redux监听不到更新。
处理 enhancer
enhancer是一个增强器,其接收一个高阶函数,会分两次调用,第一次调用传入createStore函数本身,第二次调用传入当前createStore函数接受的reducer函数。 第二次调用的函数需要返回一个“增强过”的store,这个enhancer函数通常是applyMiddleware函数,这个后面详细讲。
createStore函数进入之后的第一步,就是处理enhancer,实现如下:
/** createStore 函数 用来创建store对象 */
export default function createStore<StateType = any,ActionType extends Action = Action
>(reducer: Reducer<StateType, ActionType>,enhancer?: any
): Store<StateType, ActionType> {/** 判断enhancer是否传入,如果传入则传入createStore reducer等*/if (typeof enhancer !== "undefined") {if (typeof enhancer !== "function") {throw new Error("enhancer参数需要传入一个函数!");}return enhancer(createStore)(reducer);}
...
}
如果enhancer函数传入,并且是一个函数,就高阶调用enhancer (这也是函数柯里化的思想)第一次传入createStore函数本身,第二次传入reducer,并且直接把enhancer的返回值,作为createSore函数的返回值,需要enhancer内部去调用createStore函数,并且处理store进行返回,
初始化一些全局变量
处理完enhancer,下面就需要初始化一些全局变量,如下:
- currentState 当前的state快照,每次更新的state都存在这个变量中
- currentReducer 当前的reducer函数,注意reducer函数是可以更换的
- currentListeners 表示当前Listener的Map 存储当前所有注册时监听函数
- nextListeners 下一步listener 默认和currentListener指向相同的Map对象,Redux采用双缓冲技术来维护Listener,下面详细讲这两个变量的作用
- listenerCnt 监听器计数器,表示当前添加到第几个监听器了,自增,用来作为ListenerMao的key值。
- isDispatching 表示当前的reducer函数正在执行,相当于把reducer执行当成一个临界资源,用isDispatching上锁。
dispatch派发更新
dispatch函数是store的核心,其传入一个action,内部会将action以及当前的state快照作为参数传入给reducer函数。并且将reducer返回值作为新的快照存入currentState变量。最后再调用listeners通知订阅者更新。
第一步是检验action函数的合法性,需要传入 { type: string }
function dispatch(action: ActionType) {/** 检查action合法性 */if (typeof action !== "object")throw new Error("dispatch错误: 请传入一个对象类型的Action");if (typeof action.type !== "string")throw new Error("dispatch错误: Action.type必须为string");...
}
第二步,检查当前dispatch是否在reducer函数内部执行
redux规定,reducer必须是纯函数,也就是其内部是不能派发新的action的,与外界交互会破坏reducer纯函数的特性 (getState获取当前快照也属于和外界交互,不被允许)
同时,如果在reducer内部调用dispatch可能会导致死循环。
所以,这一步就是使用 isDispatching 检查当前dispatch是否嵌套执行
if (isDispatching) {/** 如果isDispatching = true 表示当前正在运行reducer 此时为了保证reducer是纯函数 其内部* 1. 不能调用dispatch* 2. 不能调用getState获取state*/throw new Error("dispatch错误: 无法在reducer函数内调用dispatch 请保证reducer为纯函数!");}
第三步 就是给reducer函数的允许上锁,并且传入currentState和action执行reducer函数, 如下:
/** 调用reducer */try {isDispatching = true;/** 调用reducer 获取新的state */currentState = currentReducer(currentState, action);} finally {isDispatching = false;}
reducer的结果会被作为新的快照赋给currentState!
最后一步就是执行listener,通知订阅者更新,这里请先忽略
const listener = (currentListeners = nextListeners);
你就当成是遍历当前listener并且执行即可 如下:
/** trigger listener *//** 同步nextListeners 和 currentListeners */const listener = (currentListeners = nextListeners);listener.forEach((listener) => listener());
双缓冲技术&Listener
我们考虑一下,如果使用单独一个Map保存listener会有什么问题
如果我们注册了以下listener
const listener = ()=>{store.subscribe(listener)
}
在单Map的情况下,运行就会如下所示,造成死循环。
当我们在listner中继续注册listner,就会动态修改ListenerMap,让listenerMap变大,导致listener触发永远无法完成,死循环。
对于删除,也会有类似的问题!为了解决,我们也需要一个类似 “快照”的形式。 在trigger listener之前,就保存当前的listener快照,不论listener中如何添加 删除 listener,都不会影响快照的内容,其修改只会在下一次trigger生效。
redux的解决办法是设置两个Map指针 分别为 nextListeners 和 currentListeners 其中,nextListener为最新的ListenerMap,每次添加listener时都是向其中添加。 而currentListener为triiger之前的快照Map,每次trigger之前,都会把Next指向的Map赋给current。
如果在listener内部,嵌套调用了subscribe函数,那么此时subscribe内部会先检查,nextListener和currentListener是否指向相同的Map,如果是则拷贝一份currentListener Map给next,并且在下一次trigger之前,所有的listener都只被添加到nextListener中,其流程如下:
ensureCanMutateNextListeners
检验两个Map是否相等并且拷贝Map的逻辑被封装在 ensureCanMutatieNextListeners 函数中,本意是,保证Listener可以被修改 , 逻辑如下:
/** 保证能够修改 nextListeners Map对象* redux采用双缓冲策略 使用 nextListeners 存储最新的listener currentListener存储旧的listener* 当subscirbe的时候 只修改nextListener* 当trigger的时候,同步nextListener 和 currentListener* 为了防止listener trigger的时候再次调用subscribe导致listener Map变大 在执行trigger之前,同步两个Map* 每次subscirbe的时候都会检查,如果两个Map相同,那么就创建一个Map快照赋给nextListeners ,然后操作nextListeners* 这样即便在triiger时subscribe 也不会影响当前遍历的map*/function ensureCanMutatenextListeners() {if (nextListeners === currentListeners) {/** 创建快照 赋给nextListeners */nextListeners = new Map();currentListeners.forEach((listener, key) => {nextListeners.set(key, listener);});}}
这个函数会在每次新增 listener或者删除listener时被调用,以确保新插入的listener不会影响快照ListenerMap!
subscribe 注册监听
接下来就是注册监听的函数了
这个函数的作用是
1. 检查当前函数是否在dispatch中被调用,由于reducer的纯函数特性,其内部注册listener也是与外界交互的一部分,不被允许
2. 保证当前监听注册不会影响listener快照
3. 向NextListeners中注册listener
4. 返回 unListener函数
实现如下:
/** subscribe 函数 */function subscribe(listenerCallback: ListenerCallback) {if (typeof listenerCallback !== "function")throw new Error("subscribe错误: listenerCallback必须是函数类型!");/** 保证reducer是纯函数* 1. 在其中不能dispatch* 2. 不能注册listener* 3. 不能调用getState*/if (isDispatching)throw new Error(`subscribe错误: 不能在reducer中注册listener 请保证reducer是纯函数`);/** 开始注册,先检查当前双缓冲书否为同步状态,如果同步,创建快照 */ensureCanMutatenextListeners();const listenerId = listenerCnt++;nextListeners.set(listenerId, listenerCallback);/** 表示已经注册 */let isSubscribed = true;return () => {if (!isSubscribed) return; // 防止多次unsubcribe/** 保证在reducer中也不能 unsubcribe */if (isDispatching)throw new Error(`unsubscribe错误: 不能在reducer中注销listener 请保证reducer是纯函数`);isSubscribed = false;/** 删除前也需要先创建快照 */ensureCanMutatenextListeners();nextListeners.delete(listenerId);currentListeners = null; // current没用了 可以直接回收};}
需要注意的是,在reducer函数中unListen注销监听也是不允许的,需要在unlisten函数中判断。
同时,删除listener也需要先调用ensureCanMutateNextListener来保证不影响currentListeners!
删除之后,由于currentListener的值已经赋给dispatch函数中的listeners变量了
并且此时肯定保证nextListener和currentListener不同了,可以直接把currentListener置空,此时在trigger执行完之后,对应的Map快照会被直接垃圾回收。
getState 获取当前state快照
getState的作用很简单,就是返回当前的currentState快照,不过需要注意,在其中需要拦截reducer中调用getState的情况,如下:
/** getState 在dispacth中 不能调用 */function getState(): StateType {if (isDispatching)throw new Error("无法在reducer中调用 getState 请保证reducer是纯函数");return currentState;}
初始化state & EActionType
调用createStore时,会调用一次传入的reducer进行初始化,传入reducer的action为 EActionType.INIT 为redux的内部type值 如下:
/** redux 内置action类型 */
export enum EActionType {/** 初始化Action */INIT = `@@redux/INIT`,/** 更换Reducer 之后初始化Action类型*/REPLACE = `@@redux/REPLACE`,
}
调用逻辑如下:
/** 初始化reducer 获得initialState */dispatch({ type: EActionType.INIT } as ActionType);
所以,redux初始化状态下,reducer第一次接收到的action对应的type为 @@redux/INIT
更换reduer
某些场景下,我们需要更换store中保存的reducer , redux为我们提供了replaceReduer函数,如下:
/** 更换reducer */function replaceReducer(reducer: Reducer<StateType, ActionType>) {if (typeof reducer !== "function")throw new Error("reducer 必须是一个函数!");currentReducer = reducer;/** 重新dispatch 给state赋值 */dispatch({ type: EActionType.REPLACE } as ActionType);}
通过实现可以看出,更换的reducer会被赋给currentReducer变量,并且会在替换之后调用一次reducer重新初始化,传入的type为 @@redux/REPLACE
applyMiddleware 处理中间件
redux自定义中间件需要传入一个middleWare函数。这个函数是一个高阶函数,返回一个接收store的方法,这个方法也是一个高阶函数,接收当前redux原生的dispatch函数,并且通过MonkeyPatch的方法修改这个函数,并且保存。如下
function MyMiddleware(store){return next => action => {// 在原生dispatch调用之前做一些处理 ...next(action) // 调用原生dispatcj// 在原生dispatch调用只做一些处理 ...}
}
我们举一个redux-thunk的实现为例
import { MiddlewareAPI } from "../../lib/typings";export function thunk(middlewareApi: MiddlewareAPI) {return (next) => (action) => {if (typeof action === "function") {action(next, middlewareApi.getState);} else {next(action);}};
}
可以看到 thunk函数接受一个middlewareApi方法,你可以简单的将其理解为 store、返回一个函数
这个函数接受一个next , 也就是dispatch方法,再次返回一个函数 这个函数就是真正在redux中调用的dispatch方法,相当于对原生的dispatch方法进行了覆盖。
这个方法内部,thunk会检查,如果action是函数,那么就会执行,并且传入原生dispatch(next函数)和middlewareApi.getState方法
如果不是函数,直接执行next(action)
这样,就达到了异步action的目的。
复习完middleware的使用方法,我们再来看一下applyMiddleware实现逻辑
applyMiddleware接受一个展开的middlewares数组,如下
export default function applyMiddleWare(...middlewares: Middleware[]) {
你可以用 applyMiddleWare(middleware1, middleware2, middleware3 ...) 的方式来注册多个中间件
其返回一个函数,这个函数接受一个createStore方法,这个方法由createStore函数传入,即上面介绍的:
实现如下:
export default function applyMiddleWare(...middlewares: Middleware[]) {return (createStore: <StateType = any, ActionType extends Action = Action>(reducer: Reducer<StateType, ActionType>,enhancer?: any) => Store<StateType, ActionType>) => {... ... ...
}}
这个返回的函数接受createStore再次返回一个函数,接受createStore传入的reducer方法
这也就对应了 createStore内部调用的 enhancer(createStore, reducer)
export default function applyMiddleWare(...middlewares: Middleware[]) {return (createStore: <StateType = any, ActionType extends Action = Action>(reducer: Reducer<StateType, ActionType>,enhancer?: any) => Store<StateType, ActionType>) =>(reducer: Reducer) => {...
}}
拿到了 createStore和reducer,内层函数就调用createStore(reducer)创建了store对象,并且对这个store进行覆盖和操作。
为了不让中间件函数对store有很大的影响,内层函数封装了一个 MiddleWareApi对象,其中包含了getState方法以及dispatch方法,store中的其他方法比如subscribe都没有被暴露。
(reducer: Reducer) => {const store = createStore(reducer);let dispatch = (...args: any[]) => {throw new Error("创建中间件的过程中 不能调用dispatch");};const middlewareApi: MiddlewareAPI = {/** 创建middleware的时候 不能调用dispatch */dispatch: (...args: any[]) => dispatch(...args),getState: store.getState,};
middlewareApi.dispatch默认情况下直接调用会抛出异常,告诉中间件不能在创建中间件的过程中就调用dispatch,因为此时的dispatch函数可能还没有完成创建。
完成middlewareApi的创建后,就开始调用middlewares了,复习一下,middleware第一步接受一个middlewareApi,所以applyMiddleware中就有如下处理:
const dispatchPatchChain = middlewares.map((middleware) =>middleware(middlewareApi));
先便利middlewares数组,执行每个middleware并且传入middlewareApi,将结果的函数放回middlewares
第二部,中间件需要接受原生的dispatch,并且返回"加强过的dispatch", 但是由于可能有多个中间件,每个中间件都要对dispatch函数进行加强,所以redux内部实现了compose的组合方法,其本质上就是reduce,属于函数编程的思想。
你可以将compose理解成流水线,传入初始dispatch对象,执行每个middleware返回的dispatch处理方法,把每个处理方法返回的新的dispatch作为参数传给下一个中间件,这样就完成了一系列中间件的组合。 compose函数实现如下:
export default function compose(...patchDispatch: any[]){return (dispatch: Dispatch) => {return patchDispatch.reduce((currentDispatch,patchFn)=>{return patchFn(currentDispatch)},dispatch)}
}
可以看到,就是reduce函数的封装,其调用方式也很简单,就是
const enhancedDiaptch = compose(middlewares)(store.dispatch)
所以,applyMiddleware的最后一步,就是compose处理disptach,最后把新的dispatch赋给store进行替换。
dispatch = compose(...dispatchPatchChain)(store.dispatch);return {...store,dispatch,};
最后,贴一下完整版的applyMiddleWare
import { Action, Middleware, MiddlewareAPI, Reducer, Store } from "./typings";
import compose from "./utils/compose";/** 应用中间件 */
export default function applyMiddleWare(...middlewares: Middleware[]) {return (createStore: <StateType = any, ActionType extends Action = Action>(reducer: Reducer<StateType, ActionType>,enhancer?: any) => Store<StateType, ActionType>) =>(reducer: Reducer) => {const store = createStore(reducer);let dispatch = (...args: any[]) => {throw new Error("创建中间件的过程中 不能调用dispatch");};const middlewareApi: MiddlewareAPI = {/** 创建middleware的时候 不能调用dispatch */dispatch: (...args: any[]) => dispatch(...args),getState: store.getState,};const dispatchPatchChain = middlewares.map((middleware) =>middleware(middlewareApi));dispatch = compose(...dispatchPatchChain)(store.dispatch);return {...store,dispatch,};};
}