手写redux

147 阅读14分钟

react在大型企业级项目中用的是非常多的, 但凡一聊到react, redux又必定是跑不掉的, 作为react的衍生库, redux相当可观的解决了数据的问题, 有很多同学可能只会用, 也可能还有一部分同学用都用的不太明白, 我们不如换一种角度, 来看看redux他的一个大致实现思路, 或许可以帮助你更好的去应用redux

redux说来说出无非也就是四个核心知识点:

redux功能.png 针对每个知识点, redux都提供了对应辅助的功能, 所以在本篇博客中, 我们大致分为如下几块:

  1. 手写createStore
  2. 手写bindActionCreators
  3. 手写combineReducers
  4. 手写applyMiddleware

在此之前, 我们先来做一些准备工作

我们新建一个redux目录, 然后创建几个文件和index.js, 并在index.js中对其他文件的默认导出再做一次导出

目前大致的目录结构如下

|--redux
  |-- index.js 作为出口, # 做默认导出用
  |-- createStore.js # 用于实现createStore的逻辑
  |-- bindActionCreators.js # 用于实现redux的bindActionCreators功能
  |-- combineReducers.js # 用于实现redux的combineReducers功能
  |-- applyMiddleware.js # 用于实现redux的applyMiddleware功能
  |-- compose.js # 用于实现redux的compose函数
  |-- utils.js # 主要存放一些工具方法
// index.js
export { default as createStore } from "./createStore.js";
export { default as bindActionCreators } from "./bindActionCreators";
export { default as combinReducers } from "./combinReducers";
export { default as applyMiddleware } from "./applyMiddleware";
export { default as compose } from "./compose";

同时我们需要书写一些工具函数供我们使用

// utils.js
// 检测一个对象是否为一个plain object
export const isPlainObject = (obj) => {
  if( obj === undefined || Object.getPrototypeOf(obj) !== Object.prototype ) return false;
  return true;
}

// 用于生成一个随机字符串
export const getRandomStr = (length) => {
  if(typeof length !== "number") length = 6;
  return Math.random().toString(36).substr(2, length).split("").join(".");
}

// 在redux中, 为了时刻验证用户的reducer是否满足要求, redux都会在特定的时机去调用
// 传递进来的reducer, 只不过触发的action是一些特殊的type值, 所以我们将这些特殊type
// 值放到utilTypes对象中, 以便后续可以正常使用
export const utilTypes = {
  INIT: () => ({
    type: `@@redux/INIT${getRandomStr()}`
  }),
  UNKOWN: () => ({
    type: `@@redux/UNKOWN${getRandomStr()}`
  })
}

手写createStore

其实要写这些原理, 我们可以从一些现象去着手, 我们可以先来看看附着在createStore上的功能点有哪些需要我们去实现:

  1. createStore可以接受3个参数:

    • reducer: 作为第一个参数, 该参数也是createStore的必填参数, 主要含义为传递进来的根据action来处理数据仓库的一个函数
    • defaultState: 第二个参数, 可以不填, 该参数的含义是表示整个数据仓库的默认值
    • enhancer: 渐进增强的中间件应用函数, 配置了该函数过后, 根据所传递的中间件将改变整个redux仓库disptach的行为
  2. createStore会返回一个对象, 对象结构如下:

    • dispatch: 用于修改当前redux数据仓库数据的函数, dispatch分发了一个action以后, 会去执行reducer, reducer根据对应的action来对仓库的数据进行更改
    • subcribe: 用于订阅每次仓库的值发生改变以后要执行的回调
    • replaceReducer: 很简单, 用于替换掉当前的reducer函数
    • getState: 用于得到当前的redux仓库状态
    • Symbol("observerable"): 这个跟esnext有关, 用于生成一个符合发布订阅模式的方法(这篇博文不做探究, 因为我们几乎可以说从来不用他, 但是他的实现原理还比较复杂, 是redux为了应对将来的场景而预设的)
  3. 当仓库期间, redux会自动触发一个特殊的action来确保你传入的reducer是符合reducer标准的

OK, 知道了这个createStore他存在的形式和他的功能, 我们就可以根据自己的逻辑思维一点一点的去实现他

// 引入两个工具函数, 在编码的时候会用到
import { utilTypes, isPlainObject } from "./utils";


