跳到主要内容

认识

一、认识


1.1 Redux

Redux 基于发布订阅的模式, Redux 创建一个 store, 保存了状态信息。 React应用的 state 统一放在store里面维护,当需要修改state的时候,dispatch一个 actionreducerreducer算出新的state后,再将state发布给事先订阅的组件。所有对状态的改变都需要dispatch一个action,通过追踪action,就能得出state的变化过程。整个数据流都是单向的,可检测的,可预测的。当然,另一个额外的好处是不再需要一层一层的传递props了。

Redux 使用了 useReducer 自己维护 forceUpdate()function,但是后在之后使用了 useSyncExternalStoreWithSelector 取代作为触发渲染的 function

1.2 React-Redux

由于 Redux 通过在入口文件中进行订阅 store.subscribe(render) 之后, 在需要状态的组件中, 引入 store 文件即可访问、更新状态。那这样的话无法做到精确更新, 比如 A 组件需要状态 aB 组件需要状态 b ,那么改变 a,只希望 A 组件更新,不希望 B 组件更新。所以, React-Redux 应运而生。

React-Redux 是连接 React 应用和 Redux 状态管理的桥梁, React-redux 主要专注两件事,一是如何向 React 应用中注入 redux 中的 Store ,二是如何根据 Store 的改变,把消息派发给应用中需要状态的每一个组件。

  1. React 注入 redux 中的 Store: Redux 提供了一个 Provider 组件, 可以全局注入 redux 中的 store ,所以使用者需要把 Provider 注册到根部组件中。Provider 作用就是保存 redux 中的 store ,分配给所有需要 state 的子孙组件。

  2. 订阅 Store 中的 state 变化, 派发给应用中需要状态的每一个组件: Redux 提供了一个高阶组件 connect, 被 connect 包装后组件将获得如下功能:

    • 能够从 props 中获取改变 state 的方法 Store.dispatch

    • 如果 connect 有第一个参数,那么会将 redux state 中的数据,映射到当前组件的 props 中,子组件可以使用消费

    • 当需要的 state ,有变化的时候,会通知当前组件更新,重新渲染视图

React-Redux 通过 Provider 组件解决了 Redux 状态共享的问题, 通过 connect 高阶组件订阅状态, 并通过传递第一个参数 mapStateToProps 来订阅具体依赖状态, 只有订阅具体的依赖状态发生变化, 才会触发业务组件更新视图。当这个参数没有的时候,当前组件不会订阅 store 的改变。

二、思想


2.1 中间件

redux 应用了前端领域为数不多的中间件 compose, compose 实现如下:

const compose = (...funcs) => {
return funcs.reduce((f, g) => (x) => f(g(x)));
}

Redux 中间件可以用来强化 dispatch 函数。传统的 dispatch 是不支持异步的,但是可以针对 Redux 做强化,于是有了 redux-thunkredux-actions 等中间件

2.2 发布订阅

Redux 基于发布订阅的模式, Redux 创建一个 store, 保存了状态信息。 React应用的 state 统一放在store里面维护,当需要修改state的时候,dispatch一个 actionreducerreducer算出新的state后,再将state发布给事先订阅的组件。所有对状态的改变都需要dispatch一个action,通过追踪action,就能得出state的变化过程。整个数据流都是单向的,可检测的,可预测的。当然,另一个额外的好处是不再需要一层一层的传递props

三、Redux 工作流


四、React-Redux 工作流


4.1 Provider 注入 Store

<Provider> 组件利用 useMemo,跟据 store 变化创建出一个 contextValue 包含一个根元素订阅器和当前store。然后在 useEffectcreate 回调发起订阅, 在 destroy 回调中卸载订阅。返回通过 Context.Provider 包裹的 children。通过 context 上下文来保存传递 contextValue

const ReactReduxContext =  React.createContext(null)
function Provider({ store, context, children }) {
/* 利用useMemo,跟据store变化创建出一个contextValue 包含一个根元素订阅器和当前store */
const contextValue = useMemo(() => {
/* 创建了一个根级 Subscription 订阅器 */
const subscription = new Subscription(store)
return {
store,
subscription
} /* store 改变创建新的contextValue */
}, [store])
useEffect(() => {
const { subscription } = contextValue
/* 触发trySubscribe方法执行,创建listens */
subscription.trySubscribe() // 发起订阅
return () => {
subscription.tryUnsubscribe() // 卸载订阅
}
}, [contextValue]) /* contextValue state 改变出发新的 effect */
const Context = ReactReduxContext
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}

4.2 Subscription 订阅器

订阅器的核心就是层层订阅,上订下发

层层订阅: React-Redux 采用了层层订阅的思想,上述内容讲到 Provider 里面有一个 Subscription ,提前透露一下,每一个用 connect 包装的组件,内部也有一个 Subscription ,而且这些订阅器一层层建立起关联,Provider 中的订阅器是最根部的订阅器,可以通过 trySubscribeaddNestedSub 方法可以看到。还有一个注意的点就是,如果父组件是一个 connect ,子孙组件也有 connect ,那么父子 connectSubscription 也会建立起父子关系。

