前言
Redux作為通用的狀態管理器,可以搭配任意介面框架。所以並搭配react使用的話就要藉助redux官方提供的React繫結庫react-redux,以高效靈活的在react中使用redux。下面我們一起看看是react-redux如何靈活高效的
redux 概述
在開始之間還是大概提一下redux的內容,以免脫節。比較早的時候也解讀了下redux的原始碼實現,可以參考一下
Redux 是 JavaScript 狀態容器,旨在提供可預測化的狀態管理。
其包括action、store、reducer等三部分:
在理解各部分作用之前,我們可以通過一個更新資料的例子來捋下思路:
- 要更新資料,肯定有個資料庫來儲存和維護資料。即資料層。
- 具體如何更新,需要有負責執行的部分,即邏輯處理層。
- 具體何時更新哪個欄位、何時更新,同樣需要分發層來控制。
根據上面的例子我們再對比下redux的流程圖(圖片來自阮一峰大佬):
可以對照著來理解不同部分的作用。
action
就如上面所說負責更新欄位和更新時機。 使用者接觸到的是view(即使用者介面),對應的更新資訊通過acton傳遞給reducer。
function addTodo(text) {
return {
// 更新型別及具體內容
type: ADD_TODO,
text
}
}
reducer
負責更新資料的具體邏輯。
即根據當前state及action攜帶的資訊合成新的資料。
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
// 不同更新型別的處理邏輯不同
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
default:
return state
}
}
store
store就是負責維護和管理資料。
此外還有dispatch,subscrible等api來完成更新事件的分發。
例如:
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
import {
addTodo} from './actions'
// 註冊監聽事件
const unsubscribe = store.subscribe(() => console.log(store.getState()))
// 發起一系列 action
store.dispatch(addTodo('Learn about actions'))
// 停止監聽
unsubscribe()
到這裡,我們應該就大概明白redux如何更新管理資料的了。
- 通過store.subscribe來監聽狀態更新,這是響應變化的重要一步。
- 然後通過stroe.getState()獲取相應資料
- 具體更新通過action和reducer來實現了。
那麼對照react-redux的例項官方demo,來結合React的時候,會發現redux使用有些不同之處。
不同之處
大概可以有下面這三點:
- 元件沒有顯示呼叫store.subscrible()
- state也不是通過Store.getState()來獲取。
- 多了Provider和connect方法
可以猜測,上述差異是React-redux幫我們封裝了繫結監聽等過程,避免需要每個應用都重複相同的操作。使得React元件的資料來源只關注props和state。
下面帶著這些問題深入瞭解React-redux.
react-redux
本質上 react-redux也是react高階元件HOC的一種實現。其基於 容器元件和展示元件相分離 的開發思想來實現的。
其核心是通過兩部分來實現:
1、Provider
2、container通過connect來解除手動呼叫store.subscrible
provider 的實現
provider用法如下,繫結之後,再經過connect處理,就可以在元件中通過props訪問對應資訊了。
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
// 繫結store
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
在看原始碼之前,我們先自行猜測一下。
前面也提到了Provider是React元件。
那麼為了讓子元件都能方便的訪問到store,store這個屬性會如何傳遞呢。props?context?
核心實現
import { Component, Children } from 'react'
export default class Provider extends Component {
// 宣告context 以被子元件獲取。
getChildContext() {
return { store: this.store }
}
constructor(props, context) {
super(props, context)
// 掛載store到Provider
this.store = props.store
}
render() {
// 判斷是否只有一個child,是則返回該child節點,否則拋錯
return Children.only(this.props.children)
}
}
Provider將store傳遞給子元件,具體如何和元件繫結就是conect做的事情了。
connect
connect連線元件和store,該操作並不修改原元件而是返回一個新的增強了關聯store的元件。
根據這個描述,這顯然就是個React高階元件(HOC)嗎。先看一下使用:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
接收四個引數,具體每個引數的作用詳細可以參考http://cn.redux.js.org/docs/react-redux/api.html
- [mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定義該引數,元件將會監聽 Redux store 的變化。任何時候,只要 Redux store 發生改變,mapStateToProps 函式就會被呼叫。該回撥函式必須返回一個純物件,這個物件會與元件的 props 合併。
- mapDispatchToProps(dispatch, [ownProps]): dispatchProps: 如果傳遞的是一個物件,那麼每個定義在該物件的函式都將被當作 Redux action creator,物件所定義的方法名將作為屬性名;每個方法將返回一個新的函式,函式中dispatch方法會將 action creator 的返回值作為引數執行。這些屬性會被合併到元件的 props 中。
- [mergeProps(stateProps, dispatchProps, ownProps): props] (Function): 如果指定了這個引數,mapStateToProps() 與 mapDispatchToProps() 的執行結果和元件自身的 props 將傳入到這個回撥函式中。該回撥函式返回的物件將作為 props 傳遞到被包裝的元件中。
- [options] (Object) 如果指定這個引數,可以定製 connector 的行為
- [pure = true] (Boolean): 如果為 true,connector 將執行 shouldComponentUpdate 並且淺對比 mergeProps 的結果,避免不必要的更新,預設true
- [withRef = false] (Boolean): 如果為 true,connector 會儲存一個對被被包含的元件例項的引用,該引用通過 getWrappedInstance() 方法獲得。預設false
結合下面的例子能更清晰知道作用是什麼。
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { VisibilityFilters } from '../actions'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.completed)
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
// 將store中的state作為props傳遞給被包裹元件
// mapStateToProps對應當前元件所需要的props,不過這個props顯然是要從store中抽取的,不是所有store都需要,所以只會取state.todos
const mapStateToProps = state => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
// 將action 與被包裹元件相繫結。
// 其實就是將action中的方法賦值到Props上,以便在元件中呼叫toggleTodo方法
const mapDispatchToProps = dispatch => ({
toggleTodo: id => dispatch(toggleTodo(id))
})
// 被包裹元件就對應TodoList
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
具體實現
connect實現比較複雜一點,返回的是個高階函式我們可以先看該函式實現了什麼。
connect函式
首先該方法接受相關引數,進行引數的判斷和相容處理(不指定使用預設)。
並返回一個 wrapWithConnect 方法來裝飾傳入的容器元件。
// 每個引數的預設實現
const defaultMapStateToProps = state => ({}) // eslint-disable-line no-unused-vars
const defaultMapDispatchToProps = dispatch => ({ dispatch })
const defaultMergeProps = (stateProps, dispatchProps, parentProps) => ({
...parentProps,
...stateProps,
...dispatchProps
})
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
// 需要store中的state才會去監聽
const shouldSubscribe = Boolean(mapStateToProps)
// 更新state 方法的相容,無mapStateToProps則使用預設
const mapState = mapStateToProps || defaultMapStateToProps
let mapDispatch
// action creater是否為 函式
if (typeof mapDispatchToProps === 'function') {
// 函式直接賦值
mapDispatch = mapDispatchToProps
} else if (!mapDispatchToProps) {
// 不存在,則使用預設方法
mapDispatch = defaultMapDispatchToProps
} else {
// 否則 將action Creater 包裝起來
mapDispatch = wrapActionCreators(mapDispatchToProps)
}
const finalMergeProps = mergeProps || defaultMergeProps
const { pure = true, withRef = false } = options
const checkMergedEquals = pure && finalMergeProps !== defaultMergeProps
function wrapWithConnect(WrappedComponent) {
const connectDisplayName = `Connect(${getDisplayName(WrappedComponent)})`
class Connect extends Component {/****/}
// ****
return hoistStatics(Connect, WrappedComponent)
}
wrapWithConnect 函式 返回內容
wrapWithConnect 函式接受一個元件(connect這就是個HOC。返回一個connect元件
// ****省略*****
// hoistStatics的作用:常用語高階元件中,將被包裹元素的靜態方法,“同步”到容器元素中。
// 也就是 connect中那些WrappedComponent屬性的mix
return hoistStatics(Connect, WrappedComponent)
這裡,就是HOC常見的增加功能的實現了。 也就是增強與redux的關聯,讓使用者只需要關注props,而非每次都要自己手動繫結。
connect元件生命週期
既然connect存在生命週期,那就順著生命週期看看
建構函式,就是獲取store中的state。
this.store 即Provider中掛載的Store
// 建構函式,獲取store中的state
constructor(props, context) {
super(props, context)
this.version = version
// props或者context中,這是provider中掛載的store
this.store = props.store || context.store
// 獲取state
const storeState = this.store.getState()
// 初始化state
this.state = { storeState }
this.clearCache()
}
shouldComponentUpdate
shouldComponentUpdate這裡會根據options裡面的引數來看是否 pure 選擇不同的更新策略
shouldComponentUpdate() {
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
}
componentDidMount
componentDidMount 根據前面的shouldSubscribe標識(mapStateToProps是否為true)決定是否增加監聽事件
componentDidMount() {
this.trySubscribe()
}
trySubscribe() {
// 存在監聽必要 並且沒有註冊過監聽事件
if (shouldSubscribe && !this.unsubscribe) {
// 業務元件中沒有使用的subscribe 在這裡實現,這也是HOC的用法之一,公共方法的抽離
// 註冊完成之後,this.unsubscribe為對一個unsubscribe回撥
this.unsubscribe = this.store.subscribe(this.handleChange.bind(this))
this.handleChange()
}
}
componentWillReceiveProps
componentWillReceiveProps 判斷是否更新 ,對於pure 元件 這裡就涉及到了shallowEqual。
通過shallowEqual的實現,我們可以得到Immtable的重要性
componentWillReceiveProps(nextProps) {
if (!pure || !shallowEqual(nextProps, this.props)) {
this.haveOwnPropsChanged = true
}
}
shallowEqual淺比較的實現
由此可以看到Immutable的重要性。對於引用型別的資料,只是比較了引用地址是否相同。
對於巢狀引用資料型別,只比較key的長度和value引用地址,並沒有進一步深入比較。導致巢狀結構並不適用。
export default function shallowEqual(objA, objB) {
// 引用地址是否相同
if (objA === objB) {
return true
}
const keysA = Object.keys(objA)
const keysB = Object.keys(objB)
// key長度是否相同
if (keysA.length !== keysB.length) {
return false
}
// 迴圈比較,vakue思否相同,對於巢狀引用型別,這種比較是不能滿足預期的。
const hasOwn = Object.prototype.hasOwnProperty
for (let i = 0; i < keysA.length; i++) {
if (!hasOwn.call(objB, keysA[i]) ||
objA[keysA[i]] !== objB[keysA[i]]) {
return false
}
}
return true
}
render
再下面是render,對於是否更新進行判斷,即是否更新傳遞給子元件的props
render的關注點在於 傳遞給WrappedComponent的props如何獲得。
// this.mergedProps 的計算
if (withRef) {
this.renderedElement = createElement(WrappedComponent, {
...this.mergedProps,
ref: 'wrappedInstance'
})
} else {
this.renderedElement = createElement(WrappedComponent,
this.mergedProps
)
}
計算this.mergedProps 最終傳遞下去的props是經過mapStateToProps,mapDispatchToProps計算之後,最後再由mergeProps計算之後的state。
// 簡化程式碼
this.mergedProps = nextMergedProps = computeMergedProps(this.stateProps, this.dispatchProps, this.props)
/**
* 獲得最終props 即經過引數中的
* @param {*} stateProps 經過mapStateToProps之後的結果
* @param {*} dispatchProps mapDispatchToProps之後的結果
* @param {*} parentProps 此處即為connect元件的props this.props
* @returns
*/
function computeMergedProps(stateProps, dispatchProps, parentProps) {
// finalMergeProps 即為引數中的mergeProps 或者 defaultMergeProps。 將前兩引數計算結果再進行處理。
const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
if (process.env.NODE_ENV !== 'production') {
checkStateShape(mergedProps, 'mergeProps')
}
return mergedProps
}
到這裡connect的作用也體現出來了:
- 根據引數決定監資料的變化
- 將store和action作為warpered的props傳入,一共元件使用store中的state和action
- 對於部分操作進行快取優化,提升執行效率
此時再回過頭去看上面的例子應該更清晰了。
結束語
參考文章
http://cn.redux.js.org/docs/react-redux/api.html
到這裡就結束了react-redux的原始碼解析,更多是自己的學習筆記吧。
使用一定程度之後再回頭看,可能對自己的理解更有幫助。
另外閱讀原始碼不是要盲目去讀,而是在應用之後帶著問題去讀。
這樣會更清晰如何去優化如何去提升。因為水平有限肯定有錯漏指出,歡迎指出。