// 正儿八经的createStore函数
export default (reducer, defaultState, enhancer) => {
  // 首先reducer作为必填项, 如果出现错误, 是不能容忍的
  if(typeof reducer !== "function") throw new TypeError("Expected the reducer to be a function");
  // 我们知道, 我们有的时候不会传递defaultState, 而是直接将第二个参数书写为enhancer
  // 这样redux也能够识别, 所以我们可以做一个简单的校验(当然redux做的校验更细节)
  if( typeof defaultState === "function" && enhancer === undefined ) enhancer = defaultState;

  // 这里为什么要把这两哥们提出来在内部用变量保存哈, 主要是因为我们后续要对
  // 这些东西进行替换或者修改操作, 这样写可能更直观一点
  let curReducer = reducer; // 当前的reducer
  let curState = defaultState; // 当前的状态
  let listeners = []; // 后续用来存放监听回调函数的数组
  
  if( typeof enhancer === "function" ) {
    // 这里要做一些中间件的操作, 要跟后续的applyMiddleware有关, 我们先不做处理
    return;
  }

  // 能够走到这里, 代表用户没有传递enhancer, 所以我们就按照最基本的流程去做
  // 其实就是给createStore赋予能够返回一个对象, 对象里有几个函数的能力
  const dispatch = () => {};
  const replaceReducer = () => {};
  const subcribe = () => {};
  const getState = () => {};

  return {
    dispatch,
    replaceReducer,
    subcribe,
    getState
  }
}

这个时候, 我们去自己测试一些这个createStore函数, 我相信他一定会给你一个如同官方redux的一个对象(只是缺失了Symbol(observerble)), 但是这个我们是不写的)

接下来我们要做的, 就是一步一步的完善其中的各个函数, 同时处理好一些小细节

dispatch函数和getState, replaceReducer的实现

我们知道, dispatch的功能就很简单, 他会接受一个action作为参数, 然后在内部触发reducer并将action传递给reducer从而达到修改仓库中的值的目的

// createStore.js
...
const dispatch = () => {
  // dispatch第一步就是要判断action的合法性: 
  // 1. 必须是一个平面对象: 原型指向Object.prototype
  // 2. 必须含有一个type属性
  if( !isPlainObject(action) || action["type"] === undefined ) {
    // 代表没通过验证, 直接报错
    throw new TypeError("action must be a plain object with property 'type'");
  }
  console.warn("curState", curState);
  // 过了验证以后, 我们就要触发reducer了
  curState = curReducer(curState, action);

  // 我们知道, 我们修改了状态以后, 还要做一件事情, 就是将listeners中的所有监听
  // 回调函数依次执行
  if( listeners.length === 0 ) return;
  listeners.forEach(cb => cb());
};

...
const replaceReducer = (reducer) => {
  // replaceReducer也非常简单
  // 只是在赋值之前我们需要对reducer进行一层校验
  if(reducer(undefined, utilTypes.INIT) === undefined) return;
  curReducer = reducer;
};
const getState = () => {
  // 顺便将getState也写了, 因为他实在是easy了
  return curState;
};

...

return {
  dispatch,
  replaceReducer,
  subcribe,
  getState
}
...

这个时候我们之前说过, redux会在一开始就验证reducer的合法性, 会自动触发一个action, 所以我们经常利用这个来做初始化, 但是我们要记住, 一旦createStore中使用了默认值, 则单个reducer的默认值会直接失效

// createStore.js
...
  
if( typeof enhancer === "function" ) {
  // 这里要做一些中间件的操作, 要跟后续的applyMiddleware有关, 我们先不做处理
  return;
}

// 所以这里我们需要补上一些代码, 用来测试reducer的合法性
const result = curReducer(defaultState, utilTypes.INIT); // 触发一个init type
if( result === undefined ) {
  throw new TypeError("reducer cannot return undefined");
}

...

ok, 这个时候我们的dispatch函数就写完了, 经过测试也能够达到我们想要的功能

subcribe的功能实现

subcribe主要是用来传递监听函数的, 当状态改变的时候, 会运行监听的函数

...
const subcribe = (listener) => {
  if(typeof listener !== "function") return;
  listeners.push(listener);
  // subcribe还会返回出去一个函数, 用于接触侦听
  return () => {
    let index = listeners.findIndex(listener);
    if( index === -1 ) return;
    listeners.splice(index, 1);
  }
}
...

手写bindActionCreator

bindActionCreator也比较简单, 他的主要功能如下:

  1. bindActionCreator接收2个参数

    • actionCreator / actionCreatorSet: action的创建函数, 也可以是action创建函数的一个对象集合
    • dispatch: 对应的仓库dispatch函数
  2. bindActionCreator会直接返回一个对应的函数或者函数集合, 当我们调用该函数的时候, 会自动进行dispatch, 而不需要我们再手动操作了

想到这里, 其实这个函数蛮简单的, 无非就是做个高阶函数套一层, 你给我一个函数, 我给你一个新的函数, 新的函数里自动帮你调用了dispatch

