从零实现redux,手摸手实现一个自己的redux中间件!

669 阅读9分钟

本文会带着从零实现redux各个细节,最后也会教你如何去编写一个redux的中间件(middleware)

什么是聚合?

在学习redux之前我们得先准备一些前置知识。我先提一个很多人可能都碰到过的问题,给定三个方法,如下所示要将函数依次执行,该怎么做:

const f1 = (arg) => {
  console.log('f1:' + arg);
  return arg
}

const f2 = (arg) => {
  console.log('f2:' + arg);
  return arg
}

const f3 = (arg) => {
  console.log('f3:' + arg);
  return arg
}

这个时候肯定就有人说了,这个还不简单啊直接f1('haha')f2('haha')f3('haha')亦或者f3(f2(f1('haha'))),是的,这两个方法确实能解决这个问题,但是如果有很多函数需要让你依次执行,或者是一个未知数量呢?

如果我们想要更优雅的解决这个问题,这个时候就需要引出一个函数式编程的经典概念:compose。

如果我们可以实现一个函数可以这样去解决问题,是不是就会好维护很多?

compose(f1, f2, f3)('haha')

那么我们该怎么编写这个函数呢,这里就要用到闭包和Aarray.prototype.recude这个方法,解决JavaScript的语言特性,我们可以很容易的实现compose。

const compose = (...fnArr) => fnArr.reduce((pre, cur) => (...args) => cur(pre(...args)))

// 或者

const compose = (...fnArr) => arg => fnArr.reduce((pre, cur) => cur(pre), arg)

redux举例

那么我们这个例子和redux又有什么关系呢,众所周知store有三大核心,就是action、reducer、store。其中的reducer就和我们用到的Array.prototype.reduce(reducer, initialValue)很像,reducer就是一个纯函数,接受旧的state和action,返回新的state。

我们先来简单用redux做一个例子吧:

// src/store/index.ts

import { createStore } from 'redux'

type action = {
  type: string,
  payload: number
}

export const counterReducer = (state = 0, { type, payload }: action):number => {
  switch(type) {
    case 'ADD':
      return state + payload
    case 'MINUS':
      return state - payload
    default: 
      return state
  }
}

const store = createStore(counterReducer)

export default store
import { useEffect, useState } from 'react';
import store from '../../store';

export default function Blog() {

  const [count, setCount] = useState(store.getState())

  useEffect(() => {
    const unSubscribe = store.subscribe(() => {
      setCount(store.getState())
    })

    return () => {
      unSubscribe()
    }
  }, [])

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => store.dispatch({type: 'ADD', payload: 100})}>add</button>
    </div>
  )
}

这是一个非常简单的累加器的例子,我们使用redux去对count进行缓存累加。

实现一个自己的redux.createStore

如果让我们去按照自己的案例去实现一个自己的redux,那么我们该怎么做呢?首先我们可以从使用方法里发现,调用createStore方法之后返回了一个对象,对象中包含了三个方法,分别是getState、dispatch和subscribe。那么我们不妨先按照这个想法去简单实现一下这个,先新建一个目录myRedux,然后在目录下创建两个文件分别为:createStore.js、index.js。

image.png

在createStore.js中写入这几个方法:

export default function createStore(reducer) {

  return {
    getState,
    dispatch,
    subscribe
  }
}

因为我们一般都是从index文件导入,所以要在index文件中加入:

import createStore from './createStore'

export {createStore}

我们来仔细想一下,getState方法是用来获取当前store中的state的值,而dispatch则是传入一个action参数帮助我们设置新的state值并且让所有订阅过store的方法都执行一遍,最后subscribe是帮助我们订阅这个更新,所以我们要创建一个当前的State去存储每一次的state,以及一个订阅事件的队列去管理所有的订阅事件:

// src/myRedux/createStore.js

