react-redux源码分析

引言:

  1. redux采用闭包+订阅发布模式实现全局状态管理。
  2. react-redux 主要采用ReactContext将 redux store 共享给其他子组建。子组件使用 useSelector 订阅 store状态,该hook内部由 useSyncExternalStore和selector 实现,分别实现触发渲染和避免重复渲染的功能。使用useDispatch 更新store,然后触发一系列监听者,实现视图更新。

redux 实现原理

0. redux整体设计

在阅读源码前,我们先看看原生redux 是如何使用的:

js 复制代码
// 首先定义一个reducer
function count(state, action) {
    const defaultState = {
        year: 2015,
    };
    state = state || defaultState;
    switch (action.type) {
        case 'add':
            return {
                year: state.year + 1
            };
        case 'sub':
            return {
                year: state.year - 1
            }
        default:
            return state;
    }
}
// store的创建
const createStore = require('redux').createStore;
const store = createStore(count);
// store里面的数据发生改变时,触发的回调函数
store.subscribe(function () {
    // render dom
    console.log('the year is: ', store.getState().year);
});

// action: 触发state改变的唯一方法(按照reducer的第二个参数)
const action1 = { type: 'add' };
const action3 = { type: 'sub' };
// 改变store里面的值
store.dispatch(action1); // 'the year is: 2016
store.dispatch(action3); // 'the year is: 2015

从使用中可以很明显的看到 redux 的核心功能 createStore()

我们这就看下这个函数的实现方式:

【输入】:reducer、初始值

【输出】:getState() 、subscribe()、dispatch()

  • 利用将状态 保存在 currentState(闭包)中
  • 通过发布订阅模式 派发状态变更
js 复制代码
export function createStore(
  reducer, // 用户定义的 reducer
  preloadedState, // 初始值
  enhancer, // store增强, 本文暂不讨论
){
  // 1.参数处理,确保参数接收正确
  if(){
    ...
  }
  // 实现部分
  // 这里闭包变量,用于保存store的状态和监听器
  let currentReducer = reducer 
  let currentState = preloadedState
  let currentListeners =  new Map() // 存放listeners
  let nextListeners = currentListeners
  let listenerIdCounter = 0 // 
  let isDispatching = false // 是否处于 执行action 状态
  //使用 Map 存放 listener, 执行 subscribe 调用
  function ensureCanMutateNextListeners() {}
  // 获取当前状态
  function getState(){}
  // 添加订阅
  function subscribe(listener) {}
  // dispatch 方法
  function dispatch(action) {}
  // 替换reducer
  function replaceReducer(nextReducer) {}
  // 观察者模式
  function observable() {}
  // 执行reducer 初始化
  dispatch({ type: ActionTypes.INIT })
  const store = {
    dispatch,//用于 action 分发
    subscribe,//注册 listener,实现 state改变后,能够通知其他调用者
    getState, // 获取当前状态
    replaceReducer,// 替换reducer
    [$$observable]: observable
  }
  return store
}

接下来我们看下 getState() 、subscribe()、dispatch() 这个三个函数的实现

  1. getState()

【输入】:无

【输出】:全局状态

  • 获取闭包中的 state
js 复制代码
function getState {
    // 禁止在 reducer 中调用 getState
    if (isDispatching) {
      throw new Error()
    }
    return currentState
  }
  1. dispatch()

【输入】:action

【输出】:action

  • 执行reducer,触发订阅器
js 复制代码
//let currentReducer = reducer 
 function dispatch(action) {
    try {
      isDispatching = true // 标注 分发状态,避免在 在执行recuer 过程中使用 getState()
      // 每次状态更新都会获取一个全新的 全局状态对象
      currentState = currentReducer(currentState, action) // 执行传入 reducer;
    } finally {
      isDispatching = false
    }
    const listeners = (currentListeners = nextListeners)
    // 触发订阅者回掉函数,todo: 为什么不把 currentState 直接传递到listener中
    listeners.forEach(listener => { 
      listener()
    })
    return action
  }
  1. subscribe()

【输入】:listener

【输出】:取消订阅函数

js 复制代码
// 闭包变量
// let currentListeners = new Map()
// let nextListeners = currentListeners

