Redux 不完全实现

对于框架/库,一般倾向于去解析源码理解其实现,但假如框架/库不存在呢?那么我们是不是该有足够的脑洞去开创、去推演、去实现。

开发者应该尽量把自己的思维往这个方向去培养...

文章尝试性的反推一下Redux的实现。

目标介绍:Redux

要写Redux,是写个什么样的东西?

官方描述:A predictable state container for JavaScript apps.

翻译一下:一个JavaScript 应用程序中可预测的状态容器。

通过这句话,挖掘一些关键点:

  • 1. 要有个状态:state

  • 2. 要有个(状态)容器:store

  • 3. 在状态改变的流程中,状态是可预测的,即:

    • 3.1 何时触发状态进行改变? dispatch,触发 state 的改变

    • 3.2 触发了什么改变? action,描述 state 该做什么修改

    • 3.3 状态做什么样改变? reducer,描述 state 的改变过程,传入改变前的 state,返回改变后的 state

reducer 是个纯函数,很重要,能够消除其他影响让 state 的变化真正是可预测的。

有了这些关键点,接下来就是实现。

Redux实现

被控对象(state)被包含在整个链路中,我们关心链路即可。

首先,是实现一个容器。在js里,对象、数组、字符串,只要是数据,确实都可以看做容器,但...如果需要这个容器支持监听,支持有get/set规范、并且存储值的话,闭包(函数)是个不错的选择。

/*
 * createStore 状态容器
 * @param reducers 容器总得需要知道是做什么样的改变
 * @param initialState 初始化(预置)的 state
 */

const createStore = (reducers, initialState) => {
  // 通过 reducer 取到改变后的值,重新存储
  let currentReducer = reducers;

  // 存
  let currentState = initialState;

  // 取
  const getState = () => {
    return currentState;
  };

  // 改
  const dispatch = action => {
    currentState = currentReducer(currentState, action);
    return action;
  };

  // 这里可能还需要可被观察的,留坑、不实现,有兴趣的看文章后的源码阅读链接
  return {
    getState,
    dispatch
  };
};

action 决定 state 该做什么修改,仅是个对象,定义好格式,如下例子(比如数字的重置)

/*
 * action
 * @property type 描述需要做什么操作
 * @property preload 预加载数据,包含 state 的新值
 */

const RESET = "RESET";
const RESET_ACTION = {
  type: RESET,
  preload: {
    count: 0
  }
};

reducer 描述状态的变化过程

/*
 * reducer
 * @currentState 当前的 state,旧值
 * @action 该做什么修改的类型描述
 */

const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case RESET: {
      return {
        ...state,
        ...action.preload
      };
    }
    default: {
      return state;
    }
  }
};

将上面三份代码合起来,在浏览器试一试

const store = createStore(reducer);
store.dispatch({ type: RESET, preload: { count: 10 } });
store.getState(); // output { count: 10}
store.dispatch(RESET_ACTION);
store.getState(); // output { count: 0}

Redux 雏形已具备。

但稍微有点不对,如果在 dispatch 之前去 getState,那么 state 是 {},而不是给的初始值{count:0}。

对于这点,只要在 createStore 时候的默认执行一次 dispatch,用以生成初始的 state tree。在 createStore 的 return 之前加入 dispatch:

dispatch({ type: "@redux/INIT" });

ok,至此已经有个简版 Redux 了(observable 和 subscribe 的话,加个 listeners就可以,和state的存储原理上是一样的,这里不多做介绍),但是 Redux 还有 middleware 的功能(而且这部分代码会比这里好玩一点),继续实现 middleware。

Middleware 实现

和上面一样,先明确middleware是什么?要做什么?

官方描述:

Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store's dispatch method for fun and profit. The key feature of middleware is that it is composable. Multiple middleware can be combined together, where each middleware requires no knowledge of what comes before or after it in the chain.

翻译一下:

Middleware 是通过自定义功能来扩展 redux 的推荐方法,它能够让你有效的包裹 store 的 dispatch 方法已达到所需的目的,其关键特征在于组合,多个 middleware 能够进行组合,每个 middleware 都是独立的,它们不需要知道在流程的之前或之后会发生什么。

从上面的描述中,得出结论:

首先,middleware 要是个 function 且该函数对 dispatch 的执行做包裹;

然后,每个 middleware 互不相干且可组合的;

最后,值得注意的是 middleware 内部能够访问及操作 state,不然只能做些和 state 不相干的事情,那这个扩展的意义就不大了。

来推演一下实现的过程

首先,我们需要考虑的是怎么处理每个函数,让其既是独立的,又是可组合的,而且内部还得包裹 dispatch。思考一下🤔🤔🤔

比如我有个函数 a 和函数 dispatch 我们希望执行的过程是 dispatch 被包裹在 a 内部执行,首先想到的肯定是 callback 形式,没毛病,看代码:

var a = function(next) {
  console.log("a-before");
  next();
  console.log("a-after");
};
var dispatch = function(action) {
  console.log("do ", action);
  return action;
};

a(dispatch);
// output:
// a-before
// do undefined
// a-after

但是没有能够把 dispatch 的参数传进去呀,于是对于内部函数的传参,不陌生的,我们又想到,外包再套一层 function,闭包存参(用 jqyery 的时候绑定事件没少这么干吧),看代码:

