第一篇連結: redux真的不復雜——原始碼解讀
預備知識:1. 瞭解redux的基本使用; 2. Context API
瞭解redux原理更好,如果不瞭解,不妨先看看第一篇部落格。
redux是一個狀態管理的工具,本質上是一個js物件(包含狀態,以及一些處理狀態的方法)。
所以redux具有很強的適應性,可以配合其他工具/框架一起使用。
react-redux則是一個讓你更容易地在react中使用redux的工具。
為什麼需要redux
我們使用redux的目的是儲存狀態,在react中有儲存狀態的東西嗎?
有,state。但是state有一些侷限性。
對於一個元件來說,(用setState觸發)state的變化會觸發這個元件以及它所有子元件的更新,所以為了優化考慮,我們往往將state放在更“區域性”的元件中,這樣state的變化只會引起最少的(有必要的)更新。
那麼如果我們的react應用有一些狀態在多個地方都可能用到,特別是一些全域性資料(比如當前使用者資訊,全域性的通知等等),對於這些資料我們有兩個選擇:
-
在各個區域性元件中各存一份
優點是:保證了資料的變化只會引起最小的元件更新,
缺點是:
- 難以保證各處的資料同步
- 可能各處會重複請求相同的API,損失了一定效能
-
在全域性元件中存一份
優點是:只需要在一個地方請求API,資料是完全同步的
缺點是:資料的變化會引起整個應用大量的更新。
你會發現這兩個選擇各有優缺點,但仔細想想,你會發現其實我們有第三個選擇:
利用Context API。在全域性元件外包一個Provider,將資料存在Provider上。子元件通過訪問context來使用這些資料
這樣就兼顧了各個優點:
- 資料易同步
- 只需在一個地方請求API
- 全域性資料的更新不會引起元件的更新(Context api的特性)
一切彷彿變得美好了,不是嗎?
但是問題又來了:
- 全域性(Provider上)的資料你如何管理(特別是當資料的結構複雜了之後)?
- 子元件如何根據需要更新這些資料?
這個時候你就會想到redux了,redux提供了一套優雅的管理狀態的方案。
優雅在什麼地方?接著往下看。
為什麼需要react-redux
想要使用context api的方案,並且還要使用redux,那麼你需要做的事情有:
- 用redux建立一個store
- 在react應用的最外層包一個Provider,store放在Provider上。
- 子元件想要獲取資料時:在元件外包一個Consumer,Consumer獲取到store,傳遞給元件。
- 當子元件想要更新資料時:呼叫store的dispatch方法觸發store的更新。
差不多就這些,是不是也挺簡單?
但是從技術上來講,你需要做的事情有:
- 自己寫一個provider
- 每次想要使用provider的資料的時候,自己寫一個Consumer元件
- 每次想要更新資料的時候,自己呼叫store的diapatch方法。
難受嗎?
你需要react-redux,它幫你把這些操作都封裝了起來。
react-redux原始碼分析
為了原始碼更清晰,分析時只展示了一些核心程式碼,省略了錯誤處理,通用性處理等程式碼。建議你參照著真正的原始碼閱讀。
react-redux的使用
在看原始碼之前,先簡單回憶一下react-redux的用法:
- 從react-redux庫引入一個Provider元件,將redux建立的store作為屬性傳遞給這個Provider元件。
- 給connect傳遞
mapStateToProps
和mapDispatchToProps
兩個引數(還有其他可選引數mergeProps
,options
),得到一個高階元件【注1】。 - 用高階元件“包裝”我們自己的元件,就能在自己的元件中得到對應的props。
【注1】高階元件:輸入為元件,輸出為另一個元件的函式。
ok,我們來看看原始碼的結構:
還挺複雜,不過沒關係,看看index.js:
//index.js
import Provider, { createProvider } from './components/Provider'
import connectAdvanced from './components/connectAdvanced'
import connect from './connect/connect'
export { Provider, createProvider, connectAdvanced, connect }
複製程式碼
createProvider和connectAdvanced這兩個方法是定製react-redux的工作方式時使用的,我們一般不會直接用到,所以我們就從Provider和connect這兩個模組入手。
Provider
Provider.js
的原始碼比較少,結構也很簡單:
//Provider.js
//import略
//輸出建立Provider的函式
export function createProvider(storeKey = 'store'){
class Provider extends Component {
// ...
}
return Provider
}
//預設輸出建立後的Provider
export default createProvider()
複製程式碼
這個檔案提供了兩個輸出,一個是建立Provider的函式,還有一個是建立過的Provider。
下面我們來詳細看看Provider這個元件具體是如何實現的:
class Provider extends Component {
// 訪問context的鉤子函式【注2】
getChildContext() {
// 返回一個物件,鍵是store的標識(可自定義),值是store
return { [storeKey]: this[storeKey] }
}
constructor(props, context) {
super(props, context)
// 將我們(通過props)傳進來的store存在自己的例項中。
this[storeKey] = props.store
}
render() {
// Children.only是react提供的API函式,
// 作用是限制this.props.children只能是一個React元素,否則會報錯
return Children.only(this.props.children)
}
}
Provider.childContextTypes = {
// ...
}
複製程式碼
看到這裡,可以發現Provider的實現非常簡單,只是將我們(通過props)傳進去的store,建立了一個context而已。
注2:React可以通過在一個元件中設定
getChildContext
和childContextTypes
來建立context,在其子元件中設定contextTypes
屬性來接收context
原來,react-redux只是將store放到Provider元件的context上。那麼問題來了,
問題1:
Provider的子元件如何使用store?
Connect
顧名思義,connect函式的作用是——”連線“,將一個正常的元件與我們的store連線起來,這樣子元件就可以“使用”store了。
然而實際上是如何實現連線的呢?如果你使用過react-redux的話,你就知道是:
- 使用connect建立一個高階元件
- 然後用高階元件包裹一個元件,向元件傳遞額外的props
- 元件內部通過props,能夠:
- 讀取store
- 觸發(dispatch)store中的action。
我們使用connect建立高階元件時通常會傳入mapStateToProps
,mapDispatchToProps
,然而高階元件並沒有直接將其通過props傳進被包裹元件——而是經過篩選和包裝後,再通過props傳入一些東西(store的一部分分支,或者自動dispatch的action creator)。
回答問題1(Provider的子元件如何使用store?):
通過將“篩選和包裝”後的與store相關的東西,通過props傳入元件,來使用store。
可見,篩選和包裝是一個非常重要的任務,那麼它是在connect中實現的嗎?
我們來看看connect的原始碼:
//connect.js
export function createConnect({
connectHOC = connectAdvanced, // 記住這個函式,後面會講
// 選擇器工廠:根據一些配置生成一個選擇器
// (選擇器工廠的實現,以及選擇器的作用後面我們會講)
selectorFactory = defaultSelectorFactory
// ... 一些處理connect引數的工廠函式
} = {}) {
// 這裡才是connect函式
return function connect({
mapStateToProps,
mapDispatchToProps,
mergeProps,
options={}
}){
// connect的引數有:mapStateToProps,mapDispatchToProps,mergeProps,options
// 然而這些引數並不能直接使用,它們可能是物件,也可能是函式
// 所以在這裡進行通用性處理,是他們可以直接使用
// connect返回了一個高階元件(由connectHOC建立)
return connectHOC(selectorFactory, { /*配置物件*/ })
}
}
// 預設輸出的connect
export default createConnect()
複製程式碼
似乎connect並沒有做篩選和包裝這件事,僅僅返回了一個高階元件,而這個高階元件預設是由connectAdvanced
建立的。
所以也就是說:
connect
只是connectAdvanced
(這個函式一會再看)的一個包裝,connect
本身只是一個預處理函式,真正的“篩選和包裝”其實是在connectAdvanced
這個函式裡進行。
connect
做的事情僅僅是:
將選擇器工廠和配置物件傳給connectAdvanced
進行進一步處理。
(似乎“篩選和包裝”的功能是通過選擇器工廠和配置物件實現的,下面我們來看看是不是如此。)
問題2:
“篩選和包裝”的功能是如何實現的?
connectAdvanced
從上面的程式碼可以看出,connectAdvanced
的作用是:
根據selectorFactory
和配置物件,建立一個高階元件。
下面看看原始碼:
function connectAdvanced(selectorFactory, { /*options配置物件*/ }) {
// ... 根據options初始化一些內部變數
//返回一個高階元件(輸入為一個元件,輸出為另一個元件的函式)
return function wrapWithConnect(WrappedComponent){
// 高階元件返回的元件Connect
class Connect extends Component {
constructor(props, context) {
super(props, context)
this.state = {}
// 從props或context讀取store
//(因為WrappedComponent自己可能也是一個Provider)
// 在本文的分析中我們忽略Provider巢狀的這種情況
this.store = props[storeKey] || context[storeKey]
// ...
this.initSelector() // 初始化一個selector(後面會講)
}
// 初始化selector的函式
initSelector() {/*...*/}
// ... 其他一些生命週期函式和工具函式
}
Connect.contextTypes = contextTypes // 獲取外層的context
Connect.propTypes = contextTypes
// 返回Connect元件的時候多了一步處理
// 這個函式的作用是將WrappedComponent上的靜態方法拷貝到Connect上,並返回Connect
// 這樣就可以完全把Connect當作一個“WrappedComponent”使用
return hoistNonReactStatics(Connect, WrappedComponent);
}
}
複製程式碼
結構依舊很簡單,就是返回一個高階元件。所謂高階元件,其實是一個函式。
高階元件接收被包裹的元件,返回一個Connect元件,下面我們的重點就放在這個Connect元件是如何建立的。
constructor看起來很普通,只不過從context中獲取了store,然後將store存下來。還呼叫了一個initSelector()
函式,初始化選擇器?選擇器是什麼東西???別急我們一步一步來看。
看看其原始碼:
initSelector() {
// 傳入dispatch方法和配置物件,得到一個原始選擇器
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
// 對這個原始選擇器進行進一步處理,得到一個最終的"stateful"的選擇器selector
this.selector = makeSelectorStateful(sourceSelector, this.store)
// 呼叫selector的一個方法
this.selector.run(this.props)
}
複製程式碼
看完不免又有疑惑了:
- selectorFactory到底做了什麼?
- makeSelectorStateful做了什麼?新增的run方法是做什麼的?
一個一個來看。
1. selectorFactory----------------------------------
// ...
function impureFinalPropsSelectorFactory({ /*配置物件*/ }){
// ...
}
function pureFinalPropsSelectorFactory({ /*配置物件*/ }){
// ...
}
export default function finalPropsSelectorFactory(dispatch, { /*配置物件*/ }) {
// 從配置物件拿到initMapStateToProps,initMapDispatchToProps,initMergeProps
// 這些方法是對connect函式引數(mapStateToProps,mapDispatchToProps,mergeProps)的包裝
// 所以呼叫後返回的是增強後,可以直接使用的同名函式
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
const mergeProps = initMergeProps(dispatch, options)
// 根據options的pure欄位確定使用哪種工廠函式
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
// 使用選擇器工廠,返回一個選擇器
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options //areStatesEqual, areOwnPropsEqual, areStatePropsEqual
)
}
複製程式碼
我們來看看兩種工廠函式:
// pure為false時的選擇器工廠
// 功能及其簡單,每次呼叫都返回一個新組裝的props
function impureFinalPropsSelectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch
) {
return function impureFinalPropsSelector(state, ownProps) {
return mergeProps(
mapStateToProps(state, ownProps),
mapDispatchToProps(dispatch, ownProps),
ownProps
)
}
}
// pure為true時的選擇器工廠
function pureFinalPropsSelectorFactory({
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
{ areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
}){
let hasRunAtLeastOnce = false // 是否是第一次使用選擇器
let state // 這個state並不是元件的狀態,而是redux的store
let ownProps // 儲存上一次傳入的ownProps
let stateProps // 通過mapStateToProps篩選出的要放進props的資料
let dispatchProps // 通過mapDispatchToProps篩選出的要放進props的資料
let mergedProps // 合併後的props,最終選擇器返回的就是這個引數。
// 第一次呼叫選擇器的函式
function handleFirstCall(firstState, firstOwnProps){
state = firstState
ownProps = firstOwnProps
stateProps = mapStateToProps(state, ownProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
hasRunAtLeastOnce = true
return mergedProps
}
// 處理不同情況時返回什麼props
// 這三個函式也沒什麼稀奇的騷操作,就不展開了
// 僅僅是根據需要呼叫mapStateToProps或mapDispatchToProps得到stateProps和dispatchProps
// 然後呼叫mergeProps得到mergedProps
function handleNewPropsAndNewState(){}
function handleNewProps(){}
function handleNewState(){}
function handleSubsequentCalls(nextState, nextOwnProps) {
// areOwnPropsEqual,areStatesEqual用於比較新舊state,props是否相同
// 這是從配置物件中拿到的方法,實現方式就只是一個淺比較
const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
const stateChanged = !areStatesEqual(nextState, state)
state = nextState
ownProps = nextOwnProps
if (propsChanged && stateChanged) return handleNewPropsAndNewState()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState()
return mergedProps
}
// 這裡是返回的選擇器
return function pureFinalPropsSelector(nextState, nextOwnProps) {
return hasRunAtLeastOnce // 判斷是否是第一次使用選擇器
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}
複製程式碼
可以看出選擇器工廠返回了一個函式pureFinalPropsSelector
,這就一個選擇器。
可以看出,選擇器的功能,就是接收nextState
和nextOwnProps
,返回一個經過“篩選和包裝”的props。返回的props可以直接傳給被包裹的元件。
回答問題2(“篩選和包裝”的功能是如何實現的?):
使用選擇器工廠,根據我們傳進去的配置項(經過處理的mapXXXToProps,dispatch,淺比較方法),生成一個具有“篩選和包裝”功能的選擇器
在connectAvanced
中使用的selectorFactory
已經弄明白了,下面看看另一個makeSelectorStateful
函式。
2. makeSelectorStateful---------------------------------
function makeSelectorStateful(sourceSelector, store) {
// 建立了一個selector物件,這個物件有一個run方法
const selector = {
// run方法接收原始props(外部傳給被包裹元件的props),
// 並且呼叫了一次原始選擇器,得到呼叫後的props,
// 將新props和內部快取的舊props比較,
// 根據結果,設定selector的shouldComponentUpdate屬性。
run: function runComponentSelector(props) {
try {
const nextProps = sourceSelector(store.getState(), props)
if (nextProps !== selector.props || selector.error) {
selector.shouldComponentUpdate = true
selector.props = nextProps
selector.error = null
}
} catch (error) {
selector.shouldComponentUpdate = true
selector.error = error
}
}
}
return selector
}
複製程式碼
現在再回到connectAdvanced的原始碼:
function connectAdvanced(selectorFactory, { /*options配置物件*/ }) {
//返回一個高階元件(輸入為一個元件,輸出為另一個元件的函式)
return function wrapWithConnect(WrappedComponent){
// 高階元件返回的元件Connect
class Connect extends Component {
constructor(props, context) {
super(props, context)
this.state = {}
// 從props或context讀取store
this.store = props[storeKey] || context[storeKey]
this.initSelector() // 初始化一個選擇器
}
initSelector() {/*...*/}
// 我們現在要重點看這裡!!!!!!!!
// ... 其他一些生命週期函式和工具函式
}
// ...
return hoistNonReactStatics(Connect, WrappedComponent);
}
}
複製程式碼
我們已經知道選擇器的功能是:獲取“篩選和包裝”後的props,現在我們看看Connect元件是如何使用選擇器的。將關注點放在Connect這個元件的生命週期函式是如何使用的:
class Connect extends Component {
constructor(props, context) {
// ...
this.initSelector()
}
componentDidMount() {
// 向store中新增監聽器,監聽器函式在下面
// 就不詳細看這個函式的實現了,就是簡單的呼叫store的subscribe方法
this.subscription.trySubscribe()
// 執行選擇器,根據選擇器執行後的結果判斷是否需要更新
this.selector.run(this.props)
if (this.selector.shouldComponentUpdate) this.forceUpdate()
}
// 當接收新的props時,執行選擇器
componentWillReceiveProps(nextProps) {
this.selector.run(nextProps)
}
// 根據選擇器執行後的結果判斷是否需要更新
shouldComponentUpdate() {
return this.selector.shouldComponentUpdate
}
// 解除安裝元件時清理記憶體
componentWillUnmount() {
// 解除安裝監聽器
if (this.subscription) this.subscription.tryUnsubscribe()
this.store = null
this.selector.run = noop // 空函式function noop() {}
this.selector.shouldComponentUpdate = false
}
// 監聽器,將會用subscribe方法新增到store上,每當store被dispatch會被呼叫
onStateChange() {
this.selector.run(this.props)
}
render() {
const selector = this.selector
selector.shouldComponentUpdate = false
if (selector.error) {
throw selector.error
} else {
// addExtraProps方法的作用時將selector篩選後的props,新增到原本的props上。
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
}
}
複製程式碼
原來這麼簡單啊,就只是:
-
在需要的時候:
- Connect元件第一次裝載元件時
- Connect元件接收props時
- 監聽store的監聽器被觸發時
執行選擇器,得到需要新增的額外的props
-
根據執行的結果確定是否更新Connect元件
-
渲染時向被包裹元件新增額外的props。
總結
如果你看到了最後,你會發現,react-redux的實現方式和我們文章開頭的解決方案一毛一樣:
- store存在父元件的context上
- 給子元件新增額外的props,以實現和store的互動
實現的亮點在於,react-redux用了高階元件這種優雅的方式,將這種需求進行了封裝。
如果有疑問,或者想要交流的地方,歡迎在評論區討論。