第一篇連接: redux真的不復雜——源碼解讀javascript
預備知識:1. 瞭解redux的基本使用; 2. Context APIhtml
瞭解redux原理更好,若是不瞭解,不妨先看看第一篇博客。java
redux是一個狀態管理的工具,本質上是一個js對象(包含狀態,以及一些處理狀態的方法)。react
因此redux具備很強的適應性,能夠配合其餘工具/框架一塊兒使用。git
react-redux則是一個讓你更容易地在react中使用redux的工具。github
咱們使用redux的目的是存儲狀態,在react中有存儲狀態的東西嗎?redux
有,state。可是state有一些侷限性。api
對於一個組件來講,(用setState觸發)state的變化會觸發這個組件以及它全部子組件的更新,因此爲了優化考慮,咱們每每將state放在更「局部」的組件中,這樣state的變化只會引發最少的(有必要的)更新。緩存
那麼若是咱們的react應用有一些狀態在多個地方均可能用到,特別是一些全局數據(好比當前用戶信息,全局的通知等等),對於這些數據咱們有兩個選擇:app
在各個局部組件中各存一份
優勢是:保證了數據的變化只會引發最小的組件更新,
缺點是:
在全局組件中存一份
優勢是:只須要在一個地方請求API,數據是徹底同步的
缺點是:數據的變化會引發整個應用大量的更新。
你會發現這兩個選擇各有優缺點,但仔細想一想,你會發現其實咱們有第三個選擇:
利用Context API。在全局組件外包一個Provider,將數據存在Provider上。子組件經過訪問context來使用這些數據
這樣就兼顧了各個優勢:
一切彷彿變得美好了,不是嗎?
可是問題又來了:
這個時候你就會想到redux了,redux提供了一套優雅的管理狀態的方案。
優雅在什麼地方?接着往下看。
想要使用context api的方案,而且還要使用redux,那麼你須要作的事情有:
差很少就這些,是否是也挺簡單?
可是從技術上來說,你須要作的事情有:
難受嗎?
你須要react-redux,它幫你把這些操做都封裝了起來。
爲了源碼更清晰,分析時只展現了一些核心代碼,省略了錯誤處理,通用性處理等代碼。建議你參照着真正的源碼閱讀。
在看源碼以前,先簡單回憶一下react-redux的用法:
mapStateToProps
和mapDispatchToProps
兩個參數(還有其餘可選參數mergeProps
,options
),獲得一個高階組件【注1】。【注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.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函數的做用是——」鏈接「,將一個正常的組件與咱們的store鏈接起來,這樣子組件就能夠「使用」store了。
然而其實是如何實現鏈接的呢?若是你使用過react-redux的話,你就知道是:
咱們使用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
的做用是:
根據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)
}
複製代碼
看完難免又有疑惑了:
一個一個來看。
// ...
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
函數。
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))
}
}
}
複製代碼
原來這麼簡單啊,就只是:
在須要的時候:
運行選擇器,獲得須要添加的額外的props
根據運行的結果肯定是否更新Connect組件
渲染時向被包裹組件添加額外的props。
若是你看到了最後,你會發現,react-redux的實現方式和咱們文章開頭的解決方案一毛同樣:
實現的亮點在於,react-redux用了高階組件這種優雅的方式,將這種需求進行了封裝。
若是有疑問,或者想要交流的地方,歡迎在評論區討論。