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

 

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值