Redux,React-Redux工作原理以及源码解析

Redux

简述

Redux 最本质的东西是一个 store 对象,store向外暴露以下方法,用来获取/更新的 state,以及订阅 state 的更新,本质上就是一个发布/订阅。

下面三行伪代码来简单描述出来 redux 的工作原理:

scss 复制代码
// 创建一个 store
const store = createStore(reducer);
// 订阅 store 中 state 变化,state 更新后执行回调 `console.log(store.getState()`
store.subscribe(() => console.log(store.getState()));
// 派发一个更新行为,用来更新 store 内部的 state
store.dispatch(action);

下图为 redux 官方展示的 Redux 内部的工作原理图,但是我认为并不是很清晰,没有表明如何 dispatch 之后如何更新 UI,那就是 subscribe 操作:

个人理解如下图:

解释:

  1. UI 订阅了 store 内部 state 变化
  2. UI 事件触发了dispatch({ type: 'xxx', payload: yyy })
  3. 执行 dispatch 时,内部先执行 reducer
  4. reducer 通过 dispatch 过来的 action.type 和 store 内部上一次的state,来更新state,注意这个更新 state 是全量替换的,如何 state 是一个结构复杂的对象,也是重新构造一个对象,而不是直接修改原来 state 的属性
  5. 更新 state 之后,通知 UI,Store 发生变化,更新 UI

源码解析

store 对象是由 Redux 中的 createStore 方法创建(工厂模式?)点击看源码

可以看到 store 对象中其实只有少量的几个属性,我们这里先关注最核心的三个属性:

  • dispatch: 更新 store 内部数据,查看源码
  • subscribe: 订阅 store 中 state 的变化,查看源码
  • getState: 获取 store 内部 state 值

注意这里并没有直接导出 state 属性,通过 createStore 时在内部维护 currentState,然后通过 getState 返回,形成了闭包,防止直接修改。

React-Redux

在上文中,简单描述了工作原理:发布订阅

但是在 react 使用 redux 时,是如何做到 dispatch 之后可以重新渲染组件呢

个人的思路就是在组件挂载时,订阅 store 更新,更新之后 render 组件(通过 useState 来实现 rerender)

纯Redux版本

  1. 在函数组件内部直接使用 useState(store.getState().count) 来初始化一个组件内部的 state
  2. 在一个 useEffect 中订阅 store 中 state 的变化,回调函数是 setCouter 然后重新渲染 react
typescript 复制代码
import { useEffect, useState } from "react";
import { createStore } from "redux";

const store = createStore<
  { count: number },
  { type: string; payload?: number }
>((state = { count: 0 }, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    default:
      return state;
  }
});

function App() {
  const [count, setCount] = useState(store.getState().count);

  useEffect(() => {
    const rerender = () => {
        setCount(store.getState().count);
    }
    const unsubscribe = store.subscribe(rerender);
    return unsubscribe
  }, [store.subscribe]);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => store.dispatch({ type: "INCREMENT" })}>
        Increment
      </button>
      <button onClick={() => store.dispatch({ type: "DECREMENT" })}>
        Decrement
      </button>
    </div>
  );
}

export default App;

React-Redux

最简单的理解:

  1. react-redux 通过在 Provider 组件中提供了store
  2. 在 Provider 组件的后代组件中使用 useSelector中,封装了对子状态的选择,以及自动处理订阅等一系列操作
javascript 复制代码
import { Provider } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import store from "./store";

const SubComponent = () => {
  const dispatch = useDispatch();
  const { todos } = useSelector(
    (state: RootState) => state.todos
  );
  
  return todos.map(item => (
      <div key={item.id}>{item.name}</div>
  ))
  
}

const App = () => {
  return (
    <Provider store={store}>
    </Provider>
  );
};

export default App;

可以看一下 react-redux 源码中 useSelector 的 hook 实现,实现原理也蛮简单的,关键步骤用到了 react18 中的useSyncExternalStore 来实现,点击看源码

useSelector 的伪代码如下

