一文搞懂 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 应该能够优雅地处理这些情况。

相关推荐
萌萌哒草头将军5 小时前
⚡⚡⚡尤雨溪宣布开发 Vite Devtools,这两个很哇塞 🚀 Vite 的插件,你一定要知道!
前端·vue.js·vite
_一条咸鱼_5 小时前
揭秘 Android TextInputLayout:从源码深度剖析其使用原理
android·java·面试
_一条咸鱼_6 小时前
揭秘!Android VideoView 使用原理大起底
android·java·面试
_一条咸鱼_6 小时前
深度揭秘!Android TextView 使用原理全解析
android·java·面试
小彭努力中6 小时前
7.Three.js 中 CubeCamera详解与实战示例
开发语言·前端·javascript·vue.js·ecmascript
_一条咸鱼_6 小时前
深度剖析:Android Canvas 使用原理全揭秘
android·java·面试
_一条咸鱼_6 小时前
深度剖析!Android TextureView 使用原理全揭秘
android·java·面试
_一条咸鱼_6 小时前
揭秘!Android CheckBox 使用原理全解析
android·java·面试