面试还问redux?那我从头手撸源码吧(中间件篇)

昨天的文章手写了一版redux的核心源码,redux库除了数据的状态管理还有一块重要的内容那就是中间件,今天我还是尝试将此部分源码完成。

中间件

react中管理数据的流程是单向的,就是说,从派发动作一直到发布订阅触发渲染是一条路走到头,那么如果想要在中间添加或是更改某个逻辑就需要找到action或是reducer来修改,有没有更方便的做法呢?

而中间件(middleware)就是一个可插拔的机制,如果想要扩展某个功能,比如添加日志,在更新前后打印出state状态,只需要将日志中间件装到redux上即可,于是便有了日志功能,当不想使用时可再拿掉,非常方便。

目前有很多第三方的中间件安装即可使用,比如刚刚提及的日志中间件:redux-logger,使用npm安装它:

npm install redux-logger

redux包提供了一个方法可以装载中间件:applyMiddleware。在创建store对象的时候,可以传入第二个参数,它就是中间件:

import { createStore, applyMiddleware } from "redux";
import { reducer } from "./reducer";
import ReduxLogger from "redux-logger";
//使用applyMiddleware加载中间件
let store = createStore(reducer, applyMiddleware(ReduxLogger));

装载好中间件就在派发动作上扩展了相应的功能,这时我们正常编写redux程序,当执行dispatch方法时会在控制台打印出state更新日志:

以上就是一个使用中间件的例子。

浅析中间件的原理

那么中间件的执行原理是什么呢?就用刚刚的日志中间件举例,它的功能是在state对象的更新前后分别输出状态,那么肯定是在派发(dispatch)动作的那一刻去实现的,那我们改写一下redux库,将“打印日志”功能添加到dispatch方法里:

let temp = store.dispatch;//暂存原dispatch方法
store.dispatch = function(action) {
  console.log("旧state:", store.getState());
  temp(action);//执行原dispatch方法
  console.log("新state:", store.getState());
};

这样就实现了“日志中间件”,但是直接改写redux库是不可能的,我们需要一个通用的办法去定义中间件,redux提供了这样一个方法:applyMiddleware

它的使用方法很简单,将需要加载的中间件依次传入applyMiddleware方法中即可:

applyMiddleware(ReduxLogger, ReduxThunk);

手写applyMiddleware源码

中间件原理我们分析完了,即然中间件就是扩展dispatch方法,那么applyMiddlware必然会将中间件的dispatch方法和原始dispatch传入才可行,没错,我们就看看它的方法签名:

var applyMiddleware = (middlewares) => (createStore) => (reducer) => {};

以上就是applyMiddleware方法,它又是一个三层的高阶函数,这里用到了函数柯里化的思想,将多个参数拆分为单一参数的高阶函数,以保证每一层只有一个参数,这样更加灵活可分块调用。写成箭头函数不好理解,我们改写为普通函数形式:

var applyMiddleware = function (middlewares){
  return function (createStore){
    return function (reducer){
      //在这里装载中间件
    }
  }
};

通过函数参数就可以看到,三层函数分别传入了中间件(middleware)、创建仓库方法(createStore)和reducer函数,这正是我们装载一个中间件所需要的。

接下来我们的目标就是将中间件提供的dispatch覆盖redux原有的dispatch方法,这样就“装载”好了中间件。

var applyMiddleware = function (middlewares) {
    return function (createStore) {
        return function (reducer) {
            let store = createStore(reducer);
            //调用中间件,返回新dispatch方法
            let newDispatch = middlewares(store)(store.dispatch);
            //覆盖原有的dispatch方法并返回仓库对象
            return {
                ...store,
                dispatch: newDispatch
            }
        }
    }
}

有了通用写法,我们自己模拟实现一个日志中间件:

function reduxLogger(store) {
    return function (dispatch) {
        //dispatch参数即原redux派发方法
        return function (action) {
            //返回的这个函数即新方法
            //最终会传入applyMiddleware覆盖掉dispatch
            console.log(`更新前:${JSON.stringify(store.getState())}`);
            dispatch(action);
            console.log(`更新后:${JSON.stringify(store.getState())}`);
        }
    }
}

调用我们自己的方法装载中间件:applyMiddleware(reduxLogger);,运行效果如下:

组合中间件

但是到现在还没完,还记得官方redux库吗?人家的applyMiddlewares方法是支持传入多个中间件的,如:applyMiddlewares(middleware1,middleware2);我们目前的方法还不支持这种写法,最终的目的是想把若干个中间件一次组合为一个整体,一起加载。

洋葱模型

洋葱模型的概念似乎是在Koa2框架中提出的,它是指中间件的执行机制,当多个中间件执行时,后一个中间件会套在前一个中间件的里面:

执行完一个中间件会一直向里走,直到最后一个执行结束,再从内而外走出,就像是在剥洋葱一样。