scss 复制代码
function useSelector(seletor) {
    // 从react-redux中的provider中获取context
    const reduxContext = useReduxContext()
    // 从 上下文中获取store和subscription,subscription 是 react-redux 优化过的发布订阅
    const { store, subscription } = reduxContext
    // 对 seletor 函数进行包装,这里先不关注,其实就是选择 redux 内部 state 的 subState
    // 比如 seletor = (state) => state.subState
    const wrappedSelector = wrapper(seletor)
    // 使用 useSyncExternalStoreWithSelector 完成自动订阅
    const selectedState = useSyncExternalStoreWithSelector(
      subscription.addNestedSub,
      store.getState,
      getServerState || store.getState,
      wrappedSelector,
      equalityFn,
    )

    React.useDebugValue(selectedState)
    // 返回改状态
    return selectedState
}

顺便可以看一下 React 内部关于 useSyncExternalStore源码实现,也很简单,下面是 useSyncExternalStore 的执行过程:

简化之后,可以理解为:

  1. 在 useEffect 中执行 subscribeToStore

  2. subscribeToStore 内部其实就是订阅了handleStoreChange

  3. handleStoreChange 首先检查状态有没有更新(checkIfSnapshotChanged),决定是否要执行强制更新forceStoreRerender

    1. checkIfSnapshotChanged 通过 getSnapshot 和 value进行浅比较

最后,看了源码实现,useSelector 的伪代码实现可以简化为

scss 复制代码
function useSelector(seletor) {
    // 从react-redux中的provider中获取context
    const reduxContext = useReduxContext()
    // 从 上下文中获取store和subscription,subscription 是 react-redux 优化过的发布订阅
    const { store, subscription } = reduxContext
    // 对 seletor 函数进行包装,这里先不关注,其实就是选择 redux 内部 state 的 subState
    // 比如 seletor = (state) => state.subState
    const wrappedSelector = wrapper(seletor)
    // 使用 useSyncExternalStoreWithSelector 完成自动订阅
    // const selectedState = useSyncExternalStoreWithSelector(
    //   subscription.addNestedSub,
    //   store.getState,
    //   getServerState || store.getState,
    //   wrappedSelector,
    //   equalityFn,
    // )
    
    const selectedState = wrappedSelector();
    useEffect(() => {
        const unsub = subscription.scribe(() => {
            if(selectedState !== preState) {
                forceRender()
            }
        })
        return unsub;
    }, [subscription.scribe])

    React.useDebugValue(selectedState)
    // 返回改状态
    return selectedState
}

所以简化之后的代码是不是和最开始的代码非常类似

ini 复制代码
  useEffect(() => {
    const rerender = () => {
        setCount(store.getState().count);
    }
    const unsubscribe = store.subscribe(rerender);
    return unsubscribe
  }, [store.subscribe]);
相关推荐
啊波次得饿佛哥22 分钟前
在winform中使用chromiumWebBrowser显示Echarts图表
前端·javascript·echarts·winform·cefsharp
秋天的一阵风1 小时前
突发奇想:border: 0 和boder: none 有区别吗?🤔🤔🤔
前端·css·html
秋天的一阵风1 小时前
🌈尘埃落定!ECMASCRIPT 2025 标准来袭,开发者的新福音🎁
前端·javascript·ecmascript 8
Coffeeee1 小时前
重新开始学Threejs,了解一下里面的一些高级几何体
前端·typescript·three.js
沉迷...1 小时前
el-input限制输入只能是数字 限制input只能输入数字
开发语言·前端·elementui
xx24061 小时前
date-picker组件的shortcuts为什么不能配置在vue的data的return中
前端·javascript·vue.js
古时的风筝1 小时前
Caddy 比Nginx 还优秀吗
前端·后端·程序员
Anlici2 小时前
无脑字节面基🥲
前端·面试·架构
古时的风筝2 小时前
Cursor 建议搭配 CursorRules 食用
前端·后端·cursor
前端南玖2 小时前
通过performance面板验证浏览器资源加载与渲染机制
前端·面试·浏览器