// bindActionCreator.js
// 用于帮助我们创建一个可以自动dispatch的action creator
const getAutoDispatchActionCreator = (actionCreator, dispatch) => (...args) => dispatch(actionCreator(...args));

export default (actionCreator, dispatch) => {
  console.log(actionCreator);
  // 同样我们先做一个兼容性的处理
  if(!(typeof actionCreator !== "function" || typeof actionCreator !== "object")) {
    throw new TypeError("bindActionCreators expected an object or a function"); 
  }

  // 如果dispatch没有传, 我们直接将源对象返回出去
  if(typeof dispatch !== "function") return actionCreator;

  // 如果actionCreator就是一个函数, 不是对象, 我们就直接调用上面的函数
  if(typeof actionCreator === "function") {
    return getAutoDispatchActionCreator(actionCreator, dispatch);
  }

  const autoDispatchCreators = {};

  // 如果actionCreator是一个对象, 那我们需要返回一个新的对象, 对象里的每一个函数, 都映射成一个新的函数
  for(const prop in actionCreator) {
    if(typeof actionCreator[prop] !== "function") {
      throw new TypeError("bindActionCreators expected an object or a function"); 
    }

    autoDispatchCreators[prop] = getAutoDispatchActionCreator(actionCreator[prop]);
  }

  return autoDispatchCreators;
}

手写combineReducers

combineReducer我们要出现他的两个现象:

  1. 接受一个函数对象作为参数, 也可以接收单独的一个函数作为参数(虽然这没有多大的意义)
  2. 会返回一个函数reducer, 返回的函数可以接受action来改变状态

combineReducers也好说, 他其实就是把多个reducer聚合到一起, 我们知道一个store只能有一个reducer, 这个reducer就是一个函数, 该函数最终要返回一个新的状态, 那么我们怎么把这个多个reducer聚合到一个最终的reducer中呢?

// combineReducers.js
import { utilTypes } from "./utils";

export default (reducers) => {
  // 如果传进来的reducers就是一个函数, 直接就返回出去
  if( typeof reducers === "function" ) return reducers;

  // 对reducers进行校验
  const (key in reducers) {
    let curReducer = reducers[key]; // 拿到当前reducer
    // 当拿到reducer的时候, 这里要确认一下reducer的合法性
    // redux是直接进行了两次redux的合法性校验
    let passFlag = true;
    const arr = [utilTypes.INIT, utilTypes.UNKOWN];
    arr.forEach(el => {
      let result = curReducer(el());
      if(result === undefined) passFlag = false;
    })

    if( passFlag === false ) {
      // 代表校验没有通过, 直接报错
      throw new TypeError("reducer cannot return undefined");
    }
  }

  // 这个时候我要做的就是返回出去一个函数, 这个函数就是传递给store的reducer
  return (state = {}, action) => {
    const lastState = {};
    // 当我拿到一个action的时候, 我要做的事情也非常简单, 我直接将传进来的reducers每个都走一遍
    for( const key in reducers ) {
      // 这里传值的时候要注意了, 当我们执行reducer的时候, 一定要记得是将
      // 属于他的状态传递进去, 而不是将整个状态传递
      // 比如: 总的状态是{ loginInfo: {}, userInfo: {} };
      // 我们reducers[key]可以拿到login的reducer, 这时候我们要传的就是
      // loginInfo的reducer
      lastState[key] = reducers[key](state[key], action);
    }

    // 这个时候lastReducers其实就是已经处理好之后的state对象了
    // 这里可能有些同学看不太明白, 其实我们知道action他是一个对象, 
    // 如果我传给第一个reducer, 他里面的reducer没有该action的type值
    // 那么他会直接返回默认值出来, 如果他有, 那么他会将对应的执行结果返回
    // 所以我让每个reducer都走一次, 这样就可以做到一个聚合的效果, 有的话
    // 就修改了, 没有的话会返回默认值, 这个一定要记得
    return lastState
  }
}

手写applyMiddleware

这个才是重头戏, applyMiddleware算是redux源码中比较复杂和绕的地方了, 在看这块的时候, 你必须确保你对redux中间件的运行机制(洋葱模型)摸清楚了, 不然你一定会绕不出来, 同时你还需要掌握一点函数组合(compose)的知识

在写applyMiddleware之前, 我们需要做一点准备工作, 之前不是留了一个compose文件, 我们先来把他搞定

compose文件中导出了一个方法, 该方法的基本功能如下:

  1. 接受多个函数作为参数
  2. 返回一个新的函数, 该函数执行的时候会将之前所有函数的功能进行组合, 并将最后的结果返回
