手写一个极简redux和中间件


手写一个极简redux和中间件

redux是当取react最热门的状态管理库,在使用量上一骑绝尘,远远甩开mobx和recoil等。

很多人一开始可能觉得redux非常难用,规矩特别多,这是因为如果项目没有很复杂,不到不得已的时候还用不上redux。

只有遇到 React 实在解决不了的问题,你才需要 Redux。

但其实redux本身并没有很复杂,源码也才几百行,今天我们就实现一个简易版本的redux,抽丝剥茧,由浅入深认识redux的原理。

简易redux

首先我们创建一个react项目,然后加入redux:

create-react-app lredux

yarn add redux

创建一个常规的redux页面,

src
 | store
    | index.js
 | pages
    | index.jsx

store/index.js:

 import { createStore } from "redux";

const countReducer = (state = 0, action) => {
    switch (action.type) {
        case 'ADD':
            return state + action.payload;
        case 'MINUS':
            return state - action.payload;
        default:
            return state;
    }
}

export default createStore(countReducer);

pages/index.js:

import React, { Component } from "react";
import store from "../store";

export default class Index extends Component {

  componentDidMount() {
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }

  componentWillUnmount() {
    this.unsubscribe();
  }

  onAdd = () => {
    store.dispatch({ type: "ADD", payload: 2 });
  };
  
  render() {
    return (
      <div>
        <button onClick={this.onAdd}>ADD</button>
        <div>{store.getState()}</div>
      </div>
    );
  }
}

在App.js中引入该页面,运行后可以看到如下页面:

image

每点击一次ADD可以看到数字增加2.这是基本的redux用法,接下来我们实现一个自定义的lredux。

在src下创建如下项目结构:

项目结构

src
 | lredux 自定义redux
    | createStore.js
    | index.js

重点是createStore.js,在这个文件中我们要实现redux的createStore方法,该方法接收一个reducer生成一个store,这个store有dispatch方法用于修改数据,并且有一个数组用于保存订阅更新的回调,一旦dispatch执行后,就调用对应的回调。

lredux/createStore.js:

export default function createStore(reducer) {
    // 保存数据更改后的回调
    const listeners = [];
    // 状态
    let state;
    // 获取状态
    function getState() {
        return state;
    }
    // 订阅数据更新的回调
    function subscribe(func) {
        listeners.push(func);
        // 返回解绑订阅
        return () => {
            const index = listeners.indexOf(func);
            listeners.splice(index, 1);
        };
    }
    // 通过action更新状态,并且通知回调执行
    function dispatch(action) {
        state = reducer(state, action);
        listeners.forEach(listener => listener());
    }
    // 创建store的时候默认调用一次dispatch作为数据的初始化
    // type是一个随机数,为了和用户的type区分开
    dispatch({ type: Math.random() })

    return { getState, subscribe, dispatch };
}

在lredux/index.js暴露我们lredux的方法:

import createStore from "./createStore";

export { createStore };

在store/index.js中修改导入的redux为lredux:

import { createStore } from "../lredux";

启动项目,可以看到功能仍然正常执行,说明此时我们已经实现了一个简易版本的redux。

中间件功能的实现

此时我们的lredux还没有中间件的功能,不能针对一些异步、Promise的action进行处理,比如我们增加一个延时执行的方法:

onAsyncAdd = () => {
    store.dispatch((dispatch) =>
      setTimeout(() => {
        dispatch({ type: "ADD", payload: 2 });
      }, 1000)
    );
  };

  render() {
    return (
      <div>
        <button onClick={this.onAdd}>ADD</button>
        <button onClick={this.onAsyncAdd}>AsyncAdd</button>
        <div>{store.getState()}</div>
      </div>
    );
  }

此时的action是一个function,我们可以用redux的一个中间件redux-thunk来处理,但是我们的lredux还没有中间件的功能,我们现在来实现。

中间件的实现代码不多,但并不是很好理解,我们先引入redux-thunk和redux-logger看redux是如何使用中间件的:

yarn add redux-thunk redux-logger

修改store.index:

import { createStore, applyMiddleware } from "redux";
import logger from "redux-logger";
import thunk from "redux-thunk";
...
export default createStore(countReducer, applyMiddleware(thunk, logger));

这个时候再运行项目可以看到AsyncAdd是可以生效的。可以看到redux中间件的使用是在createStore传入第二个参数,applyMiddleware的执行结果,而applyMiddleware是传入中间件的一个函数。我们仿照其实现一个中间件。

首先在lreact下创建applyMiddleWare.js:

