React-Redux 技术分享

前言

在上一节 Redux 技术分享 中介绍了 Redux 基本使用以及底层的源码实现。那在实际项目中,如何将 Redux 提供的数据在React 组件中使用呢?这就需要借助于 React-Redux 来做桥梁。

由于在实际开发中多数以函数 Hooks 来编写组件,本节将主要围绕 Hooks 相关 API 来使用和学习 React-Redux 其原理。

基本用法

安装:

yarn add redux react-redux
复制代码

加减数 Demo:

// index.js
import React from 'react';
import ReactDom from 'react-dom';
import { Provider, useDispatch, useSelector } from 'react-redux';
import store from './store';

function App() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <div>{count}</div>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>点击 + 1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>点击 - 1</button>
    </div>
  )
}

ReactDom.render(
  <Provider store={store}>
    <App />
  </Provider>, 
  document.getElementById('root')
);

// store.js
import { createStore } from 'redux';

const iniState = {
  count: 0
}
function reducer(state = iniState, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
}
let store = createStore(reducer);

export default store;
复制代码

分析 Demo:

从上面 Demo 中我们使用了 React-Redux 提供的三个属性方法:Provider, useDispatch, useSelector,而这三个 API 足以满足在 Hooks 开发下的使用需求。

  • Provider:提供者,可通过创建一个 React Context 对象来得到。Provider 接收一个 value 属性,可传递给消费组件(子组件)使用;
  • useDispatch:和 Redux dispatch 作用一致,用于派发 action 来更新 store 中的 state;
  • useSelector:获取 Redux store 中的 state,来作为函数组件中的 状态 去使用,且 state 更新后,所有消费它的组件都会被更新(核心关键)

相信上面这个简单的 Demo 大家都能够看明白。下面我们开始从源码入手,理解 React-Redux 的运行机制。

源码分析

核心方法概览:

import React, { useMemo, useContext, useEffect, useLayoutEffect } from 'react';

// react-redux 内部实现了一套订阅机制,用于订阅 redux store 中的 state 在发生变化后,
// 更新 state 对应的 React 消费组件
import Subscription from './Subscription';

// React Context 对象
const ReactReduxContext = React.createContext(null);

// 提供者:内部会使用 Context 对象返回的一个 Provider React 组件
function Provider({ store, context, children }) {}

// 获取 React Context 对象
function useReduxContext() {}

// 获取 Provider value 提供的 Redux store 对象
function useStore() {}

// 获取 Redux dispatch 方法
function useDispatch() {}

// 被 useSelector 所使用,用于为 useSelector 返回的 state 创建订阅器,来监听状态变化去更新视图
function useSelectorWithStoreAndSubscription(...args) {}

// 默认的一个比较新老状态函数,决定更新视图的规则
const refEquality = (a, b) => a === b;

// 选择器,让函数组件可以拿到 Redux store 中的 state 数据
function useSelector(selector, equalityFn = refEquality) {}

export {
  Provider,
  useStore,
  useDispatch,
  useSelector,
  ReactReduxContext,
}
复制代码

Provider

React-Redux 提供的 Provider 并不是 React Context 对象下的 Provider 组件,它是自定义封装的一个组件(名字和 Context.Provider 相同),但在它内部会返回 Context.Provider 作为包裹组件 – 提供者

它可以接收一个 store 作为 props(Context.Provider 只能是 value 作为 props),而这个 store 就是我们熟悉的 Redux store 对象。

ReactDom.render(
  <Provider store={store}>
    <App />
  </Provider>, 
  document.getElementById('root')
);
复制代码

我们来看看 Provider 内部实现:

import React, { useMemo, useEffect } from 'react';

// React Context 对象
const ReactReduxContext = React.createContext(null);

