# redux 实现

本文结合 ts模拟实现一版 redux

# redux 是什么

redux就是一个纯状态管理器,最开始接触它的时候是在学习 react了解到的 react-redux,对于其中的一些新的概念,比如 storereducerstatedispatch等概念觉得很高大上,当然现在用了很久了已经是比较熟悉了

今天我们再重新梳理一下 redux,结合 ts来模拟实现一版,主要是为了学习其中的思想,把一些好的实践运用于我们的实际工作中。

# 开始

本项目使用 ts 实现,首先需要安装 typescriptts-node,然后在 package.json 中添加如下内容:

{
  ...
  "scripts": {
    "start": "nodemon --exec node --loader ts-node/esm --experimental-modules --es-module-specifier-resolution=node ./test/index.ts",
    "build": "tsc -p ."
  },
}

start命令用于测试,build命令用于项目打包。

项目整体结构如下:

.
├── package.json
├── readme.md
├── src
│   ├── applyMiddleware.ts
│   ├── bindActionCreators.ts
│   ├── combineReducers.ts
│   ├── compose.ts
│   ├── createStore.ts
│   ├── index.ts
│   └── types.ts
├── test
│   ├── index.ts
│   ├── middlewares
│   │   ├── log.ts
│   │   └── time.ts
│   └── reducers
│       ├── goods.ts
│       ├── index.ts
│       └── user.ts
└── tsconfig.json

完整项目地址 (opens new window),感兴趣可以 start 查看

# createStore

首先看一下 createStore的用法:

const store = createStore(reducer[, initialState[, storeEnhencer);
const { getState, dispatch, subscribe } = store;

其中,createStore接受两个参数,reducerinitialState,返回一个 store对象,这里贴上实现:

const createStore: CreateStore = (reducer, initialState, storeEnhencer): Store => {
  const listeners: Listeners = []
  let state = initialState || {}
  const getState = () => {
    return state
  }
  const dispatch = (action: Action) => {
    let nextState = reducer(state, action) as State
    state = nextState
    listeners.forEach((listener: Listener) => {
      listener(nextState)
    })
  }
  const subscribe = (listener: Listener) => {
    listeners.push(listener)
    // 返回退订函数
    return function unSubscribe() {
      listeners.splice(listeners.findIndex(listener), 1)
    }
  }
  const store = { getState, dispatch, subscribe }
  return store
}

可以看到,在 createStore内部实际是使用了发布订阅的模式,通过 subscribe方法在 listeners数组中添加监听者,再通过 dispatch方法更新 state,并且通知订阅者。

然后,我们新建 test/index.ts文件,添加如下测试内容:

import { createStore } from "../src/index";

const initialState = { name: "jack", age: 10 };

const reducer = (state = initialState, action: Action) => {
  switch (action.type) {
    case "INCREACE":
      return {
        ...state,
        age: state.age + 1,
      };
    default:
      return initialState;
  }
};

const store = createStore(reducer);

store.subscribe((state: State) => {
  console.log(state, "subscribe");
});

console.log(store.getState(), "getState");

setTimeout(() => {
  store.dispatch({ type: "INCREACE" });
}, 1000);

可以看到,初始输出结果是 10,一秒后输出为 11

# combineReducers

上面的 reducer 中只存储了一个 state,如果是多个 state放在一个 reducer中,那维护起来就比较困难了,此时我们需要实现一个 combineReducers 方法,将各个子模块的 reducer 聚合起来

combineReducers的用法如下:

const reducer1 = function (state = {}, action) {
  switch (action.type) {
    case "xxx":
    default:
      return state1;
  }
};

const reducer2 = function (state = {}, action) {
  switch (action.type) {
    case "xxx":
    default:
      return state2;
  }
};

const reducer = combineReducers({ reducer1, reducer2 });

然后实现 combineReducers 如下:

const combineReducers = (reducers: Reducers) => {
  const keys = Object.keys(reducers);
  const newState: State = {};
  return function newReducer(state: State, action: Action) {
    keys.forEach((key) => {
      const reducer = reducers[key];
      const prevState = state[key];
      newState[key] = reducer(prevState, action);
    });
    return newState;
  };
};

combineReducers 返回了一个新的 reducer函数,这个函数将作为传递给 createStore的参数,返回的newReducer 函数和原来的 reducer函数一致,都是接收 stateaction 参数

newReducer方法中,通过遍历执行 reducers 中的方法,将每个 reducer模块执行的结果保存到 newState中,作为新的 state保存到 createStore

# applyMiddleware

createStore方法中,我们还可以传入 applyMiddleware参数,用以拓展 dispatch 的能力

applyMiddleware 的使用方式如下:

const mid1 = (store: Store) => (next: Dispatch) => (action: Action) => {
  console.log(store.getState(), "state==========>before");
  next(action);
  console.log(store.getState(), "state==========>after");
};

const mid2 = (store: Store) => (next: Dispatch) => (action: Action) => {
  console.log(store.getState(), "state==========>before");
  next(action);
  console.log(store.getState(), "state==========>after");
};

const store = createStore(reducer, {}, applyMiddleware(mid1, mid2));

当我们执行 dispatch 的时候,会依次执行 mid1 => mid2 => dispatch

现在看下 applyMiddleware 如何实现:

const applyMiddleware = (...middlewares: Middleware[]) => {

  return function storeEnhencer(oldCreateStore: CreateStore) {

    return function newCreateStore (reducer: Reducer, initialState: State) {

      const store = oldCreateStore(reducer, initialState)

      const partialStore = { getState: store.getState }

      const midders = middlewares.map((item: Middleware) => item(partialStore as Store))

      midders.reverse().forEach((middle: Middle) => {
        store.dispatch = middle(store.dispatch)
      });

      return store
    }
  }
}

首先执行 applyMiddleware 返回一个 storeEnhencer函数,该方法接收 oldCreateStore 作为参数,oldCreateStore实际上就是当前的 createStore 方法

此处我们需要修改一下 createStore 方法,这样再看 applyMiddleware 就比较好理解了:

createStore

const createStore: CreateStore = (reducer, initialState, storeEnhencer): Store => {
  // initialState 作为第二个参数没有传递
  if (typeof initialState === 'function' && !storeEnhencer) {
    storeEnhencer = initialState
    initialState = {}
    return storeEnhencer(createStore)(reducer, initialState)
  }
  // other
  ...
}

当传递有第二个参数的时候,并且第二个参数是函数的情况下(实际 initialState 没有传入),然后执行 storeEnhencer 并把 createStore 作为参数传入,此时返回的是 newCreateStore 方法,然后再执行了 newCreateStore(reducer, initialState),在这个函数内部实际上是执行了 oldCreateStore 方法,oldCreateStore 对应的就是 createStore,此处就是创建出了 store

然后再执行:

// 这里只是将 store 的 getState 和 dispatch 方法暴露出去了
const partialStore = {
  getState: store.getState,
  dispatch: (action, ...args) => store.dispatch(action, ...args)
}

const midders = middlewares.map((item: Middleware) => item(partialStore as Store))

实际上是将 store 传入到各个中间件方法中执行一遍,返回的结果如下

const m1 = (next) => (action) => {};

const m2 = (next) => (action) => {};

const midders = [m1, m2];

得到的是包含高阶函数的数组,然后再执行

midders.reverse().forEach((middle: Middle) => {
  store.dispatch = middle(store.dispatch);
});

这一步相当于装饰器的作用,就是将 store.dispatch 放入到每个高阶函数中执行一遍,相当于通过每个高阶函数来增强一下 dispatch的能力

这部分装饰器的理解可以查看之前的写的这篇文章AOP (opens new window)

通过上面的分析可以看到,在redux中提供的这几个方法,createStorecombineReducersapplyMiddleware这几个方法中,都使用到了 闭包,由此可见,正确的使用好闭包可以实现出很强大的功能

# compose

applyMiddleware 方法中,重写 dispatch 使用的是如下的方式:

midders.reverse().forEach((middle: Middle) => {
  store.dispatch = middle(store.dispatch);
});

使用一个函数可以如下表示:

function compose(midders, dispatch) {
  midders.reverse().forEach((middle: Middle) => {
    dispatch = middle(dispatch);
  });
  return dispatch;
}

这种方式比较好理解,但是有个问题是 不够函数式,因为我们需要传递一个 dispatch 参数,并且在函数内部还修改了 dispatch参数,产生了副作用,这对于追求 函数式编程的 redux来说显然是不太合适的。

于是 redux 给出了一个 compose 的实现方式:

const compose = (middlewares: Middle[]) => {
  if (middlewares.length === 1) {
    return middlewares[0];
  }
  return middlewares.reduce(function (prev, next) {
    return function (...args) {
      return prev(next(...args));
    };
  });
};

分析 compose 可知,每次 reduce 迭代返回一个新的函数,并且在该新函数中,prev 保存着对于上次迭代返回的函数的引用,这样函数内部有对于外部变量的引用,形成一个 闭包

对于 compose 函数的解析可以查看之前写的compose 解析 (opens new window)

# bindActionCreators

bindActionCreators 这个方法在开发中使用的很少,主要在 react-reduxconnect函数中使用到

这个方法主要将 dispatch action 的细节隐藏起来,用户只需要传递创建 action 的函数给 redux即可:

// action creators
const increment = () => ({ type: "INCREMENT" });
const setName = (name: any) => ({ type: "SET_NAME", name: name });

// 转换成可调用的 actions
const actions = {
  increment() {
    return store.dispatch(increment());
  },
  setName() {
    return store.dispatch(setName("jack"));
  },
};

// 组件中调用
actions.increment();
actions.setName();

以上生成 actions 的过程,我们发现内部的方法结构都比较相似,于是我们将这个过程来优化一下

我们期望优化后使用的方式如下:

const actions = bindActionCreators({ increment, setName }, store.dispatch);
actions.increment();

实现 bindActionCreators

const actionCreator = (creator: ActionCreator, dispatch: Dispatch) => {
  return function name() {
    return dispatch(creator.apply(this, arguments));
  };
};

const bindActionCreators = (
  actionCreators: ActionCreators,
  dispatch: Dispatch
) => {
  if (typeof actionCreators === "function") {
    return actionCreator(actionCreators, dispatch);
  }

  let boundActions: BoundActions = {};
  Object.keys(actionCreators).forEach((key) => {
    boundActions[key] = actionCreator(actionCreators[key], dispatch);
  });

  return boundActions;
};

总体上,这个过程还是比较简单的,只是将创建 actions 的过程给隐藏了起来

# 总结

以上基本是 redux 的主要内容,基本没有太难理解的东西,这里总结下项目中用到的比较核心的东西:

  1. 发布订阅模式:总体的结构实际上就是在 createStore 中使用了发布订阅的模式
  2. 闭包:此项目频繁使用到了闭包,闭包的灵活使用确实可以很巧妙的解决一些问题
  3. commose 方法:项目中的一大亮点可以说是提供的一个 compose 函数,所谓的 洋葱模型可以是对这个函数模型的解析,实质上就是 装饰者模式,这个函数可以说是遵守 函数式编程 的极好体现,它可以运用于需要 高阶函数的地方,比如 react 的多个 高阶组件 的组合就可以使用这种方式

refer:

完全理解 redux (opens new window)

最后更新时间: 4/12/2022, 4:24:51 PM