React Context提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法,这样我们就不需要通过层层的props传递实现通信,这大大降低了我们的代码复杂度,同时也带来了新的问题
- 组件变得很难复用
- Context 对象 提供的Provider组件允许消费组件订阅 context 的变化,一旦变化它内部的所有消费组件都会重新渲染,会产生性能问题。
虽然第二点我们可以使用useMemo处理:
function Button() {
let {theme} = useContext(appContext)
return useMemo(() => {
return <Children class={theme} />
}, [theme])
}
但是手动优化和管理依赖必然会带来一定程度的心智负担,那么react-redux作为社区知名的状态管理库,肯定被很多大型项目所使用,大型项目里的状态可能分散在各个模块下,它是怎么解决上述的性能缺陷的呢?
我们知道 Redux 是一个单一的状态机,它只关注state的变化,至于视图层怎么变化,关键在于React-redux。
先看一下我们平时在代码中是怎么使用:(完整代码👇)
import {createStore} from 'redux'
import reducer from './reducer'
import {Provider} from 'react-redux'
let store = createStore(todoApp);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
function App() {
return (
<>
<WrapChildFunc />
<WrapChildClass />
</>
)
}
如上代码,react-redux提供了Provider组件,接收一个store对象(redux的store对象),重点源码如下
function Provider({ store, context, children }) {
const contextValue = useMemo(() => {
const subscription = new Subscription(store)
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription
}
}, [store])
const previousState = useMemo(() => store.getState(), [store])
useEffect(() => {
const { subscription } = contextValue
subscription.trySubscribe()
if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
return () => {
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
}, [contextValue, previousState])
const Context = context || ReactReduxContext
return <Context.Provider value={contextValue}>
{children}</Context.Provider> // 渲染子级元素,使整个应用成为Provider的子组件
}
其实Provider主要做了两件事:
- 在原应用组件上包裹一层,使整个应用成为Provider的子组件
- 接收redux的store作为props,通过context对象传递给所包含的消费组件 还是利用Context对象与其他组件共享数据
Class组件
class ChildClass extends React.Component {
constructor(props) {
super(props)
console.log('class', this.props, this.props.children)
}
render() {
const {ClassNum, addClick, reduceClick} = this.props
return (
<>
<div className="App">{ClassNum}</div>
<div>Class组件</div>
<button onClick={addClick}>+</button>
<button onClick={reduceClick}>-</button>
</>
)
}
}
const mapStateToProps = state => {
return {
ClassNum: state.ClassNum
}
}
const mapDispatchToProps = dispatch => {
return {
addClick: () => {
dispatch({type: 'CLASS_ADD'})
},
reduceClick: () => {
dispatch({type: 'CLASS_REDUCE'})
}
}
}
// 使用connect容器组件包裹ChildClass UI组件
export const WrapChildClass = connect(mapStateToProps, mapDispatchToProps)(ChildClass)
connect模块就是一个高阶组件,主要作用是:
- connect通过context获取Provider中的store,通过store.getState()获取state tree
- connect模块返回函数wrapWithComponent
- wrapWithConnect返回一个ReactComponent对象 Connect,Connect重新render外部传入的原组件WrappedComponent(UI组件),并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent 具体代码比较复杂,大家感兴趣的可以看一下源码
Function 组件
我们主要分析下Hooks写法
export default function Child () {
// 获取redux中state树中指定的值
const {FuncNum} = useSelector((state: stateType) => {
console.log('Func', state)
return {
FuncNum: state.FuncNum
}
})
// 饮用store中的dispatch 方法
const dispth = useDispatch()
const addClick = useCallback(
() => dispth({type: 'FUNC_ADD'}),
[FuncNum],
)
const reduceClick = useCallback(
() => dispth({type: 'FUNC_REDUCE'}),
[FuncNum],
)
return useMemo(() => (
<div>
<div className="App">{FuncNum}</div>
{Math.random()}
<div>Func组件</div>
<button onClick={addClick}>+</button>
<button onClick={reduceClick}>-</button>
</div>
), [FuncNum])
}
可以看到,我们通过useSelector获取store中state对象对应的数据,再通过useDispatch方法获取到dispatch的引用
useDispatch
export function createDispatchHook(context = ReactReduxContext) {
const useStore =
context === ReactReduxContext ? useDefaultStore : createStoreHook(context)
return function useDispatch() {
const store = useStore()
return store.dispatch
}
}
可以看到useDispatch只返回了dispatch的引用,并没有做其他的事情,那么我们dispatch(Action)后,state变化,react是怎么知道需要更新视图的?关键就在于useSelector
useSelector
function useSelectorWithStoreAndSubscription(
selector,
equalityFn,
store,
contextSub
) {
// 强制渲染 reducer: s => s+1, state 初始值: 0; forceRender ===> dispatch
const [, forceRender] = useReducer(s => s + 1, 0)
// 创建订阅函数
const subscription = useMemo(() => new Subscription(store, contextSub), [ store, contextSub ])
const latestSubscriptionCallbackError = useRef()
// select函数
const latestSelector = useRef()
// store中的state
const latestStoreState = useRef()
// select函数返回state
const latestSelectedState = useRef()
const storeState = store.getState()
let selectedState
try {
if (
selector !== latestSelector.current ||
storeState !== latestStoreState.current ||
latestSubscriptionCallbackError.current
) {
// useSelector 选择的state
selectedState = selector(storeState)
} else {
selectedState = latestSelectedState.current
}
} catch (err) {
...
}
// effect hook
useIsomorphicLayoutEffect(() => {
latestSelector.current = selector
latestStoreState.current = storeState
latestSelectedState.current = selectedState
latestSubscriptionCallbackError.current = undefined
})
// effect hook
useIsomorphicLayoutEffect(() => {
function checkForUpdates() {
try {
const newSelectedState = latestSelector.current(store.getState())
// 判断前后选择的state是否相等
if (equalityFn(newSelectedState, latestSelectedState.current)) {
return
}
latestSelectedState.current = newSelectedState
} catch (err) {
latestSubscriptionCallbackError.current = err
}
forceRender()
}
// stateChange的回调
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
checkForUpdates()
return () => subscription.tryUnsubscribe()
}, [store, subscription])
return selectedState
}
export function createSelectorHook(context = ReactReduxContext) {
const useReduxContext =
context === ReactReduxContext
? useDefaultReduxContext
: () => useContext(context)
return function useSelector(selector, equalityFn = refEquality) {
const { store, subscription: contextSub } = useReduxContext()
const selectedState = useSelectorWithStoreAndSubscription(
selector,
equalityFn,
store,
contextSub
)
return selectedState
}
}
-
关键方法: const [, forceRender] = useReducer(s => s + 1, 0): 利用useReducer定义了一个计数器,用于强制渲染此组件
checkForUpdates:store变化后订阅函数触发的处理逻辑
-
关键流程:初始化
- 利用useSelector传入的selector函数获取Redux中store对应的值
- 定义一个latestSelectedState,用于保存上一次selector返回的值
- 定义state变化的处理函数checkForUpdates
- 利用store.subscribe订阅一次redux的store,当下次store变化后,触发订阅函数执行checkForUpdates
-
关键流程:更新
- 当用户dispacth触发了store变化后,订阅函数执行checkForUpdates
- 通过store.getState()获取最新的state值,通过equalityFn函数比较newSelectedState和latestSelectedState,如果有变化就执行forceRender,触发react创建update对象,强制渲染;否则直接return