手写一个Redux,深刻理解其原理

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

本文所有代码已经上传到GitHub,你们能够拿下来玩下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux前端

基本概念

Redux的概念有不少文章都讲过,想必你们都看过不少了,我这里再也不展开,只是简单提一下。Redux基本概念主要有如下几个:java

Store

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

{
    milk: 0
}

Actions

一个Action就是一个动做,这个动做的目的是更改Store中的某个状态,Store仍是上面的那个仓库,如今我想往仓库放一箱牛奶,那"我想往仓库放一箱牛奶"就是一个Action,代码就是这样:git

{
  type: "PUT_MILK",
  count: 1
}

 Reducers

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

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中对应的状态。json

简单例子

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

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上。app

看看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没有问题:

image-20200630152344176

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

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

 image-20200630154356840

除了这个核心逻辑外,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,最终运行结果以下:

image-20200630162957760

知道了用法,咱们尝试本身来写下呢!要手写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
    }
  }
}

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

image-20200701160700945

能够看到咱们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替换老的,跑起来是同样的效果,说明咱们写的没问题,哈哈~

image-20200701162841414

支持多个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的逻辑了,将它拿出来执行这三个方法就都执行了

因此咱们支持多个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也已经打印出来了,大功告成。

image-20200701173615349

如今咱们也能够知道他的中间件为何要包裹几层函数了:

第一层:目的是传入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小星星,你的支持是做者持续创做的动力。

“前端进阶知识”系列文章及示例源码: https://github.com/dennis-jiang/Front-End-Knowledges

欢迎关注个人公众号进击的大前端第一时间获取高质量原创~

QR1270

相关文章
相关标签/搜索