手写一个极简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中引入该页面,运行后可以看到如下页面:
每点击一次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及其中间件了,虽然没有源码的各种容错处理,但也是抛砖引玉带大家理解了一波原理了。
有任何问题都欢迎交流~
参考: