三伏天,学点react-redux 源码降降火

开门见山 🔥

酷暑难耐,学点源码降降火。有时候写业务代码久了,很多基础的东西觉得理所当然,需要静下心来梳理一下项目的底座。项目中使用的主要是类组件的用法,所以全文都是类组件相关的 api。

看源码是学习一些设计思路和一些问题的处理方案。如果 react-redux 是我们自己设计,我们需要处理哪些问题?

  • 如何把状态和单个组件绑定?
  • 如何做状态对比,不做额外刷新?
  • 如何对状态变更做监听?并且自动刷新当前组件
  • 如何处理一些错误场景和边界条件?

所以本文没有去纠结版本新旧,到底是用 hooks 还是 classComponent,api 不重要,重要的是底层的设计思路和解决问题的方法。

react-redux 的基础使用 🧊

什么是 redux?这个在很久之前的一篇文章(『React』从零开始整个 redux 中间件)中已经介绍过了,这里不再赘述。

什么是 React-Redux? react-redux 是官方推荐的 Redux 绑定库,用于将 Redux 状态管理与 React 应用集成。它的核心作用是:

  • 将 Redux 的状态注入 React 组件中
  • 触发 Redux 的 action 改变状态
  • 自动响应 Redux 状态变化并重新渲染组件

通俗理解,就是把 redux 中的 store 进行组件级的注入,并且在状态变更的时候做响应。

在类组件中,React-Redux 主要依靠两个 API:

  • connect(mapStateToProps, mapDispatchToProps)
  • <Provider store={store}> 提供上下文

举个栗子🌰

javascript 复制代码
// store.js
import { createStore } from 'redux';

const initialState = { count: 0 };
function reducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    default:
      return state;
  }
}
export const store = createStore(reducer);
javascript 复制代码
// Counter.js
import React, { Component } from 'react';
import { connect } from 'react-redux';

class Counter extends Component {
  render() {
    const { count, increment } = this.props;
    return (
      <div>
        <p>Count: {count}</p>
        <button onClick={increment}>+1</button>
      </div>
    );
  }
}

// 把 Redux 的状态映射到组件 props
const mapStateToProps = state => ({
  count: state.count,
});

// 把 dispatch 映射到组件 props
const mapDispatchToProps = dispatch => ({
  increment: () => dispatch({ type: 'INCREMENT' }),
});

// 用 connect 包装类组件
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
javascript 复制代码
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import { store } from './store';
import Counter from './Counter';

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

怎么做到的?❓

看完基础使用可能好奇,为什么可以直接在 props 中拿到store 中的状态?mapStateToProps和mapDispatchToProps是通过什么方式把状态和状态变更的 action 注入到组件中的?

Provider ❄️

直接贴代码:

js 复制代码
export default class Provider extends Component {
  getChildContext() {
    return { store: this.store }
  }

  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }

  render() {
    return Children.only(this.props.children)
  }
  
  Provider.childContextTypes = {
  store: storeShape.isRequired
}
}

Provider 中使用 getChildContext() 把 store 放到 React context 中,并且声明了childContextTypes。

随后在connect包裹的组件中,使用contextTypes 声明了需要获取的数据。这样子组件就可以通过props中直接拿到store了。

js 复制代码
...
Connect.contextTypes = {
  store: storeShape
}
Connect.propTypes = {
  store: storeShape
}
...

connect整体结构 ❄️

我们可以看看 connect 的源码,看看它主要做了什么事情。 connect 是一个高阶函数,接受四个参数:

  • mapStateToProps
  • mapDispatchToProps
  • mergeProps
  • options

返回一个包裹组件的函数,大概的结构如下:

js 复制代码
return function wrapWithConnect(WrappedComponent) {
  ...
  class Connect extends Component {
    ...
  }
  return hoistStatics(Connect, WrappedComponent)
}
  • connect 返回 wrapWithConnect,它接受你要包裹的组件 WrappedComponent
  • wrapWithConnect 内部定义了一个新的类组件 Connect,它负责逻辑处理(如订阅 Redux store、合并 props 等)。
  • Connectrender 方法会渲染传入的 WrappedComponent,并传递处理好的 props。
  • 其中hoistStatics的作用是让高阶组件"保留"原组件上的自定义静态属性和方法,保证功能不丢失,比如在自己的 component 上定义了一个 static 方法,需要用这个函数包裹,保证从外层能拿到。
javascript 复制代码
class MyComponent extends React.Component {
  static someStaticMethod() {
    // ...
  }
}

const Connected = connect(...)(MyComponent)

Connected.someStaticMethod // ✅ 依然存在!

connect 默认值处理❄️

顶部定义了一些默认值:

javascript 复制代码
const defaultMapStateToProps = state => ({})

const defaultMapDispatchToProps = dispatch => ({ dispatch })

const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
  ...parentProps,
  ...stateProps,
  ...dispatchProps
})
  • defaultMapStateToProps:如果没有传 mapStateToProps,则默认不从 state 取任何数据。
  • defaultMapDispatchToProps:如果没有传 mapDispatchToProps,则默认只传递 dispatch 方法。
  • defaultMergeProps:合并 stateProps、dispatchProps 和父组件传来的 props。

connect 参数处理❄️

connect 方法本身内部的工作流程:

javascript 复制代码
    const shouldSubscribe = Boolean(mapStateToProps)
    const mapState = mapStateToProps || defaultMapStateToProps

    let mapDispatch
    if (typeof mapDispatchToProps === 'function') {
    mapDispatch = mapDispatchToProps
    } else if (!mapDispatchToProps) {
    mapDispatch = defaultMapDispatchToProps
    } else {
    mapDispatch = wrapActionCreators(mapDispatchToProps)
    }

    const finalMergeProps = mergeProps || defaultMergeProps
    const { pure = true, withRef = false } = options
    const checkMergedEquals = pure && finalMergeProps !== defaultMergeProps

    // Helps track hot reloading.
    const version = nextVersion++
  • 参数处理 :如果没传,使用默认实现;mapDispatchToProps 还可以是对象形式,会自动 wrap 成 dispatch 调用
  • options 解析:如 pure、withRef。
  • 生成一个唯一 version:用于热更新区分。

wrapWithConnect❄️

connect 返回的函数,包裹你传进来的组件(WrappedComponent),生成一个新组件Connect,最后使用 hoistStatics处理一下静态函数。

Connect 组件内部机制❄️

  1. 构造函数

    • 获取 Redux store(从 props 或 context)。
    • 校验 store 存在。
    • 初始化 state 和内部缓存。
  2. 属性和方法说明

    • computeStateProps/computeDispatchProps:计算 stateProps 或 dispatchProps,决定是否依赖组件自身的 props。
    • updateStatePropsIfNeeded/updateDispatchPropsIfNeeded:使用 shallowEqual 对比两次的状态,只有在对应的 props 发生变化时才会重新计算,避免无效渲染。
    • updateMergedPropsIfNeeded:合并 props,决定是否更新。
    • clearCache:清空内部缓存(热更新或卸载时会用到)。

    trySubscribe/tryUnsubscribe:订阅/取消订阅 Redux store 的变化。

    javascript 复制代码
    // 使用 redux 的 store 中的 subscribe 方法订阅 store 的变化
    if (shouldSubscribe && !this.unsubscribe) {
      this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
      this.handleChange()
    }
    • handleChange:Redux store 更新后响应回调,决定是否重新渲染。
  3. 生命周期方法

    • componentDidMount:初始化订阅。
    • componentWillReceiveProps:props 变化时标记需要重新计算。
    • componentWillUnmount:组件卸载时取消订阅。
    • 开发模式下加了 componentWillUpdate,支持热更新。
  4. shouldComponentUpdate:决定是否需要渲染,优化性能。

  5. render :最终渲染 WrappedComponent,传入合并后的 props。如果在 options 配置中添加了withRef配置,会加上 ref。

talk is cheap show me the code❄️

使用 create-react-app 创建一个简单的 demo,结构如下:

js 复制代码
// 入口组件
import React, { Component } from 'react';
import './App.css';
import InputBox from './InputBox';
import Counter from './Counter';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <InputBox />
          <Counter />
        </header>
      </div>
    );
  }
}

export default App;
js 复制代码
// inputBox
import React, { Component } from 'react';
import { connect } from 'react-redux';

class InputBox extends Component {
    handleChange = (e) => {
        this.props.setInput(e.target.value);
    };

    shouldComponentUpdate(nextProps) {
        // 只在 inputValue 变化时才更新
        return nextProps.inputValue !== this.props.inputValue;
    }

    render() {
        return (
            <input
                type="text"
                value={this.props.inputValue}
                onChange={this.handleChange}
                placeholder="请输入内容"
            />
        );
    }
}

const mapStateToProps = (state) => ({
    inputValue: state.inputValue,
});

const mapDispatchToProps = (dispatch) => ({
    setInput: (value) => dispatch({ type: 'SET_INPUT', value }),
});

export default connect(mapStateToProps, mapDispatchToProps)(InputBox); 
js 复制代码
// Counter
import React, { Component } from 'react';
import { connect } from 'react-redux';

class Counter extends Component {
  shouldComponentUpdate(nextProps) {
    // 只在 count 变化时才更新
    return nextProps.count !== this.props.count;
  }

  render() {
    const { count, increment, decrement } = this.props;
    return (
      <div>
        <h2>计数器:{count}</h2>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  count: state.count,
});

const mapDispatchToProps = (dispatch) => ({
  increment: () => dispatch({ type: 'INCREMENT' }),
  decrement: () => dispatch({ type: 'DECREMENT' }),
});

export default connect(mapStateToProps, mapDispatchToProps)(Counter); 

挂载阶段

组件挂载阶段,Connect 内会 做一次 store 的状态同步。

值更新阶段

当我们在页面上对 input 进行 change 操作的时候,从空值变成 1,此时 debug InputBox 组件的逻辑如下:

  • dispatch 一个新的状态到reducer 中处理

store 更新后,由于在挂载阶段做了 store 的监听,此时会触发监听的回调。因为 inputBox 和 Counter 组件都使用 subscribe 做了监听,此时 inputBox 的 change 两边都会监听到。

关键的点在于内部的 shouldComponentUpdate 周期。

可以看到,当我们把数据框的值从空字符串变成'1'的时候,两个 子组件都会进入 shouldComponentUpdate 进行判断。在 inputBox 中,由于 stateProps(mapStateToProps 传入的对象) 的变化,shouldComponentUpdate 就会返回 true,而 Counter 中,虽然 store 变化了(inputValue 从'' => '1'),但是 stateProps 是没有变化的,所以 Counter 组件并不会重新渲染。

我们可以看到,判断组件是否重刷的三个状态,分别对应的是:

  • 组件自己的 props 有没有刷新
  • mapStateToProps 中的对象有没有刷新
  • mapDispatchToProps中的对象有没有刷新

至此,我们就理清楚了组件挂载和更新过程中 react-redux 的内部逻辑,也搞清楚了react-redux 是通过什么方式做到了状态隔离盒局部刷新的。没有黑魔法,只有淳朴的高阶组件和状态的钱比较。

卸载阶段

卸载阶段,组件内把挂载阶段绑定的 store 订阅取消,清除了 store 内的订阅。

结尾 🧊

看完本文内容可以学到:

  • connect 本质上是一个 HOC 的语法糖。
  • 优秀的库,会判断清楚所有的边界条件 connect 给了很多变量去判断一些边界条件。比如 shouldSubscribe,shouldUpdateStateProps等,都是为了兜底兼容一些错误用法。
  • redux 库里面为什么要定义 subscribe 方法,为了是给一些其他的库做一些订阅回调逻辑。

当一个三方库是一个黑盒的时候,总会觉得它里面是不是有什么魔法,用了什么非常高级的编码技巧。实际耐心看下来,是一些简单的东西进行堆砌,一大串小问题的解决方案的总和。

小声 BB❄️

最近在阅读《每周工作四小时》这本书,有点刷新自己对工作这件事情的认知。

多看,多学,多交流。

愿大家在这个节奏快速,信息爆炸的时代,可以多静下心来,慢慢走。

相关推荐
洋流1 小时前
0基础进大厂,React框架基础篇:创建你的第一个React框架项目——梦开始的地方
react.js
多啦C梦a2 小时前
React 表单界的宫斗大戏:受控组件 VS 非受控组件,谁才是正宫娘娘?
前端·javascript·react.js
红衣信4 小时前
useContext 与 useReducer 的组合使用
前端·react.js·面试
土豆12504 小时前
RTK Query 完全指南:简化数据获取与状态管理
redux
归于尽4 小时前
key、JSX、Babel编译、受控组件与非受控组件、虚拟DOM考点解析
前端·react.js·面试
讨厌吃蛋黄酥5 小时前
`useState`是同步还是异步?深入解析闭包陷阱与解决方案
前端·react.js
東南5 小时前
知其然,知其所以然,前端系列之React
前端·react.js
TE-茶叶蛋6 小时前
React 服务器组件 (RSC)
服务器·前端·react.js
_一两风6 小时前
如何从零开始创建一个 React 项目
前端·react.js
然我9 小时前
JSX:看似 HTML 的 “卧底”?
前端·react.js·面试