手写一个Redux深入理解其原理

7次阅读

共计 11630 个字符,预计需要花费 30 分钟才能阅读完成。

Redux 可是一个大名鼎鼎的库,很多地方都在用,我也用了几年了,今天这篇文章就是自己来实现一个 Redux,以便于深入理解他的原理。我们还是老套路,从基本的用法入手,然后自己实现一个 Redux 来替代源码的 NPM 包,但是功能保持不变。本文只会实现 Redux 的核心库,跟其他库的配合使用,比如 React-Redux 准备后面单独写一篇文章来讲。有时候我们过于关注使用,只记住了各种使用方式,反而忽略了他们的核心原理,但是如果我们想真正的提高技术,最好还是一个一个搞清楚,比如 Redux 和 React-Redux 看起来很像,但是他们的核心理念和关注点是不同的,Redux 其实只是一个单纯状态管理库,没有任何界面相关的东西,React-Redux 关注的是怎么将 Redux 跟 React 结合起来,用到了一些 React 的 API。

本文全部代码已经上传到 GitHub,大家可以拿下来玩下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux

基本概念

Redux 的概念有很多文章都讲过,想必大家都看过很多了,我这里不再展开,只是简单提一下。Redux 基本概念主要有以下几个:

Store

人如其名,Store 就是一个仓库,它存储了所有的状态 (State),还提供了一些操作他的 API,我们后续的操作其实都是在操作这个仓库。假如我们的仓库是用来放牛奶的,初始情况下,我们的仓库里面一箱牛奶都没有,那 Store 的状态(State) 就是:

{milk: 0}

Actions

一个 Action 就是一个动作,这个动作的目的是更改 Store 中的某个状态,Store 还是上面的那个仓库,现在我想往仓库放一箱牛奶,那 ” 我想往仓库放一箱牛奶 ” 就是一个 Action,代码就是这样:

{
  type: "PUT_MILK",
  count: 1
}

Reducers

前面 ” 我想往仓库放一箱牛奶 ” 只是想了,还没操作,具体操作要靠 Reducer,Reducer 就是根据接收的 Action 来改变 Store 中的状态,比如我接收了一个 PUT_MILK,同时数量count 是 1,那放进去的结果就是 milk 增加了 1,从 0 变成了 1,代码就是这样:

const initState = {milk: 0}

function reducer(state = initState, action) {switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk + action.count}
    default:
      return state
  }
}

可以看到 Redux 本身就是一个单纯的状态机,Store 存放了所有的状态,Action 是一个改变状态的通知,Reducer 接收到通知就更改 Store 中对应的状态。

简单例子

下面我们来看一个简单的例子,包含了前面提到的 Store,Action 和 Reducer 这几个概念:

import {createStore} from 'redux';

const initState = {milk: 0};

function reducer(state = initState, action) {switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk + action.count};
    case 'TAKE_MILK':
      return {...state, milk: state.milk - action.count};
    default:
      return state;
  }
}

let store = createStore(reducer);

// subscribe 其实就是订阅 store 的变化,一旦 store 发生了变化,传入的回调函数就会被调用
// 如果是结合页面更新,更新的操作就是在这里执行
store.subscribe(() => console.log(store.getState()));

// 将 action 发出去要用 dispatch
store.dispatch({type: 'PUT_MILK'});    // milk: 1
store.dispatch({type: 'PUT_MILK'});    // milk: 2
store.dispatch({type: 'TAKE_MILK'});   // milk: 1

自己实现

前面我们那个例子虽然短小,但是已经包含了 Redux 的核心功能了,所以我们手写的第一个目标就是替换这个例子中的 Redux。要替换这个 Redux,我们得先知道他里面都有什么东西,仔细一看,我们好像只用到了他的一个 API:

createStore:这个 API 接受 reducer 方法作为参数,返回一个 store,主要功能都在这个store 上。

看看 store 上我们都用到了啥:

store.subscribe: 订阅 state 的变化,当 state 变化的时候执行回调,可以有多个subscribe,里面的回调会依次执行。

store.dispatch: 发出 action 的方法,每次 dispatch action 都会执行 reducer 生成新的 state,然后执行subscribe 注册的回调。

store.getState: 一个简单的方法,返回当前的state

看到 subscribe 注册回调,dispatch触发回调,想到了什么,这不就是发布订阅模式吗?我之前有一篇文章详细讲过发布订阅模式了,这里直接仿写一个。

function createStore() {
  let state;              // state 记录所有状态
  let listeners = [];     // 保存所有注册的回调

  function subscribe(callback) {listeners.push(callback);       // subscribe 就是将回调保存下来
  }

  // dispatch 就是将所有的回调拿出来依次执行就行
  function dispatch() {for (let i = 0; i < listeners.length; i++) {const listener = listeners[i];
      listener();}
  }

  // getState 直接返回 state
  function getState() {return state;}

  // store 包装一下前面的方法直接返回
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}

上述代码是不是很简单嘛,Redux 核心也是一个发布订阅模式,就是这么简单!等等,好像漏了啥,reducer呢?reducer的作用是在发布事件的时候改变 state,所以我们的dispatch 在执行回调前应该先执行 reducer, 用reducer 的返回值重新给 state 赋值,dispatch改写如下:

function dispatch(action) {state = reducer(state, action);

  for (let i = 0; i < listeners.length; i++) {const listener = listeners[i];
    listener();}
}

到这里,前面例子用到的所有 API 我们都自己实现了,我们用自己的 Redux 来替换下官方的 Redux 试试:

// import {createStore} from 'redux';
import {createStore} from './myRedux';

可以看到输出结果是一样的,说明我们自己写的 Redux 没有问题:

了解了 Redux 的核心原理,我们再去看他的源码应该就没有问题了,createStore 的源码传送门。

最后我们再来梳理下 Redux 的核心流程,注意单纯的 Redux 只是个状态机,是没有 View 层的哦。

除了这个核心逻辑外,Redux 里面还有些 API 也很有意思,我们也来手写下。

手写combineReducers

combineReducers也是使用非常广泛的 API,当我们应用越来越复杂,如果将所有逻辑都写在一个 reducer 里面,最终这个文件可能会有成千上万行,所以 Redux 提供了combineReducers,可以让我们为不同的模块写自己的reducer,最终将他们组合起来。比如我们最开始那个牛奶仓库,由于我们的业务发展很好,我们又增加了一个放大米的仓库,我们可以为这两个仓库创建自己的reducer,然后将他们组合起来,使用方法如下:

import {createStore, combineReducers} from 'redux';

const initMilkState = {milk: 0};
function milkReducer(state = initMilkState, action) {switch (action.type) {
    case 'PUT_MILK':
      return {...state, milk: state.milk + action.count};
    case 'TAKE_MILK':
      return {...state, milk: state.milk - action.count};
    default:
      return state;
  }
}

const initRiceState = {rice: 0};
function riceReducer(state = initRiceState, action) {switch (action.type) {
    case 'PUT_RICE':
      return {...state, rice: state.rice + action.count};
    case 'TAKE_RICE':
      return {...state, rice: state.rice - action.count};
    default:
      return state;
  }
}

// 使用 combineReducers 组合两个 reducer
const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer});

let store = createStore(reducer);

store.subscribe(() => console.log(store.getState()));

// 操作???? 的 action
store.dispatch({type: 'PUT_MILK', count: 1});    // milk: 1
store.dispatch({type: 'PUT_MILK', count: 1});    // milk: 2
store.dispatch({type: 'TAKE_MILK', count: 1});   // milk: 1

// 操作大米的 action
store.dispatch({type: 'PUT_RICE', count: 1});    // rice: 1
store.dispatch({type: 'PUT_RICE', count: 1});    // rice: 2
store.dispatch({type: 'TAKE_RICE', count: 1});   // rice: 1

