通过比较单例模式再学习一次redux(副作用方面)

114 阅读7分钟

背景

最近在学习的过程中遇到一个问题:"Redux 的设计理念虽然不能说百分之百避免状态管理中的副作用,但是从很大程度上说,它要比单例模式更加有效的多"。这是在极客时间学习时老师提出的观点。
结合最近在项目中有用单例模式来管理权限的业务,所以打算多了解一下并且和大家分享讨论。
先交代一下业务背景

业务背景

简单理解就两点:

  1. 异步获取业务权限,一个对象key是权限点,value是boolean类型的值(可以简单理解接口会返回业务权限点的true/false)
  2. 根据权限前端对页面上的按钮进行控制(是否禁用)

当前的设计方案(SDK)

  1. 整个权限对象为一个单例对象
  2. 对象提供发布订阅器,通过事件的方式来通知订阅的对象的key的权限
  3. 对象的值的更新会触发相应的事件

业务方通过订阅权限点(触发事件的名称)来拿到最新的值。
全局对象会在数据更新时发布数据的更新事件(事件名称也是权限点)

即将遇到的问题

可以遇见的问题

  1. 事件会随着业务的迭代事件将会变多
  2. 事件的执行顺序的控制(数据未更新先消费数据,而导致异常)

比如最近就需要加入某些权限点的管理员权限,如果是管理员可以不用判断单独的权限。这样可能导致上面的问题,业务方又需要订阅管理员权限了?两个都是异步事件不能给业务方来设置吧。
这样就想引入插件,通过插件来控制,但是觉得有点复杂,正好就再看看redux的实现。

从上面思考的角度来学习redux

先温习一下Redux三个原则:

  1. 全局的状态都在一个 store 里保存
  2. 这个 store 里的状态对于整个应用来说都是只读的
  3. 如果需要更新改变状态的话,则需要通过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干的几件事

  1. 它将咱们给它的initState作为了currentState
  2. 想拿到当前的currentState需要通过getState()来获取
  3. 更新currentState必须通过dispatch
  4. 内部维护了一个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;
  };
}
  1. 为每个子 reducer 分配对应的 state 部分。
  2. 调用每个子 reducer,传入对应的 state 部分和 action。
  3. 将每个子 reducer 的返回值合并成一个新的全局 state 对象。
  4. 检查 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

看看能不能解决在文章一开始提到的添加某类权限管理员的需求
中间件是一个函数,它接受 storedispatchgetState 方法,并返回一个函数,这个函数又返回一个函数,最终这个最内层的函数接收 nextaction 作为参数。

  1. 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)。