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),完成订阅(源代码做了简单的修改):...
subscription.onStateChange = checkForUpdates
subscription.trySubscribe()
...
...
//subscription内部 trySubscribe
// handleChangeWrapper 最终执行时会调起onStateChange
this.parentSub.addNestedSub(this.handleChangeWrapper)
...
// addNestedSub
...
this.listeners.subscribe(listener)
...
可以看到,子组件内部的订阅最终会挂在父组件传下来的
subscription
的listeners
上。这样就完成了订阅。subscription
的listeners
,是subscription
完成其订阅的内部数据结构,使用双向链表即:{
callback,
next: null,
prev: null,
}
保存订阅的回调函数,唤起的时候,代码如下:
...
batch(() => {
let listener = first
while (listener) {
listener.callback()
listener = listener.next
}
})
...
就是一个简单的链表遍历,值得一提的是这个
batch
,实际运行后,调用的是React
内部的名为batchedUpdates$1
的函数,该函数会改变executionContext
的值(executionContext |= BatchedContext
),其直接结果就是在React
更新时,scheduleUpdateOnFiber
的执行不会走renderRootSync
这样的同步更新,而是会安排一个异步回调,将所有更新合并进行。这一步可以称之为性能优化。- 计算出
actualChildProps
后 ,最终返回 :<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
,唤起订阅的回调函数
流程概览
更多相关文章
- Vue 中的 .sync 修饰符stop修饰符prevent修饰符可以press.enter
- 几种常用设计模式的简单示例
- 快递100显示查询错误?快递100快递查询类FAQ
- bootstrap常用组件样式使用之,导航,列表,按钮
- Vue组件及路由
- vuex面试题
- vue父组件与子组件的数据传递
- PHP:【微信小程序】微信小程序部分组件,微信小程序轮播图
- 微信小程序数据操作、自定义事件、微信API、路由组件、变量作用