compose方法

我们同样使用洋葱模型来写一个组合方法,以达到目的。

新建一个compose.js,创建一个组合函数:

/**
 * 组合所有中间件
 * @param  {...any} middlewares 
 */
function compose(...middlewares) {
    return function (...args) {

    }
}

我的目标是当调用组合函数,传入多个中间件,将所有的中间件组合成一个函数:

var all = compose(middleware3, middleware2, middleware1);
all();//调用时,依次执行所有中间件

我们动手实现它。

写之前我们先想一下,组合功能即是将“若干”个功能封装为“一个”功能,这正是函数式编程的收敛思想,ES6中已经为我们提供了reduce函数,在这里最合适不过了:

/**
 * 组合所有中间件
 * @param  {...any} middlewares 
 */
function compose(...middlewares) {
  //args即第一个中间件所需参数
  return function (...args) {
    return middlewares.reduce((composed, current) => {

      return function (...args) {
        //当前中间件的执行结果即上一个中间件的参数
        return composed(current(...args));
      }
    })(...args);
  }
}

通过reduce函数,一步一步将后一个中间件套到前一个中间件之中,后一个中间件的结果即前一个的参数,这样层层递近,最终返回一个大函数,即完成组合。

最后可以优化为箭头函数的形式,显得逼格更高一点:

function compose(...middlewares) {
  return (...args) => middlewares.reduce((composed, current) => (...args) => composed(current(...args)))(...args)
}

完成中间件装载

在compose完成之后,最后一步的工作就是改写applyMiddlewares将所有传入的中间件组合好:

function applyMiddleware(...middlewares) {
    return function (createStore) {
        return function (reducer) {
            let store = createStore(reducer);
            //一次传入多个中间件,循环包开一层函数
            let chain = middlewares.map(middleware => {
                return middleware(store);
            });

            //组合所有的中间件
            let newDispatch = compose(...chain)(store.dispatch);
            //覆盖原有的dispatch方法
            return {
                ...store,
                dispatch: newDispatch
            }
        }
    }
}

至此,redux库的源码已经基本实现完毕。

多个中间件运行如下:

尾巴

这两天从头手写了一遍redux库发现redux的源码量并不大但是逻辑还是很复杂的,理清redux的流程是读写源码的前提。而中间件则是redux库的一个难点,主要是层层调用关系非常恼人,一个好办法是通过库源码与中间件源码对比来分析,理清思路即可,如果还有时间我会尝试再手写一版react-redux库,一个是学习提高,二是应付面试。

原文链接:segmentfault.com

上一篇:[译]JavaScript 终极指南之执行上下文、变量提升、作用域和闭包
下一篇:使用taro完成小程序开发

相关推荐

  • 🔥手写大厂前端知识点源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    10 天前
  • 🔥前端面试大厂手写源码系列(上)

    如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。

    7 天前
  • (转载)如何通俗易懂的理解Redux

    作者:Wang Namelos 链接:https://www.zhihu.com/questio...(https://www.zhihu.com/question/41312576/answer/9...

    2 年前
  • 防抖与节流(源码学习)

    最近自己撸了一个轮播图,在点击切换的时候,为了寻求更好的用户体验,引入了节流,在此记录对源码的学习过程 源码来源:underscore(https://github.com/jashkenas/und...

    2 年前
  • 阅读redux源码_compose

    先上源码: 重点看一句就够了: 现在我们先假设一个数组,有3个函数,分别是x,y,z 那么发生什么了,接下来就一步一步解释 1. 变成reduce模式: 2. reduce第一次执行,...

    2 年前
  • 阅读 is-generator-function 源码

    (https://img.javascriptcn.com/152091d995a0b72de8b8d6aa8c0c768f) 从正则表达式 (https://img.javascriptc...

    1 年前
  • 通过编写一个路由中间件来学习 Koa

    混了四年的大学生活结束了,校招没有找到工作的我还面临着失业。没办法,只有临时抱抱佛脚看看能不能找个工作了。据说最近前端圈里不会 NodeJs 是不可能找到工作的,于是抱起了 NodeJs 里比较流行的...

    2 年前
  • 通过变量来使用next()在expressjs未来中间件

    cchamberlainuser2791897(https://stackoverflow.com/users/769871/cchamberlain)提出了一个问题:passing variable...

    2 年前
  • 逐行解析Axios源码

    image(https://img.javascriptcn.com/ebf0e6753d1e997cdc179de364317eed "image") 源码目录结构 前言 本文主要关注...

    8 个月前
  • 逐行粒度的vuex源码分析

    了解vuex 什么是vuex 图片描述(https://img.javascriptcn.com/6de1abc14258c286464237639a9b815f "图片描述") vuex是一...

    1 年前

官方社区

扫码加入 JavaScript 社区