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]);
相关推荐
GISer_Jing6 分钟前
前端面试常考题目详解
前端·javascript
Boilermaker19921 小时前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y2 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁2 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry2 小时前
Fetch 笔记
前端·javascript
拾光拾趣录2 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟2 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart