前言
本文学习目标:
- redux的手写实现
- 理解Middleware的“洋葱模型”
- redux Middleware的手写实现
Redux
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,action是一个用来描述发生什么的对象。
Redux 有三大基本原则:
- 单一数据源 (整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。)
- State 是只读的 (唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。)
- 使用纯函数来执行修改 (为了描述 action 如何改变 state tree ,你需要编写 reducers。)
Redux的动机
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。用一句话来解释就是指公共状态管理库store 。那么这个store需要提供什么功能呢?
- currentState(保存公共状态)
- getState (getter)
- dispatch (setter)
- subscribe (发布订阅)
注意:为了让这个 store 的修改变得“可预测”、”可维护“,我们希望这个 store 既可以被全局访问到,又是私有的不能被直接修改(需要通过提交action来修改state)。于是我们可以使用闭包的方式来实现这个store。
在src目录下新建redux文件夹,然后新建两个文件:
index.js
// src/redux/index.js
import createStore from "./createStore";
export { createStore };
复制代码
createStore.js
// src/redux/createStore.js
export default function createStore(reducer, storeEnhancer) {
if (storeEnhancer) {
return storeEnhancer(createStore)(reducer);
}
//保存全局的state
let currentState;
//保存监听的事件
let listeners = [];
//getter
function getState() {};
//setter
function dispatch(action){};
//发布订阅
function subscribe(listener){};
return {
getState,
dispatch,
subscribe,
};
}
复制代码
getState的实现
getState主要用来获取当前state的,特别简单。
export default function createStore(reducer, storeEnhancer) {
if (storeEnhancer) {
return storeEnhancer(createStore)(reducer);
}
//保存全局的state
let currentState;
//保存监听的事件
let listeners = [];
//getter
function getState() {
return currentState;
};
//setter
function dispatch(action){};
//发布订阅
function subscribe(listener){};
return {
getState,
dispatch,
subscribe,
};
}
复制代码
dispatch的实现
dispatch用来修改当前state的。但是不可以随便修改state,需要遵循一定的规则。在使用dispatch的时候,我们会给dispatch()传入一个action对象,这个action对象包含了修改类型(actionType),和修改规则。
// ation对象
{
type: "increase",
//others
}
复制代码
export default function createStore(reducer, storeEnhancer) {
if (storeEnhancer) {
return storeEnhancer(createStore)(reducer);
}
//保存全局的state
let currentState;
//保存监听的事件
let listeners = [];
//getter
function getState() {
return currentState;
};
//setter
function dispatch(action) {
if (!isPlainObject(action)) {
//action必须为普通对象
throw new Error("Actions must be plain objects");
}
// 先修改store state
currentState = reducer(currentState, action);
// state改变,执行订阅的函数
listeners.forEach((listener) => listener());
};
//发布订阅
function subscribe(listener){};
return {
getState,
dispatch,
subscribe,
};
}
function isPlainObject(obj) {
if (typeof obj !== "object" || obj === null) return false;
let proto = obj;
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(obj) === proto;
}
复制代码
subscribe的实现
export default function createStore(reducer, storeEnhancer) {
if (storeEnhancer) {
return storeEnhancer(createStore)(reducer);
}
//保存全局的state
let currentState;
//保存监听的事件
let listeners = [];
//getter
function getState() {
return currentState;
};
//setter
function dispatch(action) {
if (!isPlainObject(action)) {
//action必须为普通对象
throw new Error("Actions must be plain objects");
}
// 先修改store state
currentState = reducer(currentState, action);
// state改变,执行订阅的函数
listeners.forEach((listener) => listener());
};
//发布订阅
function subscribe(listener) {
//将更新函数收集起来
listeners.push(listener);
//有订阅功能也必须得有取消订阅。返回一个取消订阅的函数,用于取消相关的更新函数
return () => {
let index = listeners.findIndex(listener);
listeners.splice(index, 1);
};
}
return {
getState,
dispatch,
subscribe,
};
}
function isPlainObject(obj) {
if (typeof obj !== "object" || obj === null) return false;
let proto = obj;
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(obj) === proto;
}
复制代码
然后我们来看一个计数器的例子:
在 src/store 文件夹下新建一个index.js
// src/store/index.js
import { createStore } from "../redux/index";
function counterReducer(state = 0, action) {
switch (action.type) {
case "increase":
return state + 1;
case "decrease":
return state - 1;
default:
return state;
}
}
const store = createStore(counterReducer);
export default store;
复制代码
在src目录下新建ReduxPage.js
import React, { Component } from "react";
import store from "./store/";
export default class ReduxPage extends Component {
componentDidMount() {
store.subscribe(() => {
this.forceUpdate();
//打印下store
console.log("getState", store.getState());
});
}
increase = () => {
store.dispatch({ type: "increase" });
};
decrease = () => {
store.dispatch({ type: "decrease" });
};
render() {
return (
<div>
<h3>ReduxPage</h3>
<p>{store.getState()}</p>
<button onClick={this.increase}>increase</button>
<button onClick={this.decrease} style={{ margin: "50px" }}>
decrease
</button>
</div>
);
}
}
复制代码
实现了这个简单版本的store,通过运行npm run start启动看看效果:
上面的代码运行起来发现,点击按扭可以正常的显示计数。但是页面首次加载没有显示初始数值。
这是因为在用户没有操作的时候没有返回默认值。我们需要在createStore中添加一行代码:
dispatch({ type: `@@redux/INIT${Math.random()}` });
复制代码
上面这行代码会执行默认的type,返回store的初始值。修改后的代码如下:
export default function createStore(reducer, storeEnhancer) {
if (storeEnhancer) {
return storeEnhancer(createStore)(reducer);
}
//保存全局的state
let currentState;
//保存监听的事件
let listeners = [];
//getter
function getState() {
return currentState;
}
//setter
function dispatch(action) {
if (!isPlainObject(action)) {
//action必须为普通对象
throw new Error("Actions must be plain objects");
}
// 先修改store state
currentState = reducer(currentState, action);
// state改变,执行订阅的函数
listeners.forEach((listener) => listener());
};
//发布订阅
function subscribe(listener) {
//将更新函数收集起来
listeners.push(listener);
//有订阅功能也必须得有取消订阅。返回一个取消订阅的函数,用于取消相关的更新函数
return () => {
let index = listeners.findIndex(listener);
listeners.splice(index, 1);
};
}
//初始化state,这里使用Math.random()是想与用户自定义的type区分开来。
dispatch({ type: `@@redux/INIT${Math.random()}` });
return {
getState,
dispatch,
subscribe,
};
}
function isPlainObject(obj) {
if (typeof obj !== "object" || obj === null) return false;
let proto = obj;
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(obj) === proto;
}
复制代码
到此一个简单的redux就实现了。
- 异步action
实际开发中会出现从服务端请求一个接口然后以返回的结果作为state,我们使用一个asyncIncrease方法来模拟这个异步action:
修改下ReduxPage.js
import React, { Component } from "react";
import store from "./store/";
export default class ReduxPage extends Component {
componentDidMount() {
store.subscribe(() => {
this.forceUpdate();
});
}
increase = () => {
store.dispatch({ type: "increase" });
};
//模拟异步
asyncIncrease = () => {
let timeOut = () => {
setTimeout(() => {
store.dispatch({ type: "increase" });
}, 1000);
}
thunk(store, timeOut)
};
render() {
return (
<div>
<h3>ReduxPage</h3>
<p>{store.getState()}</p>
<button onClick={this.increase}>increase</button>
<button onClick={this.asyncIncrease} style={{ margin: "50px" }}>
asyncIncrease
</button>
</div>
);
}
}
复制代码
由于刚才的手写createStore方法中的dispatch方法只接收“Plain Object”的action,当我们使用异步的action的时候没有提示用户使用相对应的中间件的处理。因此我们先使用官方的redux来看看当我们没有使用中间件的时候会给我们提示什么?
修改 src/store/index.js
// src/store/index.js
// import { createStore } from "../redux/";
import { createStore } from "redux";
function counterReducer(state = 0, action) {
switch (action.type) {
case "increase":
return state + 1;
case "decrease":
return state - 1;
default:
return state;
}
}
const store = createStore(counterReducer);
export default store;
复制代码
我们看到页面提示说,操作必须是普通对象,实际的类型是:“函数”。您可能需要在设置中添加中间件来处理调度其他值,例如“redux-thunk”来处理调度函数。
首先我们来分析下这个thunk的功能:
- 如果用户dispatch一个函数,那么thunk需要帮他执行这个函数。
- 如果用户dispatch一个action对象,那么thunk直接返回这个action。
明白了thunk的用途,我们现在就开始动手实现一个thunk吧。
//thunk用于执行函数并且提交action
function thunk(store, action) {
let next = store.dispatch
//如果用户传一个函数,执行它
if (typeof action === 'function') {
return action();
}
next(action)
}
复制代码
我们使用thunk函数来封装了一个公用的新的dispatch方法,然后在我们需要执行异步的时候引入这个thunk方法就可以了。
具体的使用我们来看ReduxPage.js
// src/ReduxPage.js
import React, { Component } from "react";
import store from "./store";
function thunk(store, action) {
let next = store.dispatch;
//如果用户传一个函数,执行它
if (typeof action === "function") {
return action();
}
next(action);
}
export default class ReduxPage extends Component {
componentDidMount() {
store.subscribe(() => {
this.forceUpdate();
console.log("getState", store.getState());
});
}
increase = () => {
store.dispatch({ type: "increase" });
};
//模拟异步
asyncIncrease = () => {
let timeOut = () => {
setTimeout(() => {
store.dispatch({ type: "increase" });
}, 1000);
};
//使用thunk处理这个异步action
thunk(store, timeOut);
};
render() {
return (
<div>
<h3>ReduxPage</h3>
<p>{store.getState()}</p>
<button onClick={this.increase}>increase</button>
<button onClick={this.asyncIncrease} style={{ margin: "50px" }}>
asyncIncrease
</button>
</div>
);
}
}
复制代码
引入thunk之后我们再来跑下刚才的计数器的例子,it works!
到此我们的thunk实现了。
Middleware
Middleware就是⼀个函数,对 store.dispatch ⽅法进⾏改造,在发出 Action 和执⾏ Reducer 这两步之间,添加了其他功能。例如 Express 或者 Koa 的 middleware 可以完成添加 CORS headers、记录日志、内容压缩等工作。middleware 最优秀的特性就是可以被链式组合。你可以在一个项目中使用多个独立的第三方 middleware。
于是我们得到启发,将多个middleware串联起来。需要用到的时候统一引入,每个 middleware接收store并在函数内部做自己的事情,做完以后再返回store给到下一个middleware,依次类推。就像这样:
funciton M1(store){
//M1 todo
return store.dispatch
}
funciton M2(store){
//M2 todo
return store.dispatch
}
funciton M3(store){
//M3 todo
return store.dispatch
}
M1(
M2(
M3(
store.dispatch(Action)
)
)
)
复制代码
洋葱模型
我们的dispatch被若干个 Middleware 包裹起来,一圈一圈的。上面的代码结构其实就像一个“洋葱模型”:smiley_cat:
其实刚才我们在需要异步action的时候就引入thunk的做法会使得我们的代码变得臃肿。如果我们还想加上一个打印日志的功能呢?这样多个中间件的使用会让我们的ReduxPage.js文件变得越来越难以维护…
函数式编程
但是上面的代码结构在多个函数嵌套的时候会出现代码向右偏移,影响代码的可读性。这个时候函数式编程思想就派上用场了。
函数式编程有两个最基本的运算:合成(compose)和柯里化(Currying)。
合成(compose)
如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做”函数的合成”(compose)。合成的好处显而易见,它让代码变得简单而富有可读性,同时通过不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力(避免了代码的向右偏移,更加优雅。:blush:)。
function funcA(arg) {
console.log("funcA", arg);
return arg;
}
function funcB(arg) {
console.log("funcB", arg);
return arg;
}
function funcC(arg) {
console.log("funcC", arg);
return arg;
}
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)));
}
let res = compose(funcA, funcB, funcC)("omg"); //compose(funcA(funcB(funcC("omg"))))
console.log("res", res); //omg
复制代码
柯里化(Currying)
柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。所谓”柯里化”,就是把一个多参数的函数,转化为单参数函数。
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
复制代码
了解了这两个概念之后,我们使用这个 compose函数来重构下刚才调用中间件的代码。
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)));
}
function thunk(){
//todo
}
function logger(){
//todo
}
function promise(){
//todo
}
function dispatch(){
//todo
}
//compose函数的作用只是让你在写深度嵌套的函数时,优雅的避免了代码的向右偏移.
compose(thunk, logger, promise)(dispatch) //thunk(logger(promise(dispatch())))
复制代码
applyMiddleware
上面代码演示了如何使用compose来优雅的调用我们的 Middleware 函数。但是实际项目中我们不知道具体会用到多少个 Middleware 。我们是否可以将这个compose方法放入到一个 applyMiddleware 的函数中,不管用户将来需要用到多少个 Middleware ,我们只需要把用户传入的 Middleware 按照序列执⾏就好了?这就是 applyMiddleware 函数的作用。
在src目录下的redux文件夹中新建一个 applyMiddleware.js 文件
// src/redux/applyMiddleware.js
//我们使用一个 applyMiddleware() 来“单纯”地使用 middleware。在 applyMiddleware() 中,我们取得最终完整的被包装过的 dispatch() 函数,并返回一个 store 的副本,这样我们就可以使用加强版本的dispatch了。
export default function applyMiddleware(...middlewares) {
//利用函数柯里化返回一个使用createStore参数的函数,因此我们就能够在函数内部操作store
return (createStore) => (reducer) => {
const store = createStore(reducer);
let dispatch = store.dispatch;
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args), //这里使用一个匿名函数来返回一个dispatch是为了不让middleware彼此之间的的dispatch不受影响(每个middleware各自执行自己的逻辑)。
};
//我们将middlewareAPI传给middleware就能访问store并且执行各自的业务,chain是个能够访问store的中间件数组
const chain = middlewares.map((middleware) =>
middleware(middlewareAPI)
);
// 返回一个加强版本的dispatch
dispatch = compose(...chain)(store.dispatch);
//返回store的副本
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))
);
}
复制代码
在src目录下新建utils文件夹,新建redux-thunk.js文件:
// src/utils/redux-thunk.js
export default function thunk({ dispatch, getState }) {
//此处的next参数指的是我们的“里层”的Middleware,如果不存在则代表store.dispatch
return (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
复制代码
再加上一个打印日志的中间件,redux-logger.js:
// src/utils/redux-logger.js
export default function logger({ dispatch, getState }) {
return (next) => (action) => {
console.log("old state", getState());
const result = next(action);
console.log("new state", getState());
return result;
};
}
复制代码
修改下src目录下的redux文件夹里的index.js文件,导出applyMiddleware。
// src/redux/index.js
import createStore from "./createStore";
import applyMiddleware from "./applyMiddleware";
export { createStore, applyMiddleware };
复制代码
将 src/store/index.js 修改为使用我们自己手写的redux,并且引入 applyMiddleware 和刚才的 thunk 中间件
// src/store/index.js
import { createStore, applyMiddleware } from "../redux/";
import thunk from "../utils/redux-thunk";
import logger from "../utils/redux-logger";
function counterReducer(state = 0, action) {
switch (action.type) {
case "increase":
return state + 1;
case "decrease":
return state - 1;
default:
return state;
}
}
const store = createStore(counterReducer, applyMiddleware(thunk, logger));
export default store;
复制代码
修改 ReduxPage.js
// src/ReduxPage.js
import React, { Component } from "react";
import store from "./store";
export default class ReduxPage extends Component {
componentDidMount() {
store.subscribe(() => {
this.forceUpdate();
});
}
increase = () => {
store.dispatch({ type: "increase" });
};
//模拟异步
asyncIncrease = () => {
let timeOut = () => {
setTimeout(() => {
store.dispatch({ type: "increase" });
}, 1000);
};
store.dispatch(timeOut);
};
render() {
return (
<div>
<h3>ReduxPage</h3>
<p>{store.getState()}</p>
<button onClick={this.increase}>increase</button>
<button onClick={this.asyncIncrease} style={{ margin: "50px" }}>
asyncIncrease
</button>
</div>
);
}
}
复制代码
最后再来跑下刚才的异步action的例子:
可以看到我们的redux-thunk、redux-logger都可以了!
最后附上项目地址: 手写redux、redux中间件的实现原理 看了觉得赞的同学欢迎给个star呀!