react-redux源码不完全指北
摘要: 本文围绕一个简单的例子展开,主要聚焦于react-redux架构下数据的流动,包括数据的派发和更新。首先介绍传统模式,然后介绍一下hooks模式的 本文基于react-redux7.2.4
传统模式
一个典型的react-redux应用一般具有如下结构:
import React from 'react'import ReactDOM from 'react-dom'import { connect,Provider } from "react-redux"import { createStore } from "redux"const initialState = { value: 0 }const ADD_ACTION = 'add';const reducer = (state = initialState ,action ) => {switch(action.type){case ADD_ACTION:return {value: ++state.value}default:break;}return state}const store = createStore(reducer);const mapStateToProps = state => {return{value: state.value}}const mapDispatchToProps = dispatch => {return {update(payload){dispatch({type: ADD_ACTION,payload})}}}const App = connect(mapStateToProps,mapDispatchToProps)( function InnerCommponent({value,update}){return(<div>value的值是: {value}<div><button onClick={ update }>点击更新</button></div></div>)})const rootElement = document.getElementById('root')ReactDOM.render(<Provider store={store}><App /></Provider>,rootElement)
基于此代码 我们从上到下 依次梳理梳理
篇1 store究竟是个什么东西
store由createStore(reducer)创建而来,最后的store具有如下的数据结构:
{dispatch,subscribe,getState,replaceReducer,[$$observable]: observable,}
其中 subscribe用于完成订阅,dispatch就是用于发起一次state更新的函数,并唤起subscribe订阅的回调,getState返回当前的state,replaceReducer用于替换reducer.
完成订阅的步骤非常简单,就是往一个数组里添加回调函数:
...nextListeners.push(listener)...
而唤起的时候,就是遍历listeners,依次执行回调:
...const listeners = (currentListeners = nextListeners)for (let i = 0; i < listeners.length; i++) {const listener = listeners[i]listener()}...
篇2 connect究竟是个什么东西,以及他的作用
connect 主要有以下几个作用:
- 初始化
mapStateToProps,mapDispatchToProps,以及mergeProps(这是connect的第三个参数),主要是参数校验,然后会将其包装成具有统一签名:initProxySelector(dispatch, { displayName })的函数。 - 初始化
selectorFactory,该函数用于生成最终我们定义的组件的props。 - 更多的工作,其实就是为
connectAdvanced函数提供初始入参配置。
最终会返回connectAdvanced函数的执行结果。
connectAdvanced除了基本的一些初始化参数配置外,还有一点就是拿到了context,该context的value在Provider构建时进行初始化,会传递store和subscription两个参数。store前边已经介绍过了,现在对subscription做一简单介绍:
subscription是Subscription类的实例,主要是封装了组件到store的订阅逻辑,同时也可以处理嵌套的子组件的订阅逻辑,保证更新是由父到子进行的,一般调用该实例的trySubscribe方法完成订阅。
说完subscription,我们继续connectAdvanced。connectAdvanced最终会返回wrapWithConnect函数,上述代码中的App也正是wrapWithConnect的执行结果。
wrapWithConnect,可以看成一个高阶组件,接受WrappedComponent即一个React组件WrappedComponent作为入参,在其内部主要干了这么几件事:
- 初始化
createChildSelector,该函数用于计算出当前组件的props - 初始化
ConnectFunction, 该内部主要功能有:- 使用
useMemo完成Subscription的初始化,此Subscription实例初始化的时候,作为子组件的subscription,一般都会传进去父组件的subscription。父组件的subscription就是由connectAdvanced拿到的context进行传递的。 - 用
useLayoutEffect(客户端)或useEffect(ssr),完成订阅(源代码做了简单的修改):
```javascript...subscription.onStateChange = checkForUpdatessubscription.trySubscribe()......//subscription内部 trySubscribe// handleChangeWrapper 最终执行时会调起onStateChangethis.parentSub.addNestedSub(this.handleChangeWrapper)...// addNestedSub...this.listeners.subscribe(listener)...
- 使用
```可以看到,子组件内部的订阅最终会挂在父组件传下来的`subscription`的`listeners`上。这样就完成了订阅。`subscription`的`listeners`,是`subscription`完成其订阅的内部数据结构,使用双向链表即:```{callback,next: null,prev: null,}```保存订阅的回调函数,订阅时调用`listeners.subscribe(listener)`:```let listener = (last = {callback,next: null,prev: last,})if (listener.prev) {// 不是第一个节点,就链到前一个的后边listener.prev.next = listener} else { // 否则就是第一个first = listener}```唤起的时候,代码如下:```...batch(() => {let listener = firstwhile (listener) {listener.callback()listener = listener.next}})...```就是一个简单的链表遍历,值得一提的是这个`batch`,实际运行后,调用的是`React`内部的名为`batchedUpdates$1`的函数,该函数会改变`executionContext`的值(`executionContext |= BatchedContext`),其直接结果就是在`React`更新时,`scheduleUpdateOnFiber`的执行不会走`renderRootSync`这样的同步更新,而是会安排一个异步回调,将所有更新合并进行。*这一步可以称之为性能优化*。* 计算出`actualChildProps`后 ,最终返回 :```javascript<ContextToUse.Provider value={overriddenContextValue}>{renderedWrappedComponent}</ContextToUse.Provider>````ContextToUse`一般就是上边提到的`context`,`overriddenContextValue`数据结构同`context`的value,只是会将`subscription`替换为当前组件的`subscription`。在组件上包裹一层provider是因为,react总是就近取context,这样一来可以保证`renderedWrappedComponent`的嵌套子组件如果访问`ContextToUse`总是可以取到和当前组件相同的context实例,并且当子组件有订阅行为时,可以将其订阅在自己的`addNestedSub`,保证更新由父到子进行。
- 最后将
WrappedComponent的静态属性合并到ConnectFunction上,返回ConnectFunction。
篇3 Provider
实际就是一个普通的组件,主要办了这么几件事:
- 初始化自身的
subscription,
并协同const subscription = new Subscription(store)
store使用useMemo一起挂到contextValue上 - 用
useLayoutEffect(客户端)或useEffect(ssr),完成订阅(源代码做了简单的修改):...subscription.onStateChange = subscription.notifyNestedSubs...subscription.trySubscribe()...//subscription内部 trySubscribe...this.store.subscribe(this.handleChangeWrapper)...//store.subscribe nextListeners介绍store的时候提到过就是一个数组,listener就是上边传过去的回调nextListeners.push(listener)...
- 最后返回
<Context.Provider value={contextValue}>{children}</Context.Provider>
ConnectFunction和Provider是在React更新流程的beiginWor阶段调用的。
介绍完基本概念可以正式开始介绍数据的更新和派发了。
篇4 数据派发
派发数据相对来说比较简单,可以想想,当数据更新后,在一个使用connect一顿操作过后的原始组件,其对外窗口只有一个,那就是props,所以更新后的数据主要就是props的计算。
ConnectFunction内部计算props时,自childPropsSelector(store.getState(), wrapperProps)始,中间经历了很长的链路,这里的细节我们无需关注,但是可以给出最后的结果就是:
// pureFinalPropsSelectorFactory...stateProps = mapStateToProps(state, ownProps)dispatchProps = mapDispatchToProps(dispatch, ownProps)mergedProps = mergeProps(stateProps, dispatchProps, ownProps)...
最后的返回结果便是,stateProps, dispatchProps, ownProps合并之后的结果。基于此呢,组件就可以从props读取到react-redux派发下来的数据了。
篇5 数据更新
一次更新由dispatch发起,基本工作就是更新state,唤起订阅的回调函数
流程概览
hooks模式
篇1 useSelector 实现数据派发
从函数签名说起
useSelector(selector, equalityFn = refEquality)
selector是一个自定义函数,useSelector调用的时候,会传进去当前state作为参数,最终返回的东西我们叫它selectedStateequalityFn则是用于判断selector返回值前后是否发生了变化
内部实现上,也会有自己的订阅行为,和subscription,实现订阅的时机和上述Provider的相同,该订阅函数主要实现的是selectedState的更新:
//checkForUpdates内部...const newStoreState = store.getState()const newSelectedState = latestSelector.current(newStoreState)if (equalityFn(newSelectedState, latestSelectedState.current)) {return}latestSelectedState.current = newSelectedStatelatestStoreState.current = newStoreState// 确确实实更新了,则发起一次更新 这个函数实际还是useReducer的dispatchforceRender()...
useSelector每次执行,都会从store拿到最新的selectedState并返回。同时,只有selector、storeState二者有其一发生变化,或者订阅函数执行时发生变化,同时,equalityFn此时的执行结果为false,也就是说前后的selectedState确实变化了,selectedState的值才会更新。
篇2 useDispatch 实现数据更新
emmm,这玩意就是store.dispatch,具体原理同上边的dispatch

浙公网安备 33010602011771号