前言
在上一节 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 中提供的具有 store
和 subscription
属性的对象。
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
}
}
},
}
}
复制代码
梳理一下:
- 在 Provider 中,创建根订阅器,调用
trySubscribe
开启根订阅,并通过store.subscripe
来监听 state 变化,若 state 变化,会执行onStateChange
(处理函数); - 在用到 useSelector 时,创建子订阅器,并调用
trySubscribe
开启子订阅,而子订阅的onStateChange
(处理函数) 是一个用于更新组件的方法:checkForUpdates
;接着将子订阅器提供的更新组件方法,作为一个监听函数listener
添加到根订阅器上:
addNestedSub(listener) {
this.trySubscribe();
return this.listeners.subscribe(listener);
}
复制代码
- 当
Redux store state
更新后,会触发根订阅器在store.subscribe
上注册的handleChangeWrapper
方法,最终执行的是:
notifyNestedSubs() {
this.listeners.notify();
}
复制代码
- 最后在
this.listeners.notify
中执行每一个子订阅器上的checkForUpdates
方法来更新视图:
notify() {
let listener = first;
while (listener) {
listener.callback(); // checkForUpdates()
listener = listener.next;
}
},
复制代码
- 经过上面的处理,当
Rudex store state
发生变化后,就会执行useSelector
中子订阅器提供的checkForUpdates
方法,如果前后 state 发生了变化,就会更新组件。
文末
本文在编写中如有不足的地方,?欢迎读者提出宝贵意见,作者进行改正。