var a = function(next) {
  return function(action) {
    console.log("a-before");
    next(action);
    console.log("a-after");
  };
};
var dispatch = function(action) {
  console.log("do ", action);
  return action;
};

a(dispatch)("test action");
// output:
// a-before
// do test action
// a-after

但如果 a 的这种类型的包裹函数是多个的,试下加个函数 b,因为要嵌套的是函数,所以将 action 作为第二次执行的参数,看代码:

var a = function(next) {
  return function(action) {
    console.log("a-before");
    next(action);
    console.log("a-after");
  };
};
var b = function(next) {
  return function(action) {
    console.log("b-before");
    next(action);
    console.log("b-after");
  };
};
var dispatch = function(action) {
  console.log("do ", action);
  return action;
};

a(b(dispatch))("test action");
// output:
// a-before
// b-before
// do test action
// b-after
// a-after

然后问题又来了 ,我要再加个函数 c,难道让我写 a(b(c(dispacth)))(action)?

既然函数 a, b, c 都是一种类型的东西,可以格式化成数组,回想一下什么方法能够依次组合数组的每一项。没错,是 reduce,继续看代码:

var a = function(next) {
  return function(action) {
    console.log("a-before");
    next(action);
    console.log("a-after");
  };
};
var b = function(next) {
  return function(action) {
    console.log("b-before");
    next(action);
    console.log("c-after");
  };
};
var c = function(next) {
  return function(action) {
    console.log("c-before");
    next(action);
    console.log("c-after");
  };
};
var dispatch = function(action) {
  console.log("do ", action);
  return action;
};

var d = [a, b, c].reduce((pre, now) => (...args) => pre(now(...args)));

d(dispatch)("test action");
// output:
// a-before
// b-before
// c-before
// do test action
// c-after
// b-after
// a-after

好了,想到了如何将 middleware 串起来和如何将 dispatch 封装的方法后,集成到 redux 的代码里试试。

先单独抽一个 compose 函数用以处理一个或多个 middleware,代码如下:

// compose.js
const compose = (...funcs) => {
  if (funcs.length === 0) {
    return arg => arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)));
};

修改 createStore 里的 dispatch 方法,让其支持当存在 middleware 的时候,dispatch 需要被重写。代码如下:

/*
 * createStore 状态容器
 * @param reducers 容器总得需要知道是做什么样的改变
 * @param initialState 初始化(预置)的 state
 * @param enhancer 扩展的 middlewares
 */

const createStore = (reducers, initialState, enhancer) => {
  // 参数互换 如果 initialState 是个函数,enhancer = undefined 则 enhancer 和 initialState 互换
  if (typeof initialState === "function" && typeof enhancer === "undefined") {
    enhancer = initialState;
    initialState = undefined;
  }

  // 如果有 middleware 的时候,则 createStore 稍后处理,处理详情参照 applyMiddleware 函数
  if (typeof enhancer !== "undefined" && typeof enhancer === "function") {
    // 为什么是这样写? 继续往下看
    return enhancer(createStore)(reducer, initialState);
  }

  // ...
  // 之前的代码
};

结合 createStore,注意到前文提出的,middleware 内部支持访问和操作 state,然后需要实现 createStore 里面的 enhancer 函数,就是函数 applyMiddleware,于是给出代码:

/*
 * applyMiddleware 实现中间件的应用
 * @param ...middlewares 插入的 state 处理流程的中间件
 */
const applyMiddleware = (...middlewares) => {
  // 传入 middlewares
  return createStore => (...args) => {
    const store = createStore(...args);
    // middleware 内部能做的 state 操作
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    };
    // 将 middleware 处理,以 middlewareAPI 作为参数执行并且取到 middleware 的内部函数
    const chain = middlewares.map(middleware => middleware(middlewareAPI));
    // 进行 compose 组合
    // 如存在 3 个 middleware A(ABefore,AAfter) B(BBefore,BAfter) C(CBefore,CAfter)
    // 则执行顺序是 ABefore - BBefore - CBefore - (真实的操作) - CAfter - BAfter - AAfter
    dispatch = compose(...chain)(store.dispatch);

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

代码写完,测试一下?

需要在 createStore 的 dispatch 里加个 "do dispatch" 的 log,方便看执行流程。

再写个middleware函数测试,看代码:

const store = createStore(reducer, applyMiddleware(logger));

const logger = ({ getState }) => {
  return next => action => {
    console.log("will dispatch", action);

    const returnValue = next(action);

    console.log("state after dispatch", getState());

    return returnValue;
  };
};
store.dispatch(RESET_ACTION);
// output
// will dispatch {type: "RESET", preload: {…}}
// do dispatch
// state after dispatch {count: 0}

一个完整的 Redux 也不止这些代码(但也没多少其它的了),还有一些断言、错误提示、开发提示、bindActionCreators 等...

实现的过程更多在于"容器操作的规范",技术方面好像确实都是一些 js 基础知识的运用。

希望通过对Redux的理解,结合对Redux的应用,产生更多的思考:

状态管理模式和业务场景的适用?

减少Redux使用过程中的样板代码?

...

或者更多?

最后,呼应下头图,一群人聚着干啥?当然是搞事啦!!! 招前端,3年起步~ 有意愿的BOSS联系,链接点这里