上订下发: 在调用 trySubscribe 的时候,能够看到订阅器会和上一级的订阅器通过 addNestedSub 建立起关联,当 storestate 发生改变,会触发 store.subscribe ,但是只会通知给 Provider 中的根 Subscription,根 Subscription 也不会直接派发更新,而是会下发给子代订阅器( connect 中的 Subscription ),再由子代订阅器,决定是否更新组件,层层下发。

问题: 为什么 React-Redux 会采用 subscription 订阅器进行订阅,而不是直接采用 store.subscribe 呢 ?

  1. 首先 state 的改变,Provider 是不能直接下发更新的,如果下发更新,那么这个更新是整个应用层级上的,还有一点,如果需要 state 的组件,做一些性能优化的策略,那么该更新的组件不会被更新,不该更新的组件反而会更新了。

  2. Subscription -> 子 Subscription 这种模式,可以逐层管理 connect 的状态派发,不会因为 state 的改变而导致更新的混乱。

/* 发布订阅者模式 */
export default class Subscription {
constructor(store, parentSub) {
//....
}
/* 负责检测是否该组件订阅,然后添加订阅者也就是listener */
addNestedSub(listener) {
this.trySubscribe()
return this.listeners.subscribe(listener)
}
/* 向listeners发布通知 */
notifyNestedSubs() {
this.listeners.notify()
}
/* 开启订阅模式 首先判断当前订阅器有没有父级订阅器 , 如果有父级订阅器(就是父级Subscription),把自己的handleChangeWrapper放入到监听者链表中 */
trySubscribe() {
/*
parentSub 即是provide value 里面的 Subscription 这里可以理解为 父级元素的 Subscription
*/
if (!this.unsubscribe) {
this.unsubscribe = this.parentSub
? this.parentSub.addNestedSub(this.handleChangeWrapper)
/* provider的Subscription是不存在parentSub,所以此时trySubscribe 就会调用 store.subscribe */
: this.store.subscribe(this.handleChangeWrapper)
this.listeners = createListenerCollection()
}
}
/* 取消订阅 */
tryUnsubscribe() {
//....
}
}

4.3 connect 控制更新

connect 核心流程如下:

  1. connect 中有一个 selector 的概念,selector 有什么用?就是通过 mapStateToProps ,mapDispatchToProps ,把 reduxstate 状态合并到 props 中,得到最新的 props

  2. 上述讲到过,每一个 connect 都会产生一个新的 Subscription ,和父级订阅器建立起关联,这样父级会触发子代的 Subscription 来实现逐层的状态派发。

  3. 有一点很重要,就是 Subscription 通知的是 checkForUpdates 函数,checkForUpdates 会形成新的 props ,与之前缓存的 props 进行浅比较,如果不想等,那么说明 state 已经变化了,直接触发一个 useReducer 来更新组件,上述代码片段中,我用 useState 代替 useReducer 了,如果相等,那么当前组件不需要更新,直接通知子代 Subscription ,检查子代 Subscription 是否更新,完成整个流程。

function connect(mapStateToProps,mapDispatchToProps){
const Context = ReactReduxContext
/* WrappedComponent 为connect 包裹的组件本身 */
return function wrapWithConnect(WrappedComponent){
function createChildSelector(store) {
/* 选择器 合并函数 mergeprops */
return selectorFactory(store.dispatch, { mapStateToProps,mapDispatchToProps })
}
/* 负责更新组件的容器 */
function ConnectFunction(props){
/* 获取 context内容 里面含有 redux中store 和父级subscription */
const contextValue = useContext(ContextToUse)
/* 创建子选择器,用于提取state中的状态和dispatch映射,合并到props中 */
const childPropsSelector = createChildSelector(contextValue.store)
const [subscription, notifyNestedSubs] = useMemo(() => {
/* 创建一个子代Subscription,并和父级subscription建立起关系 */
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription // 父级subscription,通过这个和父级订阅器建立起关联。
)
return [subscription, subscription.notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])

/* 合成的真正的props */
const actualChildProps = childPropsSelector(store.getState(), wrapperProps)
const lastChildProps = useRef()
/* 更新函数 */
const [ forceUpdate, ] = useState(0)
useEffect(()=>{
const checkForUpdates =()=>{
newChildProps = childPropsSelector()
if (newChildProps === lastChildProps.current) {
/* 订阅的state没有发生变化,那么该组件不需要更新,通知子代订阅器 */
notifyNestedSubs()
}else{
/* 这个才是真正的触发组件更新的函数 */
forceUpdate(state=>state+1)
lastChildProps.current = newChildProps /* 保存上一次的props */
}
}
subscription.onStateChange = checkForUpdates
//开启订阅者 ,当前是被connect 包转的情况 会把 当前的 checkForceUpdate 放在存入 父元素的addNestedSub中 ,一点点向上级传递 最后传到 provide
subscription.trySubscribe()
/* 先检查一遍,反正初始化state就变了 */
checkForUpdates()
},[store, subscription, childPropsSelector])

/* 利用 Provider 特性逐层传递新的 subscription */
return <ContextToUse.Provider value={{ ...contextValue, subscription}}>
<WrappedComponent {...actualChildProps} />
</ContextToUse.Provider>
}
/* memo 优化处理 */
const Connect = React.memo(ConnectFunction)
return hoistStatics(Connect, WrappedComponent) /* 继承静态属性 */
}
}

五、Redux Vs Mobx