// 这里使用 两个 listener 列表,为避免在 执行reducer 过程中重新添加新的listener 出现死循环导致栈溢出
function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) { // 在dispatch 中会同步 listeners
      nextListeners = new Map()
      currentListeners.forEach((listener, key) => {
        nextListeners.set(key, listener)
      })
    }
}
function subscribe(listener) {
    let isSubscribed = true // 设置某个订阅状态,用于标识是否被取消订阅
    ensureCanMutateNextListeners()
    const listenerId = listenerIdCounter++ // 设置监听器ID(key)
    nextListeners.set(listenerId, listener)
    // 返回 一个 取消监听器 的函数
    return function unsubscribe() {
      if (!isSubscribed) { // 避免多次调用 取消订阅,造成性能消耗
        return
      }
      isSubscribed = false 
      ensureCanMutateNextListeners()
      nextListeners.delete(listenerId)
      currentListeners = null
    }
}

2. 其他功能

  1. combinReducers()

随着应用越来越大,一方面,不能把所有数据都放到一个reducer里面,另一方面,为每个reducer创建一个store维护起来页比较麻烦,因此需要对reducer进行分片整合。

还是先从使用入手:

js 复制代码
// 创建两个reducer: count year
function count (state, action) {
  state = state || {count: 1}
  switch (action.type) {
    default:
      return state;
  }
}
function year (state, action) {
  state = state || {year: 2015}
  switch (action.type) {
    default:
      return state;
  }
}
// 将多个reducer合并成一个
var combineReducers = require('redux').combineReducers;
var rootReducer = combineReducers({
  count: count,
  year: year,
});
var createStore = require('redux').createStore;
var store = createStore(rootReducer);

源码实现:

【输入】:reducers 对象

【输出】:返回一个类似 reducer 的函数

  • 首先拷贝传入对象,避免直接引用导致reducers被以外更改,出现状态不一致问题
  • 遍历reducers 所有属性依次执行所有reducer
js 复制代码
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers= {} // 拷贝 reducers
  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
  try {
    assertReducerShape(finalReducers) // 验证reducer 参数
  } catch (e) {
    shapeAssertionError = e
  }
// 返回一个总reducers函数,其中联合了所有reducer,在触发dispatch 时通过遍历的方式 执行对应的action
// 实现功能: 
// 1. 整合所有 reducer 的状态为一个状态,使用reducers 的key 作为每个 state 的key;
// 2. 遍历调用所有 reducer 函数 
  return function combination(
    state = {},
    action
  ) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }
    let hasChanged = false
    // combineReducers 更新逻辑 遍历所有reducer
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      // 注意:根据传入 action 执行所有reducers ,因此会导致相同的type 都被执行 
      const nextStateForKey = reducer(previousStateForKey, action) 
      nextState[key] = nextStateForKey // 整合所有reducer 的状态
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 通过 === 确定是否改变状态
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}
  1. applyMiddleware()
  • 只是一个 compose() 的使用
js 复制代码
export default function applyMiddleware(...middlewares) {
  return createStore => (reducer, preloadedState) => { // 返回createStore 方法
    const store = createStore(reducer, preloadedState) // 创建 store
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI)) //TODO: 创建中间件链
    dispatch = compose(...chain)(store.dispatch) // 组合函数,创建链式调用,将中间件插在 dispatch 之前执行
    return {
      ...store,
      dispatch // dispatch 替换
    }
  }
}

// 再看看redux-thunk的实现, next就是store里面的上一个dispatch
function thunkMiddleware({ dispatch, getState }) {
  return next => action =>
    typeof action === 'function' ?
      action(dispatch, getState) :
      next(action);
 }

3. 总结

通过看源码发现redux 实现还是比较简单 闭包+订阅发布模式 ,这使得它能够整合在任意框架中。那么他是如何整合在react中的呢?

2. react-redux

1. 整体设计

整体设计:react-redux 主要采用ReactContext 将 redux store 共享给其他子组建。子组件使用 useSelector 订阅 store状态,该hook内部由 useSyncExternalStore和selector 实现,分别实现触发渲染和避免重复渲染的功能,使用useDispatch 更新store,然后触发一系列监听者,实现视图更新。

2. 核心实现 Provider & useEelector

在使用react-redux 都需要在组件树顶端添加 Provider 同时,将 store 作为参数传入,我们这就看看 Provider 做了什么?

1. Provider 组件

【输入】:

  • children: 子组件树
  • store: redux store对象
  • serverState: 支持SSR(本文不讨论)
  • context: react Context组件,自定义redux 要使用context

【输出】:Context组件

主要工作内容:

  1. 创建订阅器,这里之所以要再次创建一个订阅器,而不是直接使用redux 原生的订阅器,是因为Provider可以多次调用 但所有Provider却共享同一个Context对象(这个我们可以在 ReactReduxContext 中看到),这样通过增加中间订阅器的形式,可以快速实现Provider卸载后取消订阅。
  2. 全局共享同一个 Context对象,并将其挂在到 全局对象上便于获取。
  3. 监听 contextValue ,在组件更新时,实现更新订阅者的功能。