// compose.js
const compose = (...funcs) => {
  if(!funcs || !funcs instanceof Array) return (...args) => args;
  return (...args) => funcs.reduce((fst, sec) => fst(sec(...arg));
}

// 这里我怕同学看不懂, 我再举个例子说明一下这个compose函数式干什么的

function add(n) {
  return n + 2;
}

function mult(n) {
  return n * n;
}

const func = compose(add, mult);

const result = func(3);

console.log("result", result); // 这里会输出11, 他会先把3给到mult函数执行, 然后将mult函数的执行结果给到add执行

export default compose;

ok, 有了上面的铺垫, 我们再来康康applyMiddleware都做了什么事:

  1. 接收不限个数的中间件处理函数(中间件处理函数会返回一个dispatch创建函数)作为参数,
  2. 返回一个新的函数enhancer, enhancer会接受一个createStore函数作为参数, enhancer函数执行完毕以后还会返回一个函数, 该函数就是真正用来初始化仓库的函数了
  3. 在初始化仓库函数的内部, 会执行createstore方法得到store, 同时通过调用中间件来修改store的dispatch方法, 将所有的中间件都注册完毕以后, 将经过处理后的dispatch返回出去

再次注意: 因为这是原理博客, 我就没有过多的去介绍关于redux中间件的规范, 以及整个redux的中间件模型这块的概念了, 这块务必是要掌握的, 否则你一定不知道我在说什么

// appliMiddleware.js
import compose from "./compose";

export default (...middlewares) => {
  return function enhanced(createStore) {
    return (reducer, defaultState) => {
      const store = createStore(reducer, defaultState);
      // 这个dispatch是我们最终要传递出去覆盖掉store的dispatch的函数
      // 为什么要覆盖哈: 因为我们需要将这个dispatch函数进行反复的中间件包装增强
      // 至于一开始这个函数是什么其实不重要, 你可以直接就写为空函数, 也可以等于store.dispatch
      // 只是官方写了报错函数, 所以我这里也跟着写了
      let dispatch = () => throw new Error("u can't use dispatch now");

      // 因为我们知道每个中间件处理函数执行以后都会返回一个dispatch创建函数的数组
      // 什么叫dispatch创建函数, 就是该函数执行以后还是会返回一个函数, 该函数就是完全符合dispatch的格式

      // 所以我们可以拿到目前所有中间件处理函数的dispatch创建函数
      const dispatchProducers = middlewares.map(el => el(store));

      // 这个时候我们就要进行compose组合了, 其实我们只需要拿到最后一个dispatch创建函数的返回结果
      // 最后一个dispatch创建函数创建出来的dispatch就是我们想要的经过了多重增幅的dispatch
      // 当我们经过compose组合以后会返回一个最终的dispatch创建函数, 然后我们将
      // store.dispatch作为参数给到这个dispatch创建函数, 他就可以帮我们生成一个dispatch
      dispatch = compose(dispatchProducers)(store.dispatch);

      return {
        ...store,
        dispatch
      }
    }
  }
}

到了这里, applyMiddleware就写完了, 但是可能很多同学还是不太明白, 我们来看看中间件的规范是什么样的

const middleware = (store) => (next) => (action) => {
  // store: 就是整个仓库的store, 这个store里的dispatch就是最原始的dispatch
  // 所以我们在上面使用map映射的时候要讲store传递进去, 他这样做的原因是因为
  // 假设你这个中间件很霸道, 完全不想去增强别人已经增强过的dispatch, 你可以选择增强
  // 最原始的dispatch(也就是store.dispatch), 这是为了给开发者更多灵活性

  // next: 这个next其实就是被上一个中间件处理过后的dispatch, 他为什么叫next
  // 你仔细想想, 中间件的compose组合是从最后一个开始包的, 但是执行的时候是从第一个开始执行的
  // a(b(args)), 你说b的下一个是不是a, 但是执行的时候一定是a先执行的, 他是这么个意思
  // 你在适当的时机可以将你的action交给下一个中间件继续进行包装, 当next没有的时候, next就是store.dispatch了, 这里很细节, 一定要好好看
  next(action); 
}

这个中间件模型一定要很清楚, 否则你这里绝壁看不懂

OK, 这时候我们之前还留了个尾巴, 我们需要去处理一下createStore中传递了enhancer的情况

// createStore

...
if( typeof enhancer === "function" ) {
  return enhancer(createStore)(reducer, defaultState); // 第一个参数是createStore, 然后第二个参数就是reducer和defaultState
}
...

好了, 至此我们已经将redux的四个核心功能都摸透了, 其实他还有一些东西, 但是都是边边角角的, 我这里就不提了, 希望这篇博客能够对你有帮助, 也希望可以得到大手子的指教, see you~