export default function applyMiddleware(...middlewares) {
    return createStore => reducer => {
        const store = createStore(reducer);

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (action, ...args) => dispatch(action, ...args)
        }

        const chain = middlewares.map(middleware => middleware(middlewareAPI));

        const dispatch = compose(...chain)(store.dispatch);

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

function compose(...functions) {
    if (functions.length === 0) {
        return (...args) => args;
    } else if (functions.length === 1) {
        return functions[0];
    } else {
        return functions.reducer(
            (pre, current) => (...args) => pre(current(args))
        );
    }
}

源码很复杂,大概解释一下:

applyMiddleware先后接受了3个参数,分别是middlewares中间件、createStore和reducer;调用了createStore(reducer)创建了一个store,然年接下来的步骤是给dispatch做加强,我们的目的就是希望dipatch的时候按顺序调用中间件。

中间件长什么样?我们稍后会手写几个中间件,这里可以先说明一下中间件的规范,中间件是一个先后接收三个参数的函数,第一参数是一个对象,包括getState和dispatch,第二个参数是next,表示下一个中间件,最后的action是用户调用dispatch的参数action。

所以这里首先个每个中间件middleWare传入的middleWareAPI——一个包含getState和dispatch的对象,对应的也就是中间件的第一个参数,得到的chain是一个中间件函数数组,接下来我们对其进行聚合,聚合后会成为一个洋葱模型的函数,按中间件的顺序逐个执行。

聚合函数是用的是Array.prototype.reduce实现的,很多项目也都是用reduce实现的洋葱模型,比如koa2.

洋葱模型

说到这里可能还是难以理解,我们从thunk, logger这两个中间件的执行步骤解释一下,假设这两个函数都长这样:

function middleWare({ getState, dispatch }) {
    return next => action => {
        ...
        next(action);
    }
}

在执行middlewares.map(middleware => middleware(middlewareAPI))后,我们得到的chain大概是这样子的闭包:


[
// thunk
next =>action => {
        ...
        next(action);
    },
// logger    
next =>action => {
        ...
        next(action);
    } 
]

接下来执行的是compose聚合函数,把chain中的中间件聚合成一个新的dispatch函数:

// thunk
action => {
        ...
        next(action);
    },

而其中的next则是:


// logger
action => {
        ...
        next(action);
    },

(如果对compose的执行过程感到疑惑,需要先理解reducer的执行)

logger中的next又是谁?是原始的disptach函数,我们在compose时传入的参数:compose(…chain)(store.dispatch)。

我们在applyMiddleWare中暴露了原本的store以及新的聚合后的dispath,所以一旦用户执行了disptach,就会逐步执行thunk、logger。

写完了applyMiddlWare,我们还需要更改createStore的创建方式:

lredux/createStore:

export default function createStore(reducer, enhancer) {
    // 如果有enhancer,则从enhancer创建store
    if (enhancer) {
        return enhancer(createStore)(reducer);
    }
    ....
}    

lredux/index.js增加中间件的导出:

import createStore from "./createStore";
import applyMiddleware from "./applyMiddleware";

export { createStore,applyMiddleware };

store/index.js:

import { createStore, applyMiddleware } from "../lredux";
...

运行项目后发现一切正常,到这里我们就实现了一个有中间件功能的lredux了。

具体中间件的实现

这里我们手写几个简易版本的中间件。

logger

logger其实很简单,打印一下状态变更前后的值:

function customLogger({ dispatch, getState }) {
    return next => action => {
        console.log(`action  ${action.type} `);
        console.log(`prev state  ${getState()} `);
        console.log(action);
        next(action);
        console.log(`next state  ${getState()} `);
    }
}

thunk

thunk就是在接收到action的时候判断一下是否函数,是的话执行函数,并把dispatch和getState传入。

function customThunk({ dispatch, getState }) {
    return next => action => {
        if (typeof action === 'function') {
            action(dispatch, getState);
        } else {
            next(action);
        }
    }
}

promise

promise简单判断了action是否为promise,和thunk其实差不多。

 function customPromise({ dispatch, getState }) {
    return next => action => {
        if (isPromise(action)) {
            action.then(dispatch);
        } else {
            next(action);
        }
    }
}

这是一个极简版本,其实还要考虑promise失败的情况。

最后

到这里我们就实现了一个建议版本的redux及其中间件了,虽然没有源码的各种容错处理,但也是抛砖引玉带大家理解了一波原理了。

有任何问题都欢迎交流~

参考:


文章作者: 小林
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小林 !
评论
  目录