当前位置: 首页 > news >正文

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,};};
}

 

 

相关文章:

  • shell编程基础知识及脚本示例
  • 设计模式每日硬核训练 Day 16:责任链模式(Chain of Responsibility Pattern)完整讲解与实战应用
  • 分析型数据库入门指南:如何选择适合你的实时分析工具?
  • 哈希表基础
  • Ollama 在本地分析文件夹中的文件
  • 本安型交换机 + TSN:煤矿智能化的关键拼图
  • AI大模型从0到1记录学习 linux day21
  • 【论文阅读】-周总结-第5周
  • IDEA中使用Git
  • Vue2、Vue3区别之响应式原理
  • 深入理解 Java 单例模式:从基础到最佳实践
  • 【项目篇之垃圾回收】仿照RabbitMQ模拟实现消息队列
  • 查回来的数据除了 id,其他字段都是 null
  • 自然语言处理之机器翻译:注意力机制在低资源翻译中的突破与哲思
  • LeetCode每日一题4.27
  • 【dockerredis】用docker容器运行单机redis
  • C#中属性和字段的区别
  • pytorch搭建并训练神经网络
  • Golang 遇见 Kubernetes:云原生开发的完美结合
  • MPI Code for Ghost Data Exchange in 3D Domain Decomposition with Multi-GPUs
  • 特朗普的百日执政支持率与他“一税解千愁”的世界观和方法论
  • 江西省国资委原副主任李键主动向组织交代问题,接受审查调查
  • 国家发改委:是否进口美国饲料粮、油料不会影响我国粮食供应
  • 国家发改委答澎湃:力争6月底前下达2025年两重建设和中央预算内投资全部项目清单
  • 一季度全国城镇新增就业308万人
  • 伊朗国防部发言人:发生爆炸的港口无进出口军用物资