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]);
相关推荐
天蓝色的鱼鱼32 分钟前
前端开发者的组件设计之痛:为什么我的组件总是难以维护?
前端·react.js
codingandsleeping33 分钟前
使用orval自动拉取swagger文档并生成ts接口
前端·javascript
石金龙2 小时前
[译] Composition in CSS
前端·css
白水清风2 小时前
微前端学习记录(qiankun、wujie、micro-app)
前端·javascript·前端工程化
Ticnix2 小时前
函数封装实现Echarts多表渲染/叠加渲染
前端·echarts
用户22152044278002 小时前
new、原型和原型链浅析
前端·javascript
阿星做前端2 小时前
coze源码解读: space develop 页面
前端·javascript
叫我小窝吧2 小时前
Promise 的使用
前端·javascript
NBtab2 小时前
Vite + Vue3项目版本更新检查与页面自动刷新方案
前端
天天扭码2 小时前
来全面地review一下Flex布局(面试可用)
前端·css·面试