export default function createStore(reducer) {

  // 开辟一个空间来存储状态
  let currentState,
      currentListeners = []

  // get
  function getState() {
    return currentState
  }

  // 设置currentState并且发布所订阅的函数
  function dispatch(action) {
    // 先修改storeState
    currentState = reducer(currentState, action)
    // store改变,执行订阅函数
    currentListeners.forEach(listener => listener())
  }

  // 订阅
  function subscribe(listener) {
    currentListeners.push(listener)
  }

  return {
    getState,
    dispatch,
    subscribe
  }
}

我们可以尝试把src/store/index.js中的import { createStore } from 'redux'替换为import { createStore } from '../myRedux/index',我们就可以用新写的myRedux代替之前的redux使用了,你以为我们的myRedux已经写完了吗?错!相信有细心的小伙伴已经发现了,诶,它的初始值呢,它初始值去哪儿了?多捞哦,它竟然没有初始值!其实想要有初始值也很简单,我们只需要在createStore函数最末尾加上一个dispatch({type: '初始值'})即可,这里的'初始值'是我随意写的,源码里应该为一个随机的字符串。

这下我们的myRedux总算是大功告成了吧?还是错!我们还有功能没有写完,有订阅就得要有取消订阅嘛,所以我们要在subscribe函数里返回一个函数去取消订阅,是不是很有道理,帅的嘛就不谈,下面是完整版:

export default function createStore(reducer) {

  // 开辟一个空间来存储状态
  let currentState,
      currentListeners = []

  // get
  function getState() {
    return currentState
  }

  // 设置currentState并且发布所订阅的函数
  function dispatch(action) {
    // 先修改storeState
    currentState = reducer(currentState, action)
    // store改变,执行订阅函数
    currentListeners.forEach(listener => listener())
  }

  // 订阅
  function subscribe(listener) {
    currentListeners.push(listener)
    return () => {
      currentListeners.splice(currentListeners.indexOf(listener), 1)
    }
  }

  dispatch({type: '我是初始值'})

  return {
    getState,
    dispatch,
    subscribe
  }
}

这就很灵性嘛,一个满足我们前面例子的redux简版就出来了。

redux中间件applyMiddleware & middleware

Redux只是个纯粹的状态管理器,默认是只支持同步的,实现异步任务比如延迟、网络请求就需要有中间件的支持,比如我们最简单的redux-thunk和redux-logger。

中间件就是一个函数,对store.dispatch方法进行改造,在发出Action和执行Reducer这两步之间,添加了其他功能。

当我们使用redux-thunk去解决redux异步问题的时候,我们可以这样写:

const store = createStore(counterReducer, applyMiddleware(thunk))

下面让我们来实现一下吧,首先我们得修改一下createStore函数:

export default function createStore(reducer, enhancer) { // enhancer即为applyMiddleware(thunk)
  // enhancer是加强dispatch
  if (enhancer) {
    return enhancer(createStore)(reducer);
  }
  
  // ...
  
 }

我们去myRedux目录下新建一个applyMiddleware.js文件,因为我们上面用了函数柯里化,所以我们applyMiddleware函数内部也要柯里化一下:

export default (...middlewares) => {
  return (createStore) => (reducer) => {
}

上面第一个参数middlewares即为传入applyMiddleware的一系列中间件比如thunk、logger之类。

我们首先得明白一个点,就是redux的中间件是干嘛的,我认为它是用来加强原本的dispatch,让它可以支持新的功能比如thunk的异步,promise或者logger打印日志。这其实是一个常见的设计模式,装饰器模式。由于可能同时存在很多种的middleware,所以我们的核心原则就是要把每个加强后的dispatch传递给下一个middleware。

接下来让我们来完善这个函数吧。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer) => {
    const store = createStore(reducer);
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    // todo 加强dispatch的
    // ! 达到的目的就是,组件里执行dispatch的时候要执行中间件函数和store.dispatch
    // 原先的dispatch只能处理plain objects, 加强之后要能处理函数、promise等

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args),
    };
    const middlewaresChain = middlewares.map((middleware) =>
      middleware(middlewareAPI)
    );

    // middlewaresChain中间件数组
    // 按照顺序执行中间件函数
    // 加强版的dispatch
    dispatch = compose(...middlewaresChain)(store.dispatch);

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

