一文搞懂 redux 核心原理

在介绍 redux 核心原理之前,先整体回顾一下 redux 的核心方法

  • createStore 是创建 Store 的入口,也是实现发布-订阅模式的核心
  • 通过 getState 方法能够获取到当前的状态
  • 通过 subscribe 方法注册订阅回调函数,通过 dispatch 执行 Reducer 更新状态并触发订阅者函数执行,实现发布-订阅模式
  • 通过 Observable 方法将 Store 转化为一个可观察对象

createStore

createStore 方法核心实现步骤如下

  • 定义了 getState、subscribe、dispatch、observable 方法,用于实现获取 Store 的状态和发布-订阅模式

  • 通过 dispatch 方法初始化所有状态

  • 最后返回包含 getState、subscribe、dispatch、observable 方法的 store 对象

我们现初始化 createStore

js 复制代码
const randomString = () =>
  Math.random().toString(36).substring(7).split('').join('.');

const ActionTypes = {
  INIT: `@@redux/INIT${/* #__PURE__ */ randomString()}`,
  REPLACE: `@@redux/REPLACE${/* #__PURE__ */ randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`,
};

const $$observable = (() =>
  (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();

export function createStore(reducer, preloadedState, enhancer) {
  // 定义状态变量
  let currentReducer = reducer;
  let currentState = preloadedState;
  let isDispatching = false
  
  // 获取当前状态快照
  function getState() {
    //
  }

  // 注册回调函数
  function subscribe(listener) {
    //
  }

  // 派发 action,触发状态更新
  function dispatch(action) {
    //
  }

  // 实现了 Observable 协议的函数,使得 Store 可以被观察
  function observable() {
    //
  }

  // 创建 store 后初始化所有状态
  dispatch({ type: ActionTypes.INIT });

  const store = {
    dispatch
    subscribe,
    getState,
    [$$observable]: observable,
  }

  return store
}

getState

getState 方法比较简单,就是返回了当前的状态 currentState

js 复制代码
/**
 * Reads the state tree managed by the store.
 *
 * @returns The current state tree of your application.
 */
function getState(): S {
  if (isDispatching) {
    throw new Error(
      'You may not call store.getState() while the reducer is executing. ' +
        'The reducer has already received the state as an argument. ' +
        'Pass it down from the top reducer instead of reading it from the store.'
    )
  }

  return currentState as S
}

subscribe

subscribe 方法用于订阅者注册回调函数,有几个关键点

  • 回调函数被存储在一个 Map 对象中,但是在方法中却定义了两个 Map 对象: currentListeners 和 nextListeners,这是为了防止在 dispatch 过程中对订阅者列表进行修改引起的潜在问题
  • 在添加或删除订阅方法前,都会通过 ensureCanMutateNextListeners 方法创建一份 nextListeners 副本,添加和删除操作都是在 nextListeners 副本上进行
  • 当需要通知订阅者时(即执行 dispatch 方法),通过将 nextListeners 复制到 currentListeners,再遍历 currentListeners 触发所有订阅者执行
  • 在 subscribe 方法执行时,为每个订阅者分配了一个唯一的 id(即 listenerId),在取消订阅的 unsubscribe 方法中,通过闭包能够访问到定义的 listenerId,从而实现移除监听器的效果
js 复制代码
// 当前的订阅回调函数
let currentListeners: Map<number, ListenerCallback> | null = new Map()
// 下一个状态的订阅回调函数
let nextListeners = currentListeners
// 生成唯一的订阅回调函数的 ID
let listenerIdCounter = 0

function ensureCanMutateNextListeners() {
  if (nextListeners === currentListeners) {
    nextListeners = new Map()
    currentListeners.forEach((listener, key) => {
      nextListeners.set(key, listener)
    })
  }
}

// 注册回调函数
function subscribe(listener) {
  if (typeof listener !== 'function') {
    throw new Error(
      `Expected the listener to be a function. Instead, received: '${kindOf(
        listener
      )}'`
    )
  }

  if (isDispatching) {
    throw new Error(
      'You may not call store.subscribe() while the reducer is executing. ' +
        'If you would like to be notified after the store has been updated, subscribe from a ' +
        'component and invoke store.getState() in the callback to access the latest state. ' +
        'See https://redux.js.org/api/store#subscribelistener for more details.'
    )
  }

  // 标记为已订阅状态
  let isSubscribed = true

  // 避免直接修改 currentListeners
  ensureCanMutateNextListeners()
  // 为每个订阅者分配一个唯一的 id
  const listenerId = listenerIdCounter++
  // 将监听器添加到 nextListeners
  nextListeners.set(listenerId, listener)

  // 返回一个取消订阅函数
  // 实现逻辑就是通过 id 从 nextListeners 移除监听器
  return function unsubscribe() {
    if (!isSubscribed) {
      return
    }

    if (isDispatching) {
      throw new Error(
        'You may not unsubscribe from a store listener while the reducer is executing. ' +
          'See https://redux.js.org/api/store#subscribelistener for more details.'
      )
    }

    isSubscribed = false

    ensureCanMutateNextListeners()
    nextListeners.delete(listenerId)
    currentListeners = null
  }
}

dispatch

dispatch 方法用于执行 Reducer 更新状态并通知所有订阅者执行,具体实现有两个关键点

  • 在执行 reducer 更新状态前,会通过 isDispatching 设置为 true 的方式加锁,确保不会执行其他 action,reducer 更新状态结束后,再关闭锁
  • 在遍历通知订阅者前,将 nextListeners 复制到 currentListeners 再遍历执行,避免对于 nextListeners 的操作造成订阅者执行错误
js 复制代码
let currentReducer = reducer;

// 派发 action,触发状态更新
function dispatch(action) {
  // 确保不会执行其他 action
  if (isDispatching) {
    throw new Error('Reducers may not dispatch actions.')
  }

  try {
    // isDispatching 设置为 true,相当于加锁
    isDispatching = true

    // 执行 reducer 函数更新状态
    currentState = currentReducer(currentState, action)
  } finally {
    // 结束 dispatch 执行关闭锁
    isDispatching = false
  }

  // 修改状态后,触发所有订阅者执行
  // 注意这里将 nextListeners 复制到 currentListeners 再遍历执行,
  // 避免对于 nextListeners 的操作造成订阅者执行错误
  const listeners = (currentListeners = nextListeners)
  listeners.forEach(listener => {
    listener()
  })

  // 返回 action,符合 dispatch 的标准行为
  return action
}

observable

observable 方法将 Store 转换为一个符合 Observable 提案的可观察对象,具体实现有两个关键点

  • 定义 observeState 方法观察 observer 对象状态的变化,通过 observer 的 next 属性传入当前的状态

  • 立即执行一次 observeState 方法确保当前状态被观察,然后调用 subscribe 方法注册当前观察者,这样当后续的状态变化时,都能够通过发布-订阅模式获取到最新的状态

js 复制代码
// 实现了 Observable 协议的函数,使得 Store 可以被观察
function observable() {
  // 引用 store 的 subscribe 方法
  const outerSubscribe = subscribe;

  return {
    // 实现 subscribe 方法,接收一个 observer 对象
    subscribe(observer: unknown) {
      if (typeof observer !== 'object' || observer === null) {
        throw new TypeError(
          `Expected the observer to be an object. Instead, received: '${kindOf(
            observer
          )}'`
        );
      }

      // 定义一个函数来观察状态变化
      function observeState() {
        const observerAsObserver = observer;
        // 确保 observer 有 next 方法
        if (observerAsObserver.next) {
          // 调用 next 方法并传入当前状态
          observerAsObserver.next(getState());
        }
      }

      // 立即执行一次以确保当前状态被观察
      observeState();
      // 调用外部的 subscribe 方法注册观察者
      const unsubscribe = outerSubscribe(observeState);
      // 返回一个取消订阅的方法
      return { unsubscribe };
    },

    // 返回 observable 对象本身
    [$$observable]() {
      return this;
    },
  };
}

enhancer

嫌 createStore 不够强大,我们可以通过 enhancer 在外层包裹 createStore,加强 createStore 的能力,一般我们都把它们叫做中间件,这个后面会提到

js 复制代码
export function createStore(reducer, preloadedState, enhancer) {
  // ...

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
    preloadedState = undefined
  }

  // 增强器
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error(
        `Expected the enhancer to be a function. Instead, received: '${kindOf(
          enhancer
        )}'`
      )
    }

    return enhancer(createStore)(
      reducer,
      preloadedState as PreloadedState | undefined
    )
  }


  // ...
}

