认识
一、认识
1.1 Redux
Redux
基于发布订阅的模式, Redux
创建一个 store
, 保存了状态信息。 React
应用的 state
统一放在store
里面维护,当需要修改state
的时候,dispatch
一个 action
给reducer
,reducer
算出新的state
后,再将state
发布给事先订阅的组件。所有对状态的改变都需要dispatch
一个action
,通过追踪action
,就能得出state
的变化过程。整个数据流都是单向的,可检测的,可预测的。当然,另一个额外的好处是不再需要一层一层的传递props
了。
Redux
使用了 useReducer
自己维护 forceUpdate()
的 function
,但是后在之后使用了 useSyncExternalStoreWithSelector
取代作为触发渲染的 function
。
1.2 React-Redux
由于 Redux
通过在入口文件中进行订阅 store.subscribe(render)
之后, 在需要状态的组件中, 引入 store
文件即可访问、更新状态。那这样的话无法做到精确更新, 比如 A
组件需要状态 a
,B
组件需要状态 b
,那么改变 a
,只希望 A
组件更新,不希望 B
组件更新。所以, React-Redux
应运而生。
React-Redux
是连接 React
应用和 Redux
状态管理的桥梁, React-redux
主要专注两件事,一是如何向 React
应用中注入 redux
中的 Store
,二是如何根据 Store
的改变,把消息派发给应用中需要状态的每一个组件。
-
向
React
注入redux
中的Store
:Redux
提供了一个Provider
组件, 可以全局注入redux
中的store
,所以使用者需要把Provider
注册到根部组件中。Provider
作用就是保存redux
中的store
,分配给所有需要state
的子孙组件。 -
订阅
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-thunk
,redux-actions
等中间件
2.2 发布订阅
Redux
基于发布订阅的模式, Redux
创建一个 store
, 保存了状态信息。 React
应用的 state
统一放在store
里面维护,当需要修改state
的时候,dispatch
一个 action
给reducer
,reducer
算出新的state
后,再将state
发布给事先订阅的组件。所有对状态的改变都需要dispatch
一个action
,通过追踪action
,就能得出state
的变化过程。整个数据流都是单向的,可检测的,可预测的。当然,另一个额外的好处是不再需要一层一层的传递props
了
三、Redux 工作流
四、React-Redux 工作流
4.1 Provider 注入 Store
<Provider>
组件利用 useMemo
,跟据 store
变化创建出一个 contextValue
包含一个根元素订阅器和当前store
。然后在 useEffect
的 create
回调发起订阅, 在 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
中的订阅器是最根部的订阅器,可以通过 trySubscribe
和 addNestedSub
方法可以看到。还有一个注意的点就是,如果父组件是一个 connect
,子孙组件也有 connect
,那么父子 connect
的 Subscription
也会建立起父子关系。
上订下发: 在调用 trySubscribe
的时候,能够看到订阅器会和上一级的订阅器通过 addNestedSub
建立起关联,当 store
中 state
发生改变,会触发 store.subscribe
,但是只会通知给 Provider
中的根 Subscription
,根 Subscription
也不会直接派发更新,而是会下发给子代订阅器( connect
中的 Subscription
),再由子代订阅器,决定是否更新组件,层层下发。
问题: 为什么 React-Redux
会采用 subscription
订阅器进行订阅,而不是直接采用 store.subscribe
呢 ?
-
首先
state
的改变,Provider
是不能直接下发更新的,如果下发更新,那么这个更新是整个应用层级上的,还有一点,如果需要state
的组件,做一些性能优化的策略,那么该更新的组件不会被更新,不该更新的组件反而会更新了。 -
父
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
核心流程如下:
-
connect
中有一个selector
的概念,selector
有什么用?就是通过mapStateToProps
,mapDispatchToProps
,把redux
中state
状态合并到props
中,得到最新的props
。 -
上述讲到过,每一个
connect
都会产生一个新的Subscription
,和父级订阅器建立起关联,这样父级会触发子代的Subscription
来实现逐层的状态派发。 -
有一点很重要,就是
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) /* 继承静态属性 */
}
}