上面代码我们将大的 state 分成了两个小的 milkStatericeState,最终运行结果如下:

知道了用法,我们尝试自己来写下呢!要手写 combineReducers,我们先来分析下他干了啥,首先它的返回值是一个reducer,这个reducer 同样会作为 createStore 的参数传进去,说明这个返回值是一个跟我们之前普通 reducer 结构一样的函数。这个函数同样接收 stateaction然后返回新的 state,只是这个新的state 要符合 combineReducers 参数的数据结构。我们尝试来写下:

function combineReducers(reducerMap) {const reducerKeys = Object.keys(reducerMap);    // 先把参数里面所有的键值拿出来
  
  // 返回值是一个普通结构的 reducer 函数
  const reducer = (state = {}, action) => {const newState = {};
    
    for(let i = 0; i < reducerKeys.length; i++) {
      // reducerMap 里面每个键的值都是一个 reducer,我们把它拿出来运行下就可以得到对应键新的 state 值
      // 然后将所有 reducer 返回的 state 按照参数里面的 key 组装好
      // 最后再返回组装好的 newState 就行
      const key = reducerKeys[i];
      const currentReducer = reducerMap[key];
      const prevState = state[key];
      newState[key] = currentReducer(prevState, action);
    }
    
    return newState;
  };
  
  return reducer;
}

官方源码的实现原理跟我们的一样,只是他有更多的错误处理,大家可以对照着看下。

手写applyMiddleware

middleware是 Redux 里面很重要的一个概念,Redux 的生态主要靠这个 API 接入,比如我们想写一个 logger 的中间件可以这样写(这个中间件来自于官方文档):

// logger 是一个中间件,注意返回值嵌了好几层函数
// 我们后面来看看为什么这么设计
function logger(store) {return function(next) {return function(action) {console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

// 在 createStore 的时候将 applyMiddleware 作为第二个参数传进去
const store = createStore(
  reducer,
  applyMiddleware(logger)
)

可以看到上述代码为了支持中间件,createStore支持了第二个参数,这个参数官方称为 enhancer,顾名思义他是一个增强器,用来增强store 的能力的。官方对于 enhancer 的定义如下:

type StoreEnhancer = (next: StoreCreator) => StoreCreator

上面的结构的意思是说 enhancer 作为一个函数,他接收 StoreCreator 函数作为参数,同时返回的也必须是一个 StoreCreator 函数。注意他的返回值也是一个 StoreCreator 函数,也就是我们把他的返回值拿出来继续执行应该得到跟之前的 createStore 一样的返回结构,也就是说我们之前的 createStore 返回啥结构,他也必须返回结构,也就是这个store

{
  subscribe,
  dispatch,
  getState
}

createStore支持enhancer

根据他关于 enhancer 的定义,我们来改写下自己的createStore,让他支持enhancer

function createStore(reducer, enhancer) {   // 接收第二个参数 enhancer
  // 先处理 enhancer
  // 如果 enhancer 存在并且是函数
  // 我们将 createStore 作为参数传给他
  // 他应该返回一个新的 createStore 给我
  // 我再拿这个新的 createStore 执行,应该得到一个 store
  // 直接返回这个 store 就行
  if(enhancer && typeof enhancer === 'function'){const newCreateStore = enhancer(createStore);
    const newStore = newCreateStore(reducer);
    return newStore;
  }
  
  // 如果没有 enhancer 或者 enhancer 不是函数,直接执行之前的逻辑
  // 下面这些代码都是之前那版
  // 省略 n 行代码
    // .......
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}

这部分对应的源码看这里。

applyMiddleware返回值是一个enhancer

前面我们已经有了 enhancer 的基本结构,applyMiddleware是作为第二个参数传给 createStore 的,也就是说他是一个 enhancer,准确的说是applyMiddleware 的返回值是一个 enhancer,因为我们传给createStore 的是他的执行结果applyMiddleware()

function applyMiddleware(middleware) {
  // applyMiddleware 的返回值应该是一个 enhancer
  // 按照我们前面说的 enhancer 的参数是 createStore
  function enhancer(createStore) {
    // enhancer 应该返回一个新的 createStore
    function newCreateStore(reducer) {
      // 我们先写个空的 newCreateStore,直接返回 createStore 的结果
      const store = createStore(reducer);
      return store
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}

实现applyMiddleware

上面我们已经有了 applyMiddleware 的基本结构了,但是功能还没实现,要实现他的功能,我们必须先搞清楚一个中间件到底有什么功能,还是以前面的 logger 中间件为例:

function logger(store) {return function(next) {return function(action) {console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

这个中间件运行效果如下:

可以看到我们 let result = next(action); 这行执行之后 state 改变了,前面我们说了要改变 state 只能 dispatch(action),所以这里的next(action) 就是 dispatch(action),只是换了一个名字而已。而且注意最后一层返回值return function(action) 的结构,他的参数是 action,是不是很像dispatch(action),其实他就是一个新的dispatch(action),这个新的dispatch(action) 会调用原始的dispatch,并且在调用的前后加上自己的逻辑。所以到这里一个中间件的结构也清楚了:

  1. 一个中间件接收 store 作为参数,会返回一个函数
  2. 返回的这个函数接收老的 dispatch 函数作为参数,会返回一个新的函数
  3. 返回的新函数就是新的 dispatch 函数,这个函数里面可以拿到外面两层传进来的 store 和老 dispatch 函数

所以说白了,中间件就是加强 dispatch 的功能,用新的 dispatch 替换老的 dispatch,这不就是个装饰者模式吗?其实前面enhancer 也是一个装饰者模式,传入一个 createStore,在createStore 执行前后加上些代码,最后又返回一个增强版的createStore。可见设计模式在这些优秀的框架中还真是广泛存在,如果你对装饰者模式还不太熟悉,可以看我之前这篇文章。

遵循这个思路,我们的 applyMiddleware 就可以写出来了:

// 直接把前面的结构拿过来
function applyMiddleware(middleware) {function enhancer(createStore) {function newCreateStore(reducer) {const store = createStore(reducer);
      
      // 将 middleware 拿过来执行下,传入 store
      // 得到第一层函数
      const func = middleware(store);
      
      // 解构出原始的 dispatch
      const {dispatch} = store;
      
      // 将原始的 dispatch 函数传给 func 执行
      // 得到增强版的 dispatch
      const newDispatch = func(dispatch);
      
      // 返回的时候用增强版的 newDispatch 替换原始的 dispatch
      return {...store, dispatch: newDispatch}
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}

照例用我们自己的 applyMiddleware 替换老的,跑起来是一样的效果,说明我们写的没问题,哈哈~

支持多个middleware

我们的 applyMiddleware 还差一个功能,就是支持多个middleware,比如像这样:

applyMiddleware(
  rafScheduler,
  timeoutScheduler,
  thunk,
  vanillaPromise,
  readyStatePromise,
  logger,
  crashReporter
)

其实要支持这个也简单,我们返回的 newDispatch 里面依次的将传入的 middleware 拿出来执行就行,多个函数的串行执行可以使用辅助函数 compose,这个函数定义如下。只是需要注意的是我们这里的compose 不能把方法拿来执行就完了,应该返回一个包裹了所有方法的方法。

function compose(...func){return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

这个 compose 可能比较让人困惑,我这里还是讲解下,比如我们有三个函数,这三个函数都是我们前面接收 dispatch 返回新 dispatch 的方法:

const fun1 = dispatch => newDispatch1;
const fun2 = dispatch => newDispatch2;
const fun3 = dispatch => newDispatch3;

当我们使用了 compose(fun1, fun2, fun3) 后执行顺序是什么样的呢?

// 第一次其实执行的是
(func1, func2) => (...args) => func1(fun2(...args))
// 这次执行完的返回值是下面这个,用个变量存起来吧
const temp = (...args) => func1(fun2(...args))

// 我们下次再循环的时候其实执行的是
(temp, func3) => (...args) => temp(func3(...args));
// 这个返回值是下面这个,也就是最终的返回值,其实就是从 func3 开始从右往左执行完了所有函数
// 前面的返回值会作为后面参数
(...args) => temp(func3(...args));

// 再看看上面这个方法,如果把 dispatch 作为参数传进去会是什么效果
(dispatch) => temp(func3(dispatch));

// 然后 func3(dispatch)返回的是 newDispatch3,这个又传给了 temp(newDispatch3),也就是下面这个会执行
(newDispatch3) => func1(fun2(newDispatch3))

// 上面这个里面用 newDispatch3 执行 fun2(newDispatch3)会得到 newDispatch2
// 然后 func1(newDispatch2)会得到 newDispatch1
// 注意这时候的 newDispatch1 其实已经包含了 newDispatch3 和 newDispatch2 的逻辑了,将它拿出来执行这三个方法就都执行了

更多关于 compose 原理的细节可以看我之前这篇文章。

所以我们支持多个 middleware 的代码就是这样:

// 参数支持多个中间件
function applyMiddleware(...middlewares) {function enhancer(createStore) {function newCreateStore(reducer) {const store = createStore(reducer);
      
      // 多个 middleware,先解构出 dispatch => newDispatch 的结构
      const chain = middlewares.map(middleware => middleware(store));
      const {dispatch} = store;
      
      // 用 compose 得到一个组合了所有 newDispatch 的函数
      const newDispatchGen = compose(...chain);
      // 执行这个函数得到 newDispatch
      const newDispatch = newDispatchGen(dispatch);

      return {...store, dispatch: newDispatch}
    }
    
    return newCreateStore;
  }
  
  return enhancer;
}

最后我们再加一个 logger2 中间件实现效果:

function logger2(store) {return function(next) {return function(action) {let result = next(action);
      console.log('logger2');
      return result
    }
  }
}

let store = createStore(reducer, applyMiddleware(logger, logger2));

可以看到 logger2 也已经打印出来了,大功告成。

现在我们也可以知道他的中间件为什么要包裹几层函数了:

第一层:目的是传入 store 参数

第二层:第二层的结构是 dispatch => newDispatch,多个中间件的这层函数可以compose 起来,形成一个大的dispatch => newDispatch

第三层:这层就是最终的返回值了,其实就是newDispatch,是增强过的dispatch,是中间件的真正逻辑所在。

到这里我们的 applyMiddleware 就写完了,对应的源码可以看这里,相信看了本文再去看源码就没啥问题了!

本文所有代码已经传到 GitHub,大家可以去拿下来玩一下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux

总结

  1. 单纯的 Redux 只是一个状态机,store里面存了所有的状态state,要改变里面的状态state,只能dispatch action
  2. 对于发出来的 action 需要用 reducer 来处理,reducer会计算新的 state 来替代老的state
  3. subscribe方法可以注册回调方法,当 dispatch action 的时候会执行里面的回调。
  4. Redux 其实就是一个发布订阅模式!
  5. Redux 还支持 enhancerenhancer 其实就是一个装饰者模式,传入当前的createStore,返回一个增强的createStore
  6. Redux 使用 applyMiddleware 支持中间件,applyMiddleware的返回值其实就是一个enhancer
  7. Redux 的中间件也是一个装饰者模式,传入当前的dispatch,返回一个增强了的dispatch
  8. 单纯的 Redux 是没有 View 层的,所以他可以跟各种 UI 库结合使用,比如react-redux,计划下一篇文章就是手写react-redux

参考资料

官方文档:https://redux.js.org/

GitHub 源码:https://github.com/reduxjs/redux

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和 GitHub 小星星,你的支持是作者持续创作的动力。

作者博文 GitHub 项目地址:https://github.com/dennis-jiang/Front-End-Knowledges

作者掘金文章汇总:https://juejin.im/post/5e3ffc85518825494e2772fd

正文完
 0