function Provider({ store, context, children }) {
  // 1、contextValue 作为 Context.Provider value 被下发
  const contextValue = useMemo(() => {
    // 1-1、创建一个订阅器,因为是在 Provider 下创建,可以理解是一个 根订阅器
    const subscription = new Subscription(store);
    // 1-2、根订阅器的 onStateChange 指向 notifyNestedSubs 方法,用于在数据变化后,通知所有的 订阅者
    subscription.onStateChange = subscription.notifyNestedSubs;
    return {
      store,
      subscription,
    }
  }, [store]);

  // 2、开启订阅器的工作
  useEffect(() => {
    const { subscription } = contextValue;
    // 根订阅器的核心:开启订阅工作。本质是调用 redux store.subscribe 来订阅一个监听 state 的 listener
    // React-Redux 实现了一套自己的 Subscription,但订阅器要想正常工作,就需要在这里通过 subscribe 注册监听
    subscription.trySubscribe();

    return () => {
      subscription.tryUnsubscribe(); // 销毁订阅器的监听 store.unsubscribe
      subscription.onStateChange = null;
    }
  }, []);

  const Context = context || ReactReduxContext;

  return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
复制代码
  • 首先创建一个根订阅器 new Subscription(store),将订阅器和 store 作为 contextValue 交由 Context.Provider 提供者派发下去;
  • 接着执行 subscription.trySubscribe() 开启订阅工作,订阅的对象就是 Redux store 中的 state,订阅方式则是 Redux store.subscribe(listener)。也就是说当 Redux 数据发生变化后,会通知这个 Subscription 订阅器,然后它会做一些事情(比如让需要更新的组件更新)。

关于 Subscription 如果在这里不容易理解,可以先往下走,等到下面会专门分析 Subscription 源码,到时会更容易理解。

useReduxContext

通过 React Hooks API useContext 来获取 contextValue,即上面 Provider 中提供的具有 storesubscription 属性的对象。

import React, { useContext } from 'react';

const ReactReduxContext = React.createContext(null);

function useReduxContext() {
  const contextValue = useContext(ReactReduxContext);
  return contextValue;
}
复制代码

useStore

上面 useReduxContext 中既然可以拿到 contextValue 对象,自然可以从中获取 Redux store

function useStore() {
  const { store } = useReduxContext();
  return store;
}
复制代码

useDispatch

既然 useStore 可以拿到 Redux store,自然可以从中拿到 store.dispatch 方法:

function useDispatch() {
  const store = useStore();
  return store.dispatch;
}
复制代码

useSelector

useSelector 允许传递一个函数作为参数,并且会将 store state 作为此函数的参数,函数可以通过参数来访问 state,并且函数的返回值将作为此组件中要使用的 state。

const count = useSelector(state => state.count);
复制代码

源码实现如下:

const refEquality = (a, b) => a === b;

// equalityFn:比较函数,可自定义比较新老状态,决定更新视图的规则
function useSelector(selector, equalityFn = refEquality) {
  const { store, subscription: contextSub } = useReduxContext();
  // 将选择器用于存储和订阅
  const selectedState = useSelectorWithStoreAndSubscription(
    selector,
    equalityFn,
    store,
    contextSub
  );
  return selectedState;
}

function useSelectorWithStoreAndSubscription(
  selector,
  equalityFn,
  store,
  contextSub, // 上下文订阅器(应用的根订阅器)
) {
  const [, forceRender] = useReducer((s) => s + 1, 0);

  // 1、为 selector 选择器创建一个订阅器,并将 contextSub(根订阅器) 作为 parentSub
  const subscription = useMemo(() => new Subscription(store, contextSub), [
    store,
    contextSub,
  ]);

  // 用于存储,避免变量在每次更新都重新创建,所以用到 useRef 来持久化变量
  const latestSelector = useRef(); // 最新的选择器方法
  const latestStoreState = useRef(); // 最新的仓库状态
  const latestSelectedState = useRef(); // 最新的选择器返回的state

  const storeState = store.getState();
  let selectedState;

  // 2、每次组件更新,执行 selector 选择器来读取 state
  if (
    selector !== latestSelector.current ||
    storeState !== latestStoreState.current
  ) {
    selectedState = selector(storeState);
  } else {
    selectedState = latestSelectedState.current
  }

  // 每次执行都存储最新的数据
  useLayoutEffect(() => {
    latestSelector.current = selector;
    latestStoreState.current = storeState;
    latestSelectedState.current = selectedState;
  });

  // 3、将组件更新方法绑定在子订阅器上(onStateChange),以便根订阅器监听 state 更新后,执行其组件更新方法
  useLayoutEffect(() => {
    function checkForUpdates() {
      const newSelectedState = latestSelector.current(store.getState());
      if (equalityFn(newSelectedState, latestSelectedState.current)) {
        return;
      }
      latestSelectedState.current = newSelectedState;
      forceRender();
    }

    // 子订阅器的 onStateChange 指向一个更新机制函数,用于触发组件重渲染
    subscription.onStateChange = checkForUpdates;
    subscription.trySubscribe(); // 开启订阅

    return () => subscription.tryUnsubscribe();
  }, [store]);

  return selectedState;
}
复制代码
  • 首先读取 store 和 subscription,进入 useSelectorWithStoreAndSubscription 方法处理;
  • 在此方法内部会为 useSelector 创建一个 子订阅器,并将 contextSub(根订阅器) 作为 parentSub(后面看 Subscription 源码时这一点很重要);
  • 调用 selector 并将 store state 作为参数,执行结果就是用户想要使用的 state
  • 定义一个组件更新方法,并绑定到子订阅器上。

这样,当根订阅器监听到 Redux store state 发生变化后,可以通知这些子订阅器,进而触发组件更新渲染。

Subscription

Subscription 是 React-Redux 内部实现的一套 订阅更新 机制。涉及到 Redux store.subscripe 监听 state 变化,还会有一些 链表 数据结构相关操作。

源码如下:

const nullListeners = { notify() {} };

export default class Subscription {
  constructor(store, parentSub) {
    this.store = store;
    this.parentSub = parentSub;
    this.unsubscribe = null;
    this.listeners = nullListeners;

    this.handleChangeWrapper = this.handleChangeWrapper.bind(this);
  }

  // 供父订阅实例来添加子级实例的监听函数
  addNestedSub(listener) {
    this.trySubscribe();
    return this.listeners.subscribe(listener);
  }

  // 向listeners发布通知
  notifyNestedSubs() {
    this.listeners.notify();
  }

  // 要添加的 listener,用于触发组件的更新
  handleChangeWrapper() {
    if (this.onStateChange) {
      this.onStateChange();
    }
  }

  // 检测是否开启了订阅
  isSubscribed() {
    return Boolean(this.unsubscribe);
  }

  // 开启订阅
  trySubscribe() {
    if (!this.unsubscribe) {
      // store 监听状态的销毁函数
      this.unsubscribe = this.parentSub
        ? this.parentSub.addNestedSub(this.handleChangeWrapper)
        : this.store.subscribe(this.handleChangeWrapper); // store 监听状态变化

      this.listeners = createListenerCollection();
    }
  }

  // 取消订阅
  tryUnsubscribe() {
    if (this.unsubscribe) {
      this.unsubscribe();
      this.unsubscribe = null;
      this.listeners.clear();
      this.listeners = nullListeners;
    }
  }
}
复制代码

首先我们关注 trySubscribe 方法,不管是根订阅器还是子订阅器,它们在创建订阅器后,都会先执行此方法:

this.unsubscribe = this.parentSub
    ? this.parentSub.addNestedSub(this.handleChangeWrapper)
    : this.store.subscribe(this.handleChangeWrapper); // store 监听状态变化

this.listeners = createListenerCollection();
复制代码
  • 对于根订阅器:它会调用 store.subscribe 来注册 state 变化的监听回调 handleChangeWrapper
  • 对于子订阅器:它的 parentSub 就是根订阅器,这个我们在上面有讲过,此时会调用根订阅器的 addNestedSub 方法,并将子订阅器的 handleChangeWrapper 作为参数。

handleChangeWrapper 方法到底是做了什么呢?

handleChangeWrapper() {
  if (this.onStateChange) {
    this.onStateChange();
  }
}
复制代码

onStateChange 方法是不是在上面有印象?

  • 对于子订阅器,onStateChange 对应组件更新函数 checkForUpdates
  • 对于父订阅器,onStateChange 对于订阅器实例上的 notifyNestedSubs

我们来看一下 notifyNestedSubs 和刚刚未讲到的 addNestedSub 内部实现:

addNestedSub(listener) {
    this.trySubscribe(); // 这里可以不关心它,因为只会开启一次订阅器工作
    return this.listeners.subscribe(listener); // 订阅 listener
}

notifyNestedSubs() {
    this.listeners.notify(); // 通知 listener
}
复制代码

可以看到都涉及到了 this.listeners,在 trySubscribe 方法中我们还有一个非常重要的方法没介绍:createListenerCollection,在其内部通过 链表 来阻止每个 listener 监听函数,其核心就是按链表顺序依次执行每一个 listener。

function createListenerCollection() {
  // 链表指针,管理监听函数的执行
  let first = null;
  let last = null;

  return {
    clear() {
      first = null
      last = null
    },
    
    notify() {
      let listener = first;
      while (listener) {
        listener.callback();
        listener = listener.next;
      }
    },

    get() {
      let listeners = []
      let listener = first
      while (listener) {
        listeners.push(listener)
        listener = listener.next
      }
      return listeners
    },

    subscribe(callback) {
      let isSubscribed = true;

      let listener = (last = {
        callback,
        next: null,
        prev: last,
      })

      // 加入链表队列
      if (listener.prev) {
        listener.prev.next = listener;
      } else {
        first = listener;
      }

      return function unsubscribe() {
        if (!isSubscribed || first === null) return;
        isSubscribed = false;

        // 将 listener 从链表中移除
        if (listener.next) {
          listener.next.prev = listener.prevl;
        } else {
          last = listener.prev;
        }
        // 重新建立链表关系
        if (listener.prev) {
          listener.prev.next = listener.next
        } else {
          first = listener.next
        }
      }
    },
  }
}
复制代码

梳理一下:

  1. 在 Provider 中,创建根订阅器,调用 trySubscribe 开启根订阅,并通过 store.subscripe 来监听 state 变化,若 state 变化,会执行 onStateChange(处理函数);
  2. 在用到 useSelector 时,创建子订阅器,并调用 trySubscribe 开启子订阅,而子订阅的 onStateChange(处理函数) 是一个用于更新组件的方法:checkForUpdates;接着将子订阅器提供的更新组件方法,作为一个监听函数 listener 添加到根订阅器上:

addNestedSub(listener) {
    this.trySubscribe();
    return this.listeners.subscribe(listener);
}
复制代码
  1. Redux store state 更新后,会触发根订阅器在 store.subscribe 上注册的 handleChangeWrapper 方法,最终执行的是:
notifyNestedSubs() {
    this.listeners.notify();
}
复制代码
  1. 最后在 this.listeners.notify 中执行每一个子订阅器上的 checkForUpdates 方法来更新视图:
notify() {
  let listener = first;
  while (listener) {
    listener.callback(); // checkForUpdates()
    listener = listener.next;
  }
},
复制代码
  1. 经过上面的处理,当 Rudex store state 发生变化后,就会执行 useSelector 中子订阅器提供的 checkForUpdates 方法,如果前后 state 发生了变化,就会更新组件。

文末

本文在编写中如有不足的地方,?欢迎读者提出宝贵意见,作者进行改正。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享