前言
本文从 redux 原理出发,一步步实现一个自己的 mini-redux,主要目的是了解其内部之间的各种关系,所以本篇不会讲解太多关于 redux 的用法
redux 是什么
redux 是一种可预测的状态管理库,在 react 中,它解决的是多个组件之间的通信问题
在没有使用 redux 的情况下,如果两个组件(非父子关系)之间需要通信的话,可能需要多个中间组件来为他们进行消息传递,这样既浪费了资源,代码也会变得更复杂
redux 提出了单一数据源 store 来存储状态数据,所有的组件都可以通过 action 来修改 store,也可以从 store 中获取最新状态。使用了 redux 就可以完美解决组件之间的通信问题
redux 的设计原则
redux 的三大设计原则:
- 单一数据源
- 状态是只读的
- 使用纯函数编写 reducer
单一数据源
意思是整个 react 项目里的 state 都存放在一起,单一数据源主要是为了解决状态一致性的问题
在传统的 MVC 架构中,需要创建无数个 Model,而 Model 之间可以互相监听、触发事件甚至循环或嵌套触发事件,这些在 redux 中都是不允许的
在 redux 的思想里,一个应用永远只有唯一的数据源,这个设计也是有一些好处的,对于开发者来说,它可以更容易调试和观察状态的变化
也不用担心数据源对象过于庞大的问题,redux 提供的 combineReducers 函数可以解决这个问题
状态是只读的
这里说的状态,指的是上面说的存放在 store 中的状态数据,你不能直接对其中的状态数据进行改动,只能间接的通过发送 action 来改动状态。间接的改动状态,这是一个很关键的设计,也是单向数据流的重点之一,对于每个动作的发生,最终会影响到什么状态上的改动,一个接一个的执行顺序等等,都是可预测的
使用纯函数编写 reducer
纯函数的概念:函数的返回结果只依赖其参数,并且执行过程中不会产生副作用
在 redux 中,我们通过定义 reducer 来更改状态,每个 reducer 都是纯函数,这意味着它没有副作用,相同的输入必定有相同的输出
ps:修改外部的变量、调用 DOM API 修改页面,发送 Ajax 请求,调用 window.reload 刷新浏览器甚至是console.log 打印数据,都是副作用
就问你纯不纯
redux 的几个基本概念
store
store 是存储数据的地方,它是一个对象,有这么几个方法
- getState() 获取当前状态
- dispatch(action) 派发 action
- subscribe(handler) 监听数据的变化
action
action 可以理解为操作数据的行为
action 一般的写法如下:
代码语言:javascript复制const add = (val) => {
return {
type: 'ADD',
value: val
}
}
通过 type 去定义这个 action 是干嘛的,在 reducer 中要进行什么操作
dispatch
dispatch 的作用就是派发一个 action,让 reducer 进行数据的处理
一般写法:
代码语言:javascript复制dispatch({
type: 'ADD',
value: 1
})
reducer
reducer 里是真正更改数据的地方,dispatch 派发的 action 最终由 reducer 来进行数据的处理,并且每次的更改都是返回新的 state,这样做的目的是为了让 state 变的可预测
middleware
在创建 store 的时候 createStore 可以传入三个参数,第三个参数就是中间件,使用 redux 提供的 applyMiddleware 方法来调用,applyMiddleware 等于是对 dispatch 进行了增强,这样的话,在 dispatch 的过程中可以做一些其他的事情,比如记录 state 的变化、异步请求等等
从 0 实现一个 mini-redux
redux 的核心,就是 createStore 这个函数,store、getState、dispatch 都是这个函数返回的
redux 的大致原理就是发布订阅模式:通过 dispatch 派发 action 更改 store,通过 subscribe 订阅 store 的变化,去更新对应的 view
createStore
用过 createStore 方法的都知道,创建一个 store 需要三个参数
代码语言:javascript复制/**
* 创建 store
* @param {*} reducer
* @param {*} initState 初始 state
* @param {*} enhancer 中间件
*/
const createStore = (reducer, initState, enhancer) => {
}
这个函数会返回几个功能函数
代码语言:javascript复制/**
* 创建 store
* @param {*} reducer
* @param {*} initialState 初始 state
* @param {*} enhancer 中间件
*/
const createStore = (reducer, initialState, enhancer) => {
return {
getState,
dispatch,
subscribe,
replaceReducer
}
}
下面来实现这几个方法
getState 的实现
getState 方法的作用就是返回当前的 state
代码语言:javascript复制let currentState; // 当前 state
/**
* 返回最新的 state
*/
const getState = () => {
return currentState;
};
subscribe 的实现
subscribe 的作用是订阅 state 的变化,使用者通过这个方法订阅,当 state 变化后,执行监听函数subscribe 是一个高阶函数,它的返回值一个函数,执行该函数可以移除当前的监听函数
代码语言:javascript复制let subQueue = []; // 创建一个监听队列
/**
* 监听 state 的变动
* @param {*} listener 数据变化时要执行的函数
*/
const subscribe = (listener) => {
// 把监听函数放入监听队列里
subQueue.push(listener);
// 移除监听事件
return () => {
subQueue = subQueue.filter((l) => l !== listener);
};
};
dispatch 的实现
dispatch 方法接收一个 action 参数,来执行 reducer,执行完成后会执行所有的监听函数
代码语言:javascript复制let currentReducer = reducer;
let isDispatch = false;
/**
* 派发 action 并执行所有 监听函数
* @param {*} action
*/
const dispatch = (action) => {
// 这里使用 isDispatch 做标识,上一个处理完成后才能处理下一个
if(isDispatch) {
throw new Error('dispatching')
}
try {
currentState = currentReducer(currentState, action)
isDispatch = true;
} finally {
isDispatch = false;
}
// 执行所有监听函数
subQueue.forEach((listener) => listener())
return action
}
replaceReducer 的实现
replaceReducer 的作用就是替换当前 reducer,执行 createStore 的时候,会接收一个默认的 reducer,如果后面想要重新换一个,就需要用到 replaceReducer 了
代码语言:javascript复制/**
* 替换 reducer
* @param {*} reducer
*/
const replaceReducer = (reducer) => {
// 直接把新的 reducer 覆盖掉旧的就行了
currentReducer = reducer;
// 替换之后派发一次 dispatch
dispatch({ type: 'MINI_REDUX_REPLACE' });
};
替换之后派发一次 dispatch 的目的是初始化一下新的 reducer
完整版 createStore
要想理解并实现中间件,内容还是蛮多的,所以本篇先不写中间件相关的内容
代码语言:javascript复制/**
* 创建 store
* @param {*} reducer
* @param {*} initialState 初始 state
* @param {*} enhancer 中间件
*/
const createStore = (reducer, initialState, enhancer) => {
let currentState; // 当前 state
let subQueue = []; // 创建一个监听队列
let currentReducer = reducer;
let isDispatch = false;
if (initialState) {
currentState = initialState;
}
/**
* 返回最新的 state
*/
const getState = () => {
return currentState;
};
/**
* 监听 state 的变动
* @param {*} listener 数据变化时要执行的函数
*/
const subscribe = (listener) => {
// 把监听函数放入监听队列里
subQueue.push(listener);
// 移除监听事件
return () => {
subQueue = subQueue.filter((l) => l !== listener);
};
};
/**
* 派发 action 并执行所有 监听函数
* @param {*} action
*/
const dispatch = (action) => {
// 这里使用 isDispatch 做标识,上一个处理完成后才能处理下一个
if (isDispatch) {
throw new Error('dispatching');
}
try {
currentState = currentReducer(currentState, action);
isDispatch = true;
} finally {
isDispatch = false;
}
// 执行所有监听函数
subQueue.forEach((listener) => listener());
return action;
};
/**
* 替换 reducer
* @param {*} reducer
*/
const replaceReducer = (reducer) => {
if (reducer) {
// 直接把新的 reducer 覆盖掉旧的就行了
currentReducer = reducer;
}
// 替换之后派发一次 dispatch
dispatch({ type: 'MINI_REDUX_REPLACE' });
};
return {
getState,
dispatch,
subscribe,
replaceReducer,
};
};
export default createStore;
要想在项目中跑起来,光实现一个 createStore 是不够的
createStore 建造了一个仓库,还需要配送点(Provider)和送货员(connect)才能到用户(组件)手里
Provider、connect
首先,我们需要清楚他们三者之间的职责:
createStore:生成 store,返回一系列功能函数
Provider:把 createStore 返回的一系列函数传递到每个子组件里
connect:把 store 里的数据关联到组件上
Provider 的实现
Provider 的主要作用就是把 store 里的数据传递下去
代码语言:text复制// Provider.jsx
import React, { createContext } from 'react';
export const StoreContext = createContext(null);
const Provider = (props = {}) => {
const { store, children } = props;
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};
export default Provider;
connect 的实现
connect 是一个高阶组件,第二个参数为需要关联数据的组件,返回一个新组件
connect 的作用就是把 store 的数据关联到对应组件里,并监听 store 的变化,数据变化后更新对应组件
代码语言:text复制// connect.jsx
import React, { useContext, useEffect, useState } from 'react';
import { StoreContext } from './Provider';
const connect = (mapStateToProps, mapDispatchToProps) => (WrapComponent) => {
const ConnectComponent = () => {
const { getState, dispatch, subscribe } = useContext(StoreContext);
const [props, setProps] = useState({
getState,
dispatch,
});
let stateToProps;
let dispatchToProps;
const update = () => {
if (mapStateToProps) {
stateToProps = mapStateToProps(getState());
}
if (mapDispatchToProps) {
dispatchToProps = mapDispatchToProps(dispatch);
}
setProps({
...props,
...stateToProps,
...dispatchToProps,
});
};
useEffect(() => {
update();
subscribe(() => update());
}, []);
return <WrapComponent {...props} />;
};
return ConnectComponent;
};
export default connect;
总结
一个基础版的 mini redux 就实现完了,有空了把中间件相关的东西输出一下
这是我学习完相关内容之后输出的一个笔记,有写的不对的地方,还请各位铁汁指正 抱拳了老铁
体验在线 demo:点我点我点我
github:https://github.com/isxiaoxin/mini_redux
首发自:从 0 实现一个 mini redux - 小鑫の随笔