js 复制代码
// Provider 实现方式
function Provide(providerProps) {
// store 由 redux createStore 方法创建
  const { children, context, serverState, store } = providerProps 
// 定义Context 的存储对象
  const contextValue = React.useMemo(() => {
    // 创建 订阅器
    const subscription = createSubscription(store) 
    const baseContextValue = {
      store,
      subscription,
      getServerState: serverState ? () => serverState : undefined,
    }
    return baseContextValue
  }, [store, serverState])
// 保留状态,确定状态是否改变
  const previousState = React.useMemo(() => store.getState(), [store])
//根据环境(浏览器或者 native)使用 useEffect 或者 useLayoutEffect
  useIsomorphicLayoutEffect(() => {
    const { subscription } = contextValue
    subscription.onStateChange = subscription.notifyNestedSubs //指定订阅stroe 的回掉
    subscription.trySubscribe() // 创建订阅器
    if (previousState !== store.getState()) {// store 变更时主动 Provider的通知订阅者
      subscription.notifyNestedSubs() 
    }
    return () => {
      subscription.tryUnsubscribe()
      subscription.onStateChange = undefined
    }
  }, [contextValue, previousState])
  const Context = context || ReactReduxContext // 创建Context 保存 store
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
  1. ReactReduxContext 实现源码

该变量的主要功能是为Provider组件提供Context对象

js 复制代码
const ContextKey =  Symbol.for(`react-redux-context`)
// 获取全局对象
const gT = (
  typeof globalThis !== 'undefined'
    ? globalThis
    : {}
) 
function getContext(){
  if (!React.createContext) return {} 
// 这里将 Context挂载到全局 对象中,便于子组建能够在任何位置获取 Context对象
  const contextMap = (gT[ContextKey] ??= new Map()) 
  let realContext = contextMap.get(React.createContext)
  if (!realContext) {
    realContext = React.createContext(
      null,
    )
    if (process.env.NODE_ENV !== 'production') {
      realContext.displayName = 'ReactRedux'
    }
    contextMap.set(React.createContext, realContext)
  }
  return realContext;
}
export const ReactReduxContext = /*#__PURE__*/ getContext()
  1. createSubscription(store)

创建一个订阅器,该订阅器收集来自vdom对状态的订阅,通过监听store 的变化,执行vdom的执行回掉。

tip: 可以先看 useEelector 的实现,在返回来看这里跟容易理解

js 复制代码
const nullListeners = {
  notify() {},
  get: () => [],
}
// 发布订阅模式
export function createSubscription(store, parentSub) { // 这里的parentSub 主要目的是为了 类组件 在使用 connect时能够将子组件都传递到同一个Provider 订阅器上。
  let unsubscribe // 取消 store订阅的函数(streo.subscribe的返回值)
  let listeners = nullListeners
  let subscriptionsAmount = 0 // 订阅计数器
  let selfSubscribed = false
  // 添加订阅:useSelector 订阅 Provider 时使用的方法
  function addNestedSub(listener) {
    trySubscribe()
    const cleanupListener = listeners.subscribe(listener)
    // cleanup nested sub
    let removed = false
    return () => {
      if (!removed) {
        removed = true
        cleanupListener()
        tryUnsubscribe()
      }
    }
  }
// 执行 订阅回掉
  function notifyNestedSubs() {
    listeners.notify()
  }
// 订阅stoer变更时的回掉
  function handleChangeWrapper() {
    if (subscription.onStateChange) {
      subscription.onStateChange()
    }
  }

  function isSubscribed() {
    return selfSubscribed
  }
// 订阅 store
  function trySubscribe() {
    subscriptionsAmount++
    if (!unsubscribe) { // 检测是否已经订阅
      unsubscribe = parentSub // 订阅store
        ? parentSub.addNestedSub(handleChangeWrapper)
        : store.subscribe(handleChangeWrapper) 

      listeners = createListenerCollection()
    }
  }
// 取消订阅 store
  function tryUnsubscribe() {
    subscriptionsAmount--
    if (unsubscribe && subscriptionsAmount === 0) {
      unsubscribe()
      unsubscribe = undefined
      listeners.clear()
      listeners = nullListeners
    }
  }

  function trySubscribeSelf() {
    if (!selfSubscribed) {
      selfSubscribed = true
      trySubscribe()
    }
  }

  function tryUnsubscribeSelf() {
    if (selfSubscribed) {
      selfSubscribed = false
      tryUnsubscribe()
    }
  }
  const subscription = {
    addNestedSub,
    notifyNestedSubs,
    handleChangeWrapper,
    isSubscribed,
    trySubscribe: trySubscribeSelf,
    tryUnsubscribe: tryUnsubscribeSelf,
    getListeners: () => listeners,
  }
  return subscription
}
// 订阅器 监听列表 双链结构
function createListenerCollection() {
  return {
// 清空订阅列表
    clear() {},
// 触发订阅回调
    notify() {},
// 获取订阅列表
    get() {},
// 添加订阅,返回取消订阅的方法
    subscribe(callback) {},
  }
}
2. useSelector

【输入】:

  • selector: store分片函数
  • equalityFnOrOptions: 状态比较函数

【输出】:

  • state: 一个状态对象,变更后触发渲染
js 复制代码
export const useSelector =createSelectorHook()
const refEquality = (a, b) => a === b 
// useSelector 核心实现
export function createSelectorHook(context = ReactReduxContext) {
// 由useContext 实现,通过全局对象获取 Context 上的 value
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : createReduxContextHook(context) // 源码实现在下文
// 返回的 useSelector 函数内容
  const useSelector = (selector, equalityFnOrOptions = {}) => {
    const { equalityFn = refEquality } =
      typeof equalityFnOrOptions === 'function'
        ? { equalityFn: equalityFnOrOptions }
        : equalityFnOrOptions
    const reduxContext = useReduxContext()
    // 结构 value 值
    const { store, subscription, getServerState } = reduxContext
    //封装 selector函数
    const wrappedSelector = React.useCallback(
    // 先定义一个对象,然后通过【】获取属性,为获取其动态方法名,便于调试
      {
        [selector.name](state) { 
          const selected = selector(state)
          return selected
        },
      }[selector.name], 
      [selector],
    )
    // useSyncExternalStoreWithSelector 核心实现为 react 提供的 useSyncExternalStore
    // 这里是避免重复渲染的关键
    const selectedState = useSyncExternalStoreWithSelector(
      subscription.addNestedSub,
      store.getState,
      getServerState || store.getState,
      wrappedSelector,
      equalityFn,
    )
    React.useDebugValue(selectedState)
    return selectedState
  }
  //辅助ts进行类型推断
  Object.assign(useSelector, {
    withTypes: () => useSelector,
  })
  return useSelector 
}
  1. createReduxContextHook
js 复制代码
export function createReduxContextHook(context = ReactReduxContext) {
  return function useReduxContext(): ReactReduxContextValue {
    const contextValue = React.useContext(context)
    return contextValue!
  }
}
  1. useSyncExternalStoreWithSelector

【输入】

  1. subscribe:一个订阅器,添加订阅的方法
  2. getSnapshot: 获取状态值的方法
  3. getServerSnapshot: ssr 下获取状态值的方法
  4. seletor:切片函数,避免重复渲染的关键,实现更细粒度的状态监听
  5. isEqual: 状态比较函数,默认 ===

【输出】:一个 状态值

  • 该hook是实现触发组件渲染和避免重复渲染的关键,其核心依赖react18 提供的新 hook useSyncExternalStore。 因此大家去研究一下 useSyncExternalStore的使用,就能理解这个函数的作用。到这里就能理解Provider创建 订阅器的原因了,既useSyncExternalStore 订阅 Provider,而Provider订阅 store。当store发生变更后会触发 Provider订阅回掉执行,而Provider订阅回掉又会执行 useSyncExternalStore 的订阅回掉,从而实现组件render,更新状态。至于为什么该hook不直接订阅store,可能是为了Provider能够完全卸载吧。(嵌套Provider使用的很多吗?)
  • Selector切片原理就是创建更细粒度的状态值
  • 注意这里使用了 subscription.addNestedSub,这个方法现在我们可以回到Provider中 Subscribtion的实现了。
js 复制代码
function is(x, y) {
  return (x === y && (0 !== x || 1 / x === 1 / y)) || (x !== x && y !== y);
}
var objectIs = "function" === typeof Object.is ? Object.is : is,
  useSyncExternalStore = React.useSyncExternalStore,
  useRef = React.useRef,
  useEffect = React.useEffect,
  useMemo = React.useMemo,
  useDebugValue = React.useDebugValue;
  
export useSyncExternalStoreWithSelector = function (
  subscribe,
  getSnapshot,
  getServerSnapshot,
  selector,
  isEqual
) {
  var instRef = useRef(null);
  if (null === instRef.current) {
    var inst = { hasValue: !1, value: null };
    instRef.current = inst;
  } else inst = instRef.current;
// 创建 Seletor 切片函数
  instRef = useMemo(
    function () {
      function memoizedSelector(nextSnapshot) {
        if (!hasMemo) {
          hasMemo = !0;
          memoizedSnapshot = nextSnapshot;
          nextSnapshot = selector(nextSnapshot);
          if (void 0 !== isEqual && inst.hasValue) {
            var currentSelection = inst.value;
            if (isEqual(currentSelection, nextSnapshot))
              return (memoizedSelection = currentSelection);
          }
          return (memoizedSelection = nextSnapshot);
        }
        currentSelection = memoizedSelection;
        // 判断 原快照 是否改变
        if (objectIs(memoizedSnapshot, nextSnapshot)) return currentSelection;
        var nextSelection = selector(nextSnapshot);
        // 判断 selector 后的 快照是否被改变
        if (void 0 !== isEqual && isEqual(currentSelection, nextSelection)) 
          return (memoizedSnapshot = nextSnapshot), currentSelection;
        memoizedSnapshot = nextSnapshot;
        return (memoizedSelection = nextSelection);
      }

      var hasMemo = !1,// 是否第一次获取 快照
        memoizedSnapshot, // selector 前的快照
        memoizedSelection,// selector 后的快照
        maybeGetServerSnapshot =
          void 0 === getServerSnapshot ? null : getServerSnapshot;
      return [
        function () {
          return memoizedSelector(getSnapshot());
        },
        null === maybeGetServerSnapshot
          ? void 0
          : function () {
              return memoizedSelector(maybeGetServerSnapshot());
            }
      ];
    },
    [getSnapshot, getServerSnapshot, selector, isEqual]
  );
  // react18 useSyncExternalStore 
  var value = useSyncExternalStore(subscribe, instRef[0], instRef[1]);
  useEffect(
    function () {
      inst.hasValue = !0;
      inst.value = value;
    },
    [value]
  );
  useDebugValue(value);
  return value;
};

3.其他功能

useDispatch 和 useStore hook 就比较简单,直接使用useContext 从context 获取 store 方法,这里就不讲了,直接看源码。

  1. useDispatch()
js 复制代码
export const useDispatch = createDispatchHook()
// 获取 redux dispatch 方法
export function createDispatchHook(context = ReactReduxContext) {
  const useStore = // 获取 redux store 对象 用于拿去dispatch方法
    context === ReactReduxContext ? useDefaultStore : createStoreHook(context) 

  const useDispatch = () => {
    const store = useStore()
    return store.dispatch
  }

  Object.assign(useDispatch, {
    withTypes: () => useDispatch,
  })

  return useDispatch 
}
  1. useStore()
js 复制代码
export const useStore = createStoreHook()
// 返回 redux 的 store 对象
export function createStoreHook(context = ReactReduxContext) {
  // 获取 Context value 的值,核心方法见 getContext()
  const useReduxContext =
    context === ReactReduxContext
      ? useDefaultReduxContext
      : createReduxContextHook(context)
  const useStore = () => {
    const { store } = useReduxContext() //
    return store
  }
 // 扩展 useStore ,便于 ts 类型推断
  Object.assign(useStore, {
    withTypes: () => useStore,
  })
  return useStore
}

4.总结

react-redux 其实就是对redux 的封装,将redux的store 保存在 Context 中供react子组件使用,同时使用SelectoruseSyncExternalStore 监听全局状态的更改,实现渲染组件和避免重复渲染的问题。

相关推荐
小希爸爸4 分钟前
2、中医基础入门和养生
前端·后端
局外人LZ8 分钟前
前端项目搭建集锦:vite、vue、react、antd、vant、ts、sass、eslint、prettier、浏览器扩展,开箱即用,附带项目搭建教程
前端·vue.js·react.js
G_GreenHand22 分钟前
Dhtmlx Gantt教程
前端
鹿九巫23 分钟前
【CSS】层叠,优先级与继承(四):层叠,优先级与继承的关系
前端·css
卓怡学长25 分钟前
w304基于HTML5的民谣网站的设计与实现
java·前端·数据库·spring boot·spring·html5
宝拉不想努力了28 分钟前
vue element使用el-table时,切换tab,table表格列项发生错位问题
前端·vue.js·elementui
YONG823_API32 分钟前
深度探究获取淘宝商品数据的途径|API接口|批量自动化采集商品数据
java·前端·自动化
鱼樱前端33 分钟前
前端必知必会:JavaScript 对象与数组克隆的 7 种姿势,从浅入深一网打尽!
前端·javascript
小希爸爸1 小时前
1、中医基础入门和养生
前端·后端
神仙别闹2 小时前
基于VUE+Node.JS实现(Web)学生组队网站
前端·vue.js·node.js