前言
在上一节 Redux 技术分享 中介绍了 Redux
基本使用及底层源码实现。
现在,我们要在 React 项目中,将 Redux
提供的数据接入到 React 组件
中使用,React-Redux
可以完成这件事情。
目前普遍使用 Hook 进行开发,本节将围绕 Hooks 相关 API
来使用和学习 React-Redux
(v7.2.8) 及其原理。
与之相关的两篇文章可以翻阅这里:
一、基本用法
1、安装:
yarn add redux react-redux
2、看一个加减数 Demo:
// 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;
// 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')
);
从 Demo 中我们使用了 React-Redux
提供的三个属性方法:Provider, useDispatch, useSelector
,而这三个 API 足以满足在 Hooks 开发下的使用需求。
Provider
:提供者,类似于React Context
下的Provider
对象,它可以接受一个store
作为 prop,可将 store 中的 value 向下传递;useDispatch
:和Redux dispatch
作用一致,用于派发 action 来更新 store 中的 state;useSelector
:用于在函数组件中获取Redux store
中的state
,在 selecotr 选择的state
更新后,组件会进行重渲染。
相信上面这个简单的 Demo 大家都能够看明白。下面我们开始从源码入手,理解 React-Redux
的运行机制。
二、源码分析
1、核心方法概览:
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,
}
2、Provider
React-Redux
提供的 Provider 是基于 React 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';
// 1、要创建 React Context 对象
const ReactReduxContext = React.createContext(null);
function Provider({ store, context, children }) {
// 2、提供一个 contextValue
const contextValue = useMemo(() => {
// 2-1、创建一个根订阅器,react-redux 的更新机制借助于「发布订阅」实现
const subscription = new Subscription(store);
// 2-2、onStateChange 用于在 state 发生变化后执行的回调
// 对于根订阅器,在 state 发生变化后,执行 notifyNestedSubs 通知所有子订阅器(每一个 useSelector)
subscription.onStateChange = subscription.notifyNestedSubs;
return {
store,
subscription,
}
}, [store]);
// 3、开启订阅器的工作
useEffect(() => {
const { subscription } = contextValue;
// 在这里要开启订阅工作。本质是调用 Redux 提供的 store.subscribe 来订阅一个可监听 state 的 listener
subscription.trySubscribe();
return () => {
subscription.tryUnsubscribe(); // 销毁订阅器的监听 store.unsubscribe
subscription.onStateChange = null;
}
}, []);
const Context = context || ReactReduxContext;
// 4、返回 Context.Provider
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
从上面源码分析 + 注释,可以分为四块内容:
- 创建一个
react context
对象; - 创建一个
contextValue
,其中包含了 store 和 一个订阅器; - 开启订阅器工作,
redux
提供了监听state
变化的subscribe
,这里本质上是通过redux store.subscribe
(redux 的功能) 监听 state 变化,当 state 发生变化时,react-redux 会处理指定组件的重渲染;(最最最核心实现)
- return 返回
context.provider
,将 contextValue 向下传递。
有关
Subscription
实例的相关方法会在下面进行讲解。
3、useReduxContext
通过 React Hooks API useContext
来获取 contextValue
,即上面 Provider 中提供的具有 store
和 subscription
属性的对象。
import React, { useContext } from 'react';
const ReactReduxContext = React.createContext(null);
function useReduxContext() {
const contextValue = useContext(ReactReduxContext);
return contextValue;
}
4、useStore
上面 useReduxContext 中既然可以拿到 contextValue
对象,自然可以从中获取 Redux store
:
function useStore() {
const { store } = useReduxContext();
return store;
}
5、useDispatch
既然 useStore 可以拿到 Redux store
,自然可以从中拿到 store.dispatch
方法:
function useDispatch() {
const store = useStore();
return store.dispatch;
}
6、useSelector
useSelector
可以类比为 useContext
,用来读取 context.value,即 store.state
。
它允许传递一个函数(selector
)作为参数,内部会将 store.state 作为此函数的参数,用户可以自行根据 store.state 决定组件内要使用的数据。
const count = useSelector(state => state.count);
我们来分析下源码。
const refEquality = (a, b) => a === b;
// equalityFn 是一个比较函数,可自定义比较新老状态,决定更新视图的规则。
function useSelector(selector, equalityFn = refEquality) {
// 1、获取 store 对象
const { store, subscription: contextSub } = useReduxContext();
// 2、执行传入的 selector 拿到 selectedState,并且借助订阅机制实现 selectedState 更新组件重渲染
const selectedState = useSelectorWithStoreAndSubscription(
selector,
equalityFn,
store,
contextSub
);
return selectedState;
}
真正的核心实现是在 useSelectorWithStoreAndSubscription
中。
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
// 2、拿到 store.state
const storeState = store.getState();
let selectedState;
// 3、执行 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;
});
// 4、提供一个子订阅器,当「根订阅器」收到更新通知,会执行子订阅器的 checkForUpdates,
// 来校验当前组件是否需要被更新
useLayoutEffect(() => {
function checkForUpdates() {
const newStoreState = store.getState();
// Avoid calling selector multiple times if the store's state has not changed
// 这里很关键,如果 state 引用地址未改变,不进行重渲染。
if (newStoreState === latestStoreState.current) {
return;
}
// 执行 selector fn,拿到返回的 state
const newSelectedState = latestSelector.current(newStoreState);
// 如果 state 前后没有发生变化,取消更新
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return;
}
// selector state 发生变化,重渲染组件
latestSelectedState.current = newSelectedState;
forceRender();
}
// 子订阅器的 onStateChange 指向一个更新机制函数,用于触发组件重渲染
subscription.onStateChange = checkForUpdates;
subscription.trySubscribe(); // 开启订阅
return () => subscription.tryUnsubscribe();
}, [store]);
return selectedState;
}
从上面源码分析 + 注释,可以分为四块内容:
- 使用
useContext
先读取到contextValue.store
对象; - 为
useSelecotr
创建一个subscription
,它的目的并不是像根订阅器那样去订阅 redux store.state,而是关联「根订阅器」,由根订阅器通知「子订阅器」进行更新; - 拿到
store.state
传入并执行selector
返回用户需要的 state 数据; - 提供一个 state 变化比较函数,在发生更新时,若 state 前后存在变化,执行
forceRender
进行组件重渲染。
这样,当根订阅器监听到 Redux store state
发生变化,通知这些子订阅器,执行 equalityFn
判断是否要进行组件更新,进而触发组件更新渲染。
这里要明确一下:redux dispatch 被触发后,会执行 reducer 纯函数计算新的 state,即使 state 引用地址没有变化,也会执行「根订阅器」在 store.subscribe 中订阅的回调。
当「根订阅」通知「子订阅」并进入到 checkForUpdates 方法后,会在这里比较一次 state 前后是否发生变化。若未发生变化,不会进行后续组件重渲染逻辑,这也是为什么我们要在 reducer 中进行更新时,返回一个全新 state 对象。
不过,要彻底了解数据变化组件重渲染更新机制,需要深入理解 Subscription
的实现。
三、Subscription
Subscription 是 React-Redux 内部实现的一套 订阅 store.state 更新
机制。这里我们先梳理一下上面已经露面的 subscription 属性和方法:
- 实例对象:
new Subscription(store)
; - 开启订阅并监听
store.state
:subscription.trySubscribe()
; - 销毁订阅:
subscription.tryUnsubscribe()
; - 「根订阅」通知「子订阅」:
subscription.notifyNestedSubs
; - state 变化后的处理函数:
subscription.onStateChange
;
接下来我们从源码中以上几个维度进行分析:
1、构造函数
构造函数接收 store
和 「根订阅器」作为参数,当 store.state
发生变化,会执行 handleChangeWrapper
进而执行的是 onStateChange
。
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);
}
// state 发生变化,执行组件 listener 回调
handleChangeWrapper() {
if (this.onStateChange) {
this.onStateChange();
}
}
...
}
2、进行订阅
通过调用 subscription.trySubscribe()
开启订阅。
// 开启订阅
trySubscribe() {
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
: this.store.subscribe(this.handleChangeWrapper); // store 监听状态变化
this.listeners = createListenerCollection();
}
}
- 如果是「根订阅器」,它会通过
redux store.subscribe
监听 state,在发生变化后执行onStateChange
; - 如果是「子订阅器」,它会将
onStateChange
关联到「根订阅器」中,这样只需订阅一次store.state
,在 state 发生变化后,统一由「根订阅器」来通知到它们。
其中,订阅器之间进行关联的逻辑 addNestedSub
:
// 供父订阅实例来添加子级实例的监听函数
addNestedSub(listener) {
this.trySubscribe(); // 这里可以不关心它,因为只会开启一次订阅器工作
return this.listeners.subscribe(listener);
}
在上面 useSelector
中我们看到,「子订阅器」的 onStateChange
是一个校验更新函数 checkForUpdates
;
「根订阅器」的 onStateChange
用来通知执行子订阅器,它的实现在 notifyNestedSubs
中。
notifyNestedSubs() {
this.listeners.notify();
}
现在,最神秘的莫过于 this.listeners
,我们来看看 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;
}
},
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
}
}
},
}
}
这里可以分两步分析:
- 「根订阅器」执行
listeners.subscribe(listener)
将子订阅器的onStateChange
组合成一个链表; - 在
store.state
数据发生更新后,「根订阅器」调用listeners.notify()
执行链表触发一个个子订阅器onStateChange
。
这样,在 redux store.state
发生更新后通知到「根订阅器」,根订阅器再通知每个「子订阅器」执行 onStateChange
,进而执行 checkForUpdates
,如果组件 selector
的 state 发生了变化,组件将进行重渲染。
3、销毁订阅
做的是数据重置工作。
tryUnsubscribe() {
if (this.unsubscribe) {
this.unsubscribe();
this.unsubscribe = null;
this.listeners.clear();
this.listeners = nullListeners;
}
}
四、思考:useSelector 是如何只在我们想要的时候触发更新的?
你有没有想过这样一个问题:为什么 react-redux 不直接使用 useContext
API,而要自己实现一个 useSelector
API ?
在 从源码角度理解 React.Context 一文中我们知道,若想通过 useContext
方式读取最新数据,必须让所在组件进行重渲染,这要求 Provider
必须返回一个全新 value 对象引用。
这存在一个问题:一次更新,会将所有使用了 Provider value
数据的组件都进行重渲染,这会导致出现很大性能问题,这也是 React.context
一大弊端。
但 react-redux
并没有这个问题,它只会让组件所 selector
的数据在发生更新时进行重渲染。它是如何绕过 context 更新,并实现一套高性能更新策略呢?
- 首先,提供给
Context.Provider
的 value 对象地址不会发生变化,这使得子组件中使用了useSelector -> useContext
,但不会因顶层数据而进行重渲染。 - 那么
store.state
数据变化组件如何更新呢?其实react-redux
订阅了redux store.state
发生更新的动作(new Subscription
),然后通知依赖此变量的组件「按需」执行重渲染(subscription.onStateChange = checkForUpdates
)。
文末
感谢阅读,本文在编写中如有不足的地方,欢迎读者提出宝贵意见。