背景
最近在学习的过程中遇到一个问题:"Redux 的设计理念虽然不能说百分之百避免状态管理中的副作用,但是从很大程度上说,它要比单例模式更加有效的多"。这是在极客时间学习时老师提出的观点。
结合最近在项目中有用单例模式来管理权限的业务,所以打算多了解一下并且和大家分享讨论。
先交代一下业务背景
业务背景
简单理解就两点:
- 异步获取业务权限,一个对象key是权限点,value是boolean类型的值(可以简单理解接口会返回业务权限点的true/false)
- 根据权限前端对页面上的按钮进行控制(是否禁用)
当前的设计方案(SDK)
- 整个权限对象为一个单例对象
- 对象提供发布订阅器,通过事件的方式来通知订阅的对象的key的权限
- 对象的值的更新会触发相应的事件
业务方通过订阅权限点(触发事件的名称)来拿到最新的值。
全局对象会在数据更新时发布数据的更新事件(事件名称也是权限点)
即将遇到的问题
可以遇见的问题
- 事件会随着业务的迭代事件将会变多
- 事件的执行顺序的控制(数据未更新先消费数据,而导致异常)
比如最近就需要加入某些权限点的管理员权限,如果是管理员可以不用判断单独的权限。这样可能导致上面的问题,业务方又需要订阅管理员权限了?两个都是异步事件不能给业务方来设置吧。
这样就想引入插件,通过插件来控制,但是觉得有点复杂,正好就再看看redux的实现。
从上面思考的角度来学习redux
先温习一下Redux三个原则:
- 全局的状态都在一个 store 里保存
- 这个 store 里的状态对于整个应用来说都是只读的
- 如果需要更新改变状态的话,则需要通过reducer来完成
redux是如何创建对象的(createStore
)
创建store的代码
const store = createStore(reducers, state, enhance);
createStore的源代码
export var ActionTypes = {
INIT: '@@redux/INIT'
}
export default function createStore(reducer, initialState, enhancer) {
// 省略异常处理
var currentReducer = reducer // 处理状态更新的函数
var currentState = initialState // 初始状态
var currentListeners = []
var nextListeners = currentListeners
var isDispatching = false
// 确保可以安全地修改 `nextListeners`:
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 返回当前state
function getState() {
return currentState
}
// 注册listener,同时返回一个取消事件注册的方法
// 当调用store.dispatch的时候调用listener
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected listener to be a function.')
}
var isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
isSubscribed = false
// 从nextListeners中去除掉当前listener
ensureCanMutateNextListeners()
var index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// dispatch方法接收的action是个对象,而不是方法。
// 这个对象实际上就是我们自定义action的返回值,因为dispatch的时候,已经调用过我们的自定义action了,比如 dispatch(addTodo())
function dispatch(action) {
// 省略异常处理
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 遍历调用各个linster
var listeners = currentListeners = nextListeners
for (var i = 0; i < listeners.length; i++) {
listeners[i]()
}
return action
}
// Replaces the reducer currently used by the store to calculate the state.
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.INIT })
}
// 当create store的时候,reducer会接受一个type为ActionTypes.INIT的action,使reducer返回他们默认的state,这样可以快速的形成默认的state的结构
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer
}
}
这么看起来,这个store也提供了一个订阅发布器的对象
当我们不使用react-redux库的时候用redux类似这样
import React, { useState, useEffect } from 'react';
import store from './store';
const App = () => {
// 使用本地 state 来保存 Redux state
const [count, setCount] = useState(store.getState().count);
// 订阅 store 的更新
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setCount(store.getState().count);
});
// 组件卸载时取消订阅
return () => unsubscribe();
}, []);
// 处理增加计数
const increment = () => {
store.dispatch({ type: 'INCREMENT' });
};
// 处理减少计数
const decrement = () => {
store.dispatch({ type: 'DECREMENT' });
};
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
export default App;
从上面的代码可以看出createStore干的几件事
- 它将咱们给它的initState作为了currentState
- 想拿到当前的currentState需要通过getState()来获取
- 更新currentState必须通过dispatch
- 内部维护了一个listeners,当有dispatch时会触发listeners里的事件(subscribe返回的值执行一次即可取消订阅)
当我们不用react-redux直接使用redux时也很像一个提供了订阅发布器的对象,我们不能直接修改这个对象,需要通过它定义好的事件来进行修改
那我们通过combineReducers将这些store整合到一起,这个listeners是如何整合的呢?
回顾一下combineReducers的使用
// 两个reducer
const A = (state = initStateA, actionA) => {
// ....
};
const B = (state = initStateB, actionB) => {
// ...
};
const appReducer = combineReducers({
A,
B
});
combineReducers 的工作原理
function combineReducers(reducers) {
return function combination(state = {}, action) {
let hasChanged = false;
const nextState = {};
for (let key in reducers) {
const reducer = reducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
return hasChanged ? nextState : state;
};
}
- 为每个子 reducer 分配对应的 state 部分。
- 调用每个子 reducer,传入对应的 state 部分和 action。
- 将每个子 reducer 的返回值合并成一个新的全局 state 对象。
- 检查 state 是否发生变化,如果发生变化,则返回新的 state,否则返回原来的 state。
每次 dispatch 一个 action 时,store 会调用所有的 listeners,即使只有部分 state 发生了变化。 让GPT生成一个触发listeners的例子:
import { createStore, combineReducers } from 'redux';
// 定义初始状态
const initialCounterState = { count: 0 };
const initialMessageState = { message: '' };
// 定义 reducer
function counterReducer(state = initialCounterState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
function messageReducer(state = initialMessageState, action) {
switch (action.type) {
case 'SET_MESSAGE':
return { ...state, message: action.message };
default:
return state;
}
}
// 使用 combineReducers 创建根 reducer
const rootReducer = combineReducers({
counter: counterReducer,
message: messageReducer
});
// 创建 Redux store
const store = createStore(rootReducer);
// 订阅 store
store.subscribe(() => {
console.log('State changed:', store.getState());
});
// Dispatch actions
store.dispatch({ type: 'INCREMENT' }); // State changed: { counter: { count: 1 }, message: { message: '' } }
store.dispatch({ type: 'SET_MESSAGE', message: 'Hello, Redux!' }); // State changed: { counter: { count: 1 }, message: { message: 'Hello, Redux!' } }
无论 dispatch 的 action 是影响 counter
还是 message
,只要全局 state 发生了变化,所有的 listeners 都会被调用。
到这咱们先了解redux在默认的情况下只要dispatch了就会触发所有的listeners
最后看看applyMiddleware
看看能不能解决在文章一开始提到的添加某类权限管理员的需求
中间件是一个函数,它接受 store
的 dispatch
和 getState
方法,并返回一个函数,这个函数又返回一个函数,最终这个最内层的函数接收 next
和 action
作为参数。
applyMiddleware
用于扩展 Redux 的dispatch
函数。 示例代码:
const loggerMiddleware = store => next => action => {
console.log('Dispatching:', action);
let result = next(action); // 调用下一个中间件或 reducer
console.log('Next state:', store.getState());
return result;
};
const asyncMiddleware = store => next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
};
可以使用 applyMiddleware
将中间件应用到 Redux store 中:
import { createStore, applyMiddleware } from 'redux';
// 简单的 reducer
const initialState = { value: 0 };
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, value: state.value + 1 };
case 'DECREMENT':
return { ...state, value: state.value - 1 };
default:
return state;
}
};
// 使用 applyMiddleware 将中间件应用到 store
const store = createStore(
reducer,
applyMiddleware(loggerMiddleware, asyncMiddleware)
);
applyMiddleware
的实现:
import compose from './compose';
function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
let chain = [];
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
};
}
export default applyMiddleware;
感觉将一些后期新增的业务逻辑放到middleware中也是可行的。
这样在之前的插件机制又多了中间件的选择。
比如需要管理员权限的就传入中间件,不需要的就不传,生成不同的store。
这样可以省去不少执行顺序的事。
总结
- 修改对象的属性的值,reudx通过纯函数的方式来修改对象的值,用户不直接操作对象,单例模式也可以通过
freeze
或者其他的方式来限制属性修改,目的都是为了解决数据未更新先消费数据的问题 - Redux订阅发布统一执行,比通过事件名来订阅的话会简单一些,但是要对执行事件做优化,减少无效的执行
- Redux这种能比较好的控制事件的执行顺序,比手动维护简单
- 能更好的避免副作用
当全局对象功能并不复杂,不需要修改属性,只有一些基础的异步更新,单例+发布订阅还是比较简单的。
后面再看看插件(webpack)。