相信我们在使用react的时候一定会接触到redux,redux是一个集中的状态管理工具,我们都知道react是单向数据流,如果组件层级过深,父组件的一个state要传到子组件可能需要好多层,子组件修改父组件也是如此,所以我们就可以使用redux来统一管理这些多个组件都需要且嵌套很深的状态。
基本语法
在写一个简单redux的时候我们肯定要知道redux要怎么使用,我们可以在官网上查看,主要有以下几个内容
- createStore: 接受一个函数来返回统一的状态管理仓库
- reducer: 仓库管理员,我们初始化还是修改都需要reducer来执行
- action: 一般形式包含了type和payload,根据action.type的不同reducer来执行不同的操作
- dispatch:通知
- subscribe: 订阅 然后我们就看一下简单的redux使用例子
import { createStore } from "redux";
const reducer = (state = { a: 1 }, action) => {
switch (action.type) {
case "add":
return { ...state, ...action.payload };
default:
return { ...state };
}
};
const store = createStore(reducer);
export default store;
app.jsx
import "./styles.css";
import store from "./store";
import { useState } from "react";
export default function App() {
const [state, setState] = useState(store.getState());
const add = () => {
store.dispatch({
type: "add",
payload: {
a: state.a + 1
}
});
};
const reset = () => {
setState(store.getState());
};
// store.subscribe(reset);
store.subscribe(reset); // 订阅
return (
<div className="App">
{state.a}
<button onClick={add}>增加</button>
<button onClick={reset}>更新</button>
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
可以看codesandbox
首先我们可以看到createStore接收一个reducer返回store,然后store中暴露了{dispatch,subscribe}等方法,dispatch传入的action为有两个key分别为type、payload,dispath后我们subscribe订阅的函数也会重新执行修改state然后我们的view就会更新试图啦,让我们直接开始吧
createStore实现
我们先不管reducer,我们通过传递参数的形式来初始化我们的state,我们定义一个currentStore表示当前state,首先我们要有一个getState方法来让外部获取到当前的状态仓库
function createStore(defaultState){
let currentState=defaultState;//初始化
function getState(){ //getState()
return currentState;
}
}
dispatch
我们先来实现一个不通过reducer的dispatch,每次修改值我们必须给currentStore一个新的引用,因为reducer必须是纯函数,至于reducer为什么必须要是纯函数我们讲reducer的时候会讲到
function createStore(defaultState){
let currentState=defaultState;//初始化
function getState(){ //getState函数
return currentState;
}
function dispatch(state){
currentState={...currentState,...state}
}
return {
getState,
dispatch
}
}
subscribe
state改变后怎么通知各个使用了状态的函数来更新状态呢,这就需要使用subscribe,这里就是一个发布者订阅者模式了,我们需要一个数组来存储我们的订阅者,那么我们什么时候发布呢,当然就是dispatch 的时候啦,这时候值改变了我们就需要通知订阅者更新我们的值啦,我们在dispatch中遍历收集订阅者的数组然后执行就行啦,能订阅当然就能取消订阅,取消订阅怎么做呢,很简单,只要我们在订阅的时候同时取消订阅的方法暴露出去就行了。
function createStore(defaultState){
let currentState=defaultState;//初始化
let listeners=[]; //收集订阅者的数组
function getState(){ //getState函数
return currentState;
}
function dispatch(state){
currentState={...currentState,...state}
for(let i=0;i<listeners.length;i++){
listeners[i]();
}
}
function subscribe(fn){
listeners.push(fn);
return function unSubscribe(){ //暴露取消订阅的方法
const index=listeners.indexOf(fn);
listerners.splice(index,1);
}
}
return {
getState,
dispatch,
subscribe
}
}
到目前为止应该就已经实现了一个超级简单的redux了,我们可以试下这个redux到底行不行.
demo
const store=createStore({a:1}) //初始化state{a:1}
store.getState();//{a:1}
store.subscribe(()=>{console.log(store.getState())}) //添加订阅者,状态发生变化后我们重新获取状态
store.dispatch({a:2});//修改状态,我们直接修改a为2,当我们执行dispatch,订阅者就会被执行输出最新的状态
reducer
现在让我们加入reducer,首先reducer是个纯函数,纯函数是什么这边就不做过多解释,简单而言就是每次入参后的输出都要是一样的,然后不能有副作用,reducer接受两个参数,一个当前state,另一个就是action了,action上面也说过了有两个值,为type和payload,最后我们的返回值必须要是一个新的引用,所以很常见下面这段代码
return {...state,...action.payload}
至于为什么有这段代码原因就是redux对变更前后两次的state为浅比较,也就是说引用不变,他是无法知道state已经做过修改了,如果对比每个属性就需要深层比较,类似深拷贝,对于复杂的数据结构其实非常费性能的。
首先我们先解决reducer的问题,reducer有个问题就是如果我们业务很大,那么我们就会根据模块来分离reducer,那么这些reducer最后肯定要合并在一起返回一个store,那么这个合并操作我们该怎么做呢.
我们合并后这两个reducer,首先我们要知道我们合并后返回的是什么,是一个合并后的reducer function,传入的参数依旧是state,action,我们其实只要在传入的reducerMaps中把每个reducer都执行一遍,把所有模块状态都更新一遍就行了,下面我们给出combineReducer的代码
function combineReducer(reducerMaps) {
const finalReducer = {};
const reduersKey = Object.keys(reducerMaps);
for (let i = 0; i < reduersKey.lengthl; i++) {
const key = reduersKey[i];
finalReducer[key] = reducerMaps[key]; //传入的所有reducer放入一个数组内
}
return function combine(state, action) {
//返回一个新的combine过后的reducer,这个reducer要做的是就是遍历有所reducer更新每个状态后返回
const nextState = {}; //初始化更新后的状态仓库
for (let i = 0; i < reduersKey.length; i++) {
const key = reduersKey[i];
const reducer = finalReducer[key];
const changeState = reducer(state[key], action); //每个reducer都调用一次;
nextState[key] = changeState; //获取新的不同模块的状态并给nextState
}
return nextState; //返回nextState;
};
}
下面我们修改我们的createStore函数来获取reducer参数,然后我们我们需要初始化执行一次reducer来初始化我们的仓库,我们在哪执行呢,reducer是管理仓库的,也就是生成仓库,修改里面的值都是reducer来操作的,怎么通知我们的reducer来修改值呢,是不是就是dispatch,那我们初始化的时候执行一次
function createStore(reducer,defaultState){
let currentReducer=reducer;
let currentState=defaultState;//初始化
let listeners=[]; //收集订阅者的数组
function getState(){ //getState函数
return currentState;
}
function dispatch(action){ //修改dispatch,让他执行reducer来更新状态
currentState=currentReducer(currentState,action);
for(let i=0;i<listeners.length;i++){
listeners[i]();
}
}
function subscribe(fn){
listeners.push(fn);
return function unSubscribe(){ //暴露取消订阅的方法
const index=listeners.indexOf(fn);
listerners.splice(index,1);
}
}
dispatch(currentState,{type:'init'}) //执行一次初始化
return {
getState,
dispatch,
subscribe
}
}
让我们试一试combineReducer和createStore吧我们先来两个reducer,先来两个reducer
function userReducer(state={user:"user"},action){
switch(action.type){
case 'add':return {...state,...action.payload}
default:
return {...state}
}
}
function adminReducer(state={admin:"admin"},action){
switch(action.type){
case 'add2':return {...state,...action.payload}
default:
return {...state}
}
}
const reducer = combineReducer({user:userReducer,admin:adminReducer});
const store=createStore(reducer,{}) //初始化state为{}
store.getState();//
store.subscribe(()=>{console.log(store.getState())}) //添加订阅者,状态发生变化后我们重新获取状态
store.dispatch({type:'add',payload:{user:'user1'}});//修改状态,我们修改user为user1,当我们执行dispatch,订阅者就会被执行
可以看到我们的user模块中的user字段最终是改变了的.
需要注意的是,使用combineReducer的时候,我们createStore接受的第二个参数必须是对对应的reducer中的数据初始化,比如我们有个userReducer,那么我们初始化数据就要对user模块内的数据进行初始化
createStore(reducer,{user:{user:'11'}}),不对模块内的数据进行初始化的话redux会报一个错误,如下图,当然我们自己写的并没有这么完善,会丢出异常,这边只是希望我们按照redux的语法来要求我们自己的一个simpleRedux;
applyMiddleware
applyMiddleware是让我们添加中间件使用的,它让我们可以在dispatch前做些其他的操作,他的本质其实就是重写我们的dispatch,一般applyMiddle返回一个函数,这个返回是作为createStore的第三个参数。那么applyMiddleware是怎么实现的呢
const store=createStore(reducer,{})
const next=store.dispatch;
const logtime=(action)=>{
console.log(new Date())
next(action);
}
store.dispatch=logtime;
上面代码我们把store.dispatch做一个缓存,然后重写我们的dispatch为logtime,我们在dispatch之前就可以记录我们的时间,如果我们需要在我们的输出时间前在执行其他的我们该怎么做呢
const logtime=(action)=>{
console.log(new Date())
next(action);
}
const middleware2=action=>{
console.log('do some other')
logtime(action);
}
const store.dispatch=middleware2;
可以看到middleware2之中执行logtime之前我们就可以执行些其他的操作了,这里就可以写我们middleware2这个中间函数所需要执行的逻辑了,但是这里我们是写死的logtime,我们可以通过高阶函数来实现,如下
const middleware2=next=>action=>{
console.log('do some other');
next(action)
}
const store.dispatch=middleware2(logtime);
我们可以看到我们通过接收一个next参数,这个参数就是我们执行完other后接下来要执行的下一个中间函数了,所以我们的中间件是不是就可以设计成
const middleware=next=>action=>{
console.log('do somethine');
next(action) //下一个中间函数,当我们是最后一个中间函数的时候我们直接执行next(action)//这里的next就是缓存下来的store.dispatch;
}
让我们多来几个中间件,就可以像下面这样子来写了.
const middleware1=next=>action=>{
console.log(1);
next(action);
}
const middleware2=next=>action=>{
console.log(2);
next(action);
}
const middleware2=next=>action=>{
console.log(3);
next(action);
}
const next=store.dispatch;
store.dispatch=middleware1(middleware2(middleware3(next)))
可以看到中间函数都被写成了A(B(C(D(next))))这种形式啦。
我们就可以设计我们的applyMiddleware函数,applyMiddleware会返回一个增强函数,他的参数就是我们的各个中间件函数,applyMiddleware(...args);
function applyMiddleware(...args){
return (createStore)=>(reducer,defaultStore)=>{
const store = createStore(reducer,defaultStore);
const next=store.dispatch;
let dispatch=store.dispatch;
args.reverse().map((middleware)=>{ //倒序执行
dispatch=middleware(dispatch);
})
store.dispatch=dispatch;
return store;
}
}
我们applyMiddleware返回的函数接收一个createStore,再通过createStore来创建store,然后在重写dispatch,那么我们就可以把改写createStore了,如果我们createStore有第三个参数那么我们就需要传入createStore,创建store后重写dispatch;
function createStore(reducer,defaultState,enhancer){
//我们直接判断是否有第三个参数
if(typeof enhancer ==='function'){
return enhancer(createStore)(reducer,defaultState);
}
let currentReducer=reducer;
let currentState=defaultState;//初始化
let listeners=[]; //收集订阅者的数组
function getState(){ //getState函数
return currentState;
}
function dispatch(action){ //修改dispatch,让他执行reducer来更新状态
currentState=currentReducer(currentState,action);
for(let i=0;i<listeners.length;i++){
listeners[i]();
}
}
function subscribe(fn){
listeners.push(fn);
return function unSubscribe(){ //暴露取消订阅的方法
const index=listeners.indexOf(fn);
listerners.splice(index,1);
}
}
dispatch(currentState,{type:'init'}) //执行一次初始化
return {
getState,
dispatch,
subscribe
}
}
下面我们来试下我们的整个流程吧
完整demo
function createStore(reducer,defaultState,enhancer){
//我们直接判断是否有第三个参数
if(typeof enhancer ==='function'){
return enhancer(createStore)(reducer,defaultState);
}
let currentReducer=reducer;
let currentState=defaultState;//初始化
let listeners=[]; //收集订阅者的数组
function getState(){ //getState函数
return currentState;
}
function dispatch(action){ //修改dispatch,让他执行reducer来更新状态
currentState=currentReducer(currentState,action);
for(let i=0;i<listeners.length;i++){
listeners[i]();
}
}
function subscribe(fn){
listeners.push(fn);
return function unSubscribe(){ //暴露取消订阅的方法
const index=listeners.indexOf(fn);
listerners.splice(index,1);
}
}
dispatch(currentState,{type:'init'}) //执行一次初始化
return {
getState,
dispatch,
subscribe
}
}
function applyMiddleware(...args){
return (createStore)=>(reducer,defaultStore)=>{
const store = createStore(reducer,defaultStore);
const next=store.dispatch;
let dispatch=store.dispatch;
args.reverse().map((middleware)=>{ //倒序执行
dispatch=middleware(dispatch);
})
store.dispatch=dispatch;
return store;
}
}
//reducer
function reducer(state,action){
switch (action.type) {
case "ADD":
return {
...state,
...action.payload,
};
default:
return {
...state,
};
}
}
//写两个中间件
const middle1=next=>action=>{
console.log('输出中间件1');
next(action);
}
const middle2=next=>action=>{
console.log('输出中间件2');
next(action);
}
const store=createStore(reducer,{a:1},applyMiddleware(middle1,middle2));
store.dispatch({
type:'ADD',
payload:{
a:3
}
})
//输出中间件1
//输出中间件2