function 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)));
}

在上面的代码中,我们先得到原本的store,然后定义一个dispatch函数,这个函数是为了后面避免修改到原本的store.dispatch,然后我们定义一个middlewareAPI去提供一个方便给middleware访问的虚拟store对象,然后我们再将新的middlewareAPI传入每个middleware去进行保存,因为需要一个接一个的传下去,所以需要把所有的minddleware给串起来,这是不是就和我们一开始的那道题很像,所以我们需要用到一个compose函数,并且把原本的store.dispatch当参数传入进去给存起来,至于为什么这么做,后面我实现一个thunk中间件的时候就知道了。然后再将结果赋值给我们定义的dispatch函数,再返回出去,这样就形成了一个加强版的dispatch。

下面我们来实现一个thunk中间件和一个logger中间件:

// redux-thunk

export default ({ getState, dispatch }) => next => action => {
  if (typeof action === "function") {
    return action(dispatch, getState);
  }
  return next(action);
};

// redux-logger

export default ({ getState, dispatch }) => next => action => {
  console.log("---------------------------"); //sy-log
  console.log(action.type + "执行了!"); //sy-log

  const prevState = getState();

  console.log("prev state", prevState); //sy-log

  const returnValue = next(action);

  const nextState = getState();
  console.log("next state", nextState); //sy-log
  console.log("---------------------------"); //sy-log
  return returnValue;
};

这个函数很简短,但是我相信很多人都会看得晕晕的,别急让我们来慢慢分析这个函数,首先我们接收了一个传进来的middlewareAPI对象并对其结构,注意由于我们这里用的是解构,所以就算我们之前在applyMiddleware.js中将dispatch重新赋值过,这里的dispatch仍然指向的是未修改之前的dispatch即let dispatch = () => { throw new Error(...,所以这就是我们为什么在定义middlewareAPI的时候需要将dispatch参数重新包装一层的原因了,因为需要调用到重新赋值之后的dispatch函数。然后next就是applyMiddleware函数中的 dispatch = compose(...middlewaresChain)(store.dispatch);里的每一次上一个中间件返回的action函数,由于我们thunk和logger用的时候都是applymiddleware(thunk, logger)这种方式,所以applyMiddleware.js文件中compose的时候,我们第一次执行的是logger中返回的参数为next的函数,此时这个next则是最初始的store.dispatch,然后这个函数又返回了一个参数为action的函数给到了thunk中间件的参数为next的函数,这个时候thunk中间件中的next指向的就是logger中间件的参数为action的函数,并且再将这个函数放在整个thunk函数最后去执行,thunk接收了logger的action函数之后返回了一个他自己的action的函数。

那么当我们这样异步去调用store.dispatch的时候:

store.dispatch((dispatch, getState) => {
  setTimeout(() => {
    // console.log("now ", getState()); //sy-log
    dispatch({type: "ADD", payload: 1});
  }, 1000);
});

我们首先进入的是thunk所返回的参数为action的dispatch函数,进入到thunk的dispatch函数中先判断当前action值是否为函数,如果是函数,那么就先去执行这个函数,在函数里我们又把dispatch调用一遍,注意!这个时候的dispatch函数则是thunk函数中传入的middlewareAPI.dispatch并且还是未赋值之前的dispatch函数,所以这个时候dispatch函数为dispatch: (...args) => dispatch(...args),这就是我们为什么需要自调用一遍dispatch函数了,所以第二遍自调用的dispatch函数为dispatch = compose(...middlewaresChain)(store.dispatch)这个时候我们又回到了thunk函数中,此时action值就是{type: "ADD", payload: 1}这个对象,然后就会调用next(action),而这个时候的next是logger函数所返回的一个参数为action的函数,所以我们进入到了logger返回的action函数中,我们依次执行,而logger函数中的next则是最原始的store.dispatch,所以此时的next(action)为store.dispatch(action),至此就完整的执行了一遍。

结语

相信能通读完整个文章的人应该都对redux有了一个清楚的认知了吧。试试着手谢谢自己的middleware吧!