// ********* 我们举个例子,我们可以通过这个加强 dispatch
function enhancer(createStore) {
  return function (reducer, preloadedState) {
    const store = createStore(reducer, preloadedState)
    const dispatch = store.dispatch
    
    const _dispatch = (actions) => {
      if (typeof actions === 'function') {
        return actions(dispatch)
      }

      return dispatch(actions)
    }

    return {
      ...store,
      dispatch: _dispatch
    }

  };
}

applyMiddleware

middleware是使用自定义功能扩展Redux的建议方法,applyMiddleware的作用就是将这些中间件组合起来,其中每个中间件不需要知道链中它之前或之后的内容。

applyMiddleware 最核心的作用就是增强 dispatch

js 复制代码
// 这里的 next 参数就是 dispatch,中间件有点像洋葱模型
function logger({ getState }) {
  return function (next) {
    return function(action) {
      console.log('will dispatch', action);
  
      const returnValue = next(action);
  
      console.log('state after dispatch', getState());
      return returnValue;
    } 
  };
}

const thunk = (store) => (next) => (action) => {
  console.log('thunk');

  return next(action);
};

const store = createStore(counterReducer, applyMiddleware(logger, thunk));

这里我用 logger 和 thunk 来讲解 applyMiddleware

js 复制代码
function applyMiddleware(...middlewares) {
  /*
    我们在调用 createStore 的时候,会优先判断是否有 enhancer
    if (typeof enhancer !== 'undefined') {
      return enhancer(createStore)(
        reducer,
        preloadedState as PreloadedState | undefined
      )
    }

    这里的参数就是这么来的
  */
  return (createStore) => (reducer, preloadedState) => {
    // 创建 store
    const store = createStore(reducer, preloadedState);
    let dispatch: Dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
        'Other middleware would not be applied to this dispatch.'
      );
    };

    // 阉割版的 store
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args),
    };

    // 将中间件的函数保存到数组中
    const chain = middlewares.map((middleware) => middleware(middlewareAPI));
    // 增强 dispatch
    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch,
    };
  };
}

function compose(...funcs) {
  if (funcs.length === 0) {
    // infer the argument type so it is usable in inference down the line
    return (arg) => arg;
  }

  if (funcs.length === 1) {
    return funcs[0];
  }

  // 洋葱模型,逐个执行
  return funcs.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args))
  );
}

export default applyMiddleware;

我们可以看到 chain 就是 logger 和 thunk 里面的函数,并且在 compose 中,我们可以发现中间件是逐个调用,我这里定义一个 counterReducer,再触发action

js 复制代码
function counterReducer(
  state = { value: 0 },
  action: { type: string; payload: any }
) {
  // console.log(action);

  switch (action.type) {
    case 'incremented':
      return { value: state.value + 1 };
    case 'decremented':
      return { value: state.value - 1 };
    default:
      return state;
  }
}

const store = createStore(counterReducer, applyMiddleware(logger, thunk));

store.dispatch({ type: 'decremented' });

下面是输出结果

bindActionCreators

bindActionCreators 的功能是将第一个参数对象转换为同名 key 对象,然后可以很方便地使用 dispatch

我们先看看我们平常是怎么调用它的

js 复制代码
const actions = bindActionCreators(
  {
    incremented: () => ({ type: 'incremented' }),
    decremented: () => ({ type: 'decremented' }),
  },
  store.dispatch
)

显然可以看到 bindActionCreators 是有两个参数组成,第一个参数是action集合,第二个是 dispatch,其结果就是返回一个函数,函数内部帮我们调用 dispatch

js 复制代码
function bindActionCreator<A extends Action>(
  actionCreator: ActionCreator<A>,
  dispatch: Dispatch<A>
) {
  return function (this: any, ...args: any[]) {
    return dispatch(actionCreator.apply(this, args))
  }
}

export default function bindActionCreators(
  actionCreators: ActionCreator<any> | ActionCreatorsMapObject,
  dispatch: Dispatch
) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, but instead received: '${kindOf(
        actionCreators
      )}'. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  const boundActionCreators: ActionCreatorsMapObject = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

具体实现有三个关键点

  • 如果参数 actionCreators 类型为 function,则直接执行这个函数。
  • 如果参数 actionCreators 类型不是非 null 的对象,抛错;
  • 如果参数 actionCreators 类型为非 null 对象,则浅复制一份给变量 boundActionCreators。返回值为 boundActionCreators 。其目的是将参数对象转换成一个同名 key 对象。

combineReducers

在了解 combineReducers 实现之前,我们先看看我们平常是怎么调用的

js 复制代码
const theDefaultReducer = (state = 0, action) => {
  switch (action.type) {
    case 'incremented':
      return { value: state.value + 1 };
    case 'decremented':
      return { value: state.value - 1 };
    default:
      return state;
  }
}
const firstNamedReducer = (state = 1, action) => {/* */}
const secondNamedReducer = (state = 2, action) => {/* */}


const rootReducer = combineReducers({
  default: theDefaultReducer,
  first: firstNamedReducer,
  second: secondNamedReducer
})

我们可以从代码中看出,combineReducers 就是将多个 reducer 组合成一个大的 reducer

js 复制代码
const randomString = () =>
  Math.random().toString(36).substring(7).split('').join('.')

const ActionTypes = {
  INIT: `@@redux/INIT${/* #__PURE__ */ randomString()}`,
  REPLACE: `@@redux/REPLACE${/* #__PURE__ */ randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);

  // 只记录 reducer 函数
  const finalReducers = {};
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i];

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key];
    }
  }

  const finalReducerKeys = Object.keys(finalReducers);

  let shapeAssertionError: unknown;
  try {
    // 验证 reducer 是否符合要求
    assertReducerShape(finalReducers);
  } catch (e) {
    shapeAssertionError = e;
  }

  return function combination(state, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError;
    }

    let hasChanged = false;
    // 新的 state
    const nextState = {};
    for (let i = 0; i < finalReducerKeys.length; i++) {
      // 获取当前的 reducerKey
      const key = finalReducerKeys[i];
      // 获取当前的 reducer
      const reducer = finalReducers[key];
      // 获取当前 reducer 对应的 state 值
      const previousStateForKey = state[key];
      // 触发 reducer 的函数,返回新的值
      const nextStateForKey = reducer(previousStateForKey, action);

      if (typeof nextStateForKey === 'undefined') {
        const actionType = action && action.type;
        throw new Error(/* */)
      }

      nextState[key] = nextStateForKey;

      // 判断 state 是否有变化
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }

    // 判断是否有变化,没有则返回旧的 state,有则返回新的 state
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length;
    return hasChanged ? nextState : state;
  };
}

function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    // 如果传入 reducer 的状态是 undefined(即在应用初始化时),reducer 应该返回一个初始状态
    const initialState = reducer(undefined, { type: ActionTypes.INIT })
    
    if (typeof initialState === 'undefined') {
     throw new Error(/* */)
    }

    // 验证未知 action 类型的处理,对于任何未知的 action 类型,reducer 应该返回当前状态
    if (
      typeof reducer(undefined, {
        type: ActionTypes.PROBE_UNKNOWN_ACTION()
      }) === 'undefined'
    ) {
      throw new Error(/* */)
    }
  })
}

ActionTypes.PROBE_UNKNOWN_ACTION() 是一个返回具有随机类型的 action 创建函数的函数。这是为了确保我们不是在测试 reducer 对特定、已知类型的 action 的响应,而是测试它对完全未知的 action 类型的响应。这是因为在实际应用中,可能会有新的、未知的 action 类型被分派,而 reducer 应该能够优雅地处理这些情况。

相关推荐
undefined&&懒洋洋7 分钟前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者2 小时前
React 19 新特性详解
前端
随云6322 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6322 小时前
WebGL编程指南之进入三维世界
前端·webgl
J老熊2 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
我爱学Python!3 小时前
面试问我LLM中的RAG,秒过!!!
人工智能·面试·llm·prompt·ai大模型·rag·大模型应用
OLDERHARD3 小时前
Java - LeetCode面试经典150题 - 矩阵 (四)
java·leetcode·面试
寻找09之夏3 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
银氨溶液4 小时前
MySql数据引擎InnoDB引起的锁问题
数据库·mysql·面试·求职
多多米10054 小时前
初学Vue(2)
前端·javascript·vue.js