react-redux 是什麼
react-redux
是 redux
官方 React
繫結庫。它幫助我們連線UI層和資料層。本文目的不是介紹 react-redux
的使用,而是要動手實現一個簡易的 react-redux
,希望能夠對你有所幫助。
首先思考一下,倘若不使用 react-redux
,我們的 react
專案中該如何結合 redux
進行開發呢。
每個需要與 redux
結合使用的元件,我們都需要做以下幾件事:
- 在元件中獲取
store
中的狀態 - 監聽
store
中狀態的改變,在狀態改變時,重新整理元件 - 在元件解除安裝時,移除對狀態變化的監聽。
如下:
import React from 'react';
import store from '../store';
import actions from '../store/actions/counter';
/**
* reducer 是 combineReducer({counter, ...})
* state 的結構為
* {
* counter: {number: 0},
* ....
* }
*/
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
number: store.getState().counter.number
}
}
componentDidMount() {
this.unsub = store.subscribe(() => {
if(this.state.number === store.getState().counter.number) {
return;
}
this.setState({
number: store.getState().counter.number
});
});
}
render() {
return (
<div>
<p>{`number: ${this.state.number}`}</p>
<button onClick={() => {store.dispatch(actions.add(2))}}>+</button>
<button onClick={() => {store.dispatch(actions.minus(2))}}>-</button>
<div>
)
}
componentWillUnmount() {
this.unsub();
}
}
如果我們的專案中有很多元件需要與 redux
結合使用,那麼這些元件都需要重複寫這些邏輯。顯然,我們需要想辦法複用這部分的邏輯,不然會顯得我們很蠢。我們知道,react
中高階元件可以實現邏輯的複用。
文中所用到的 Counter
程式碼在 https://github.com/YvetteLau/Blog
中的 myreact-redux/counter
中,建議先 clone
程式碼,當然啦,如果覺得本文不錯的話,給個star鼓勵。
邏輯複用
在 src
目錄下新建一個 react-redux
資料夾,後續的檔案都新建在此資料夾中。
建立 connect.js 檔案
檔案建立在 react-redux/components
資料夾下:
我們將重複的邏輯編寫 connect
中。
import React, { Component } from 'react';
import store from '../../store';
export default function connect (WrappedComponent) {
return class Connect extends Component {
constructor(props) {
super(props);
this.state = store.getState();
}
componentDidMount() {
this.unsub = store.subscribe(() => {
this.setState({
this.setState(store.getState());
});
});
}
componentWillUnmount() {
this.unsub();
}
render() {
return (
<WrappedComponent {...this.state} {...this.props}/>
)
}
}
}
有個小小的問題,儘管這邏輯是重複的,但是每個元件需要的資料是不一樣的,不應該把所有的狀態都傳遞給元件,因此我們希望在呼叫 connect
時,能夠將需要的狀態內容告知 connect
。另外,元件中可能還需要修改狀態,那麼也要告訴 connect
,它需要派發哪些動作,否則 connect
無法知道該繫結那些動作給你。
為此,我們新增兩個引數:mapStateToProps
和 mapDispatchToProps
,這兩個引數負責告訴 connect
元件需要的 state
內容和將要派發的動作。
mapStateToProps 和 mapDispatchToProps
我們知道 mapStateToProps
和 mapDispatchToProps
的作用是什麼,但是目前為止,我們還不清楚,這兩個引數應該是一個什麼樣的格式傳遞給 connect
去使用。
import { connect } from 'react-redux';
....
//connect 的使用
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
-
mapStateToProps 告訴
connect
,元件需要繫結的狀態。mapStateToProps
需要從整個狀態中挑選元件需要的狀態,但是在呼叫connect
時,我們並不能獲取到store
,不過connect
內部是可以獲取到store
的,為此,我們將mapStateToProps
定義為一個函式,在connect
內部呼叫它,將store
中的state
傳遞給它,然後將函式返回的結果作為屬性傳遞給元件。元件中通過this.props.XXX
來獲取。因此,mapStateToProps
的格式應該類似下面這樣://將 store.getState() 傳遞給 mapStateToProps mapStateToProps = state => ({ number: state.counter.number });
-
mapDispatchToProps 告訴
connect
,元件需要繫結的動作。回想一下,元件中派發動作:
store.dispatch({actions.add(2)})
。connect
包裝之後,我們仍要能派發動作,肯定是this.props.XXX()
這樣的一種格式。比如,計數器的增加,呼叫
this.props.add(2)
,就是需要派發store.dispatch({actions.add(2)})
,因此add
屬性,對應的內容就是(num) => { store.dispatch({actions.add(num)}) }
。傳遞給元件的屬性類似下面這樣:{ add: (num) => { store.dispatch(actions.add(num)) }, minus: (num) => { store.dispatch(actions.minus(num)) } }
和
mapStateToProps
一樣,在呼叫connect
時,我們並不能獲取到store.dispatch
,因此我們也需要將mapDispatchToProps
設計為一個函式,在connect
內部呼叫,這樣可以將store.dispatch
傳遞給它。所以,mapStateToProps
應該是下面這樣的格式://將 store.dispacth 傳遞給 mapDispatchToProps mapDispatchToProps = (dispatch) => ({ add: (num) => { dispatch(actions.add(num)) }, minus: (num) => { dispatch(actions.minus(num)) } })
至此,我們已經搞清楚 mapStateToProps
和 mapDispatchToProps
的格式,是時候進一步改進 connect
了。
connect 1.0 版本
import React, { Component } from 'react';
import store from '../../store';
export default function connect (mapStateToProps, mapDispatchToProps) {
return function wrapWithConnect (WrappedComponent) {
return class Connect extends Component {
constructor(props) {
super(props);
this.state = mapStateToProps(store.getState());
this.mappedDispatch = mapDispatchToProps(store.dispatch);
}
componentDidMount() {
this.unsub = store.subscribe(() => {
const mappedState = mapStateToProps(store.getState());
//TODO 做一層淺比較,如果狀態沒有改變,則不setState
this.setState(mappedState);
});
}
componentWillUnmount() {
this.unsub();
}
render() {
return (
<WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
)
}
}
}
}
我們知道,connect
是作為 react-redux
庫的方法提供的,因此我們不可能直接在 connect.js
中去匯入 store
,這個 store
應該由使用 react-redux
的應用傳入。react
中資料傳遞有兩種:通過屬性 props
或者是通過上下文物件 context
,通過 connect
包裝的元件在應用中分佈,而 context
設計目的是為了共享那些對於一個元件樹而言是“全域性”的資料。
我們需要把 store
放在 context
上,這樣根元件下的所有子孫元件都可以獲取到 store
。這部分內容,我們當然可以自己在應用中編寫相應程式碼,不過很顯然,這些程式碼在每個應用中都是重複的。因此我們把這部分內容也封裝在 react-redux
內部。
此處,我們使用舊的 Context API
來寫(鑑於我們實現的 react-redux 4.x 分支的程式碼,因此我們使用舊版的 context API)。
Provider
我們需要提供一個 Provider
元件,它的功能就是接收應用傳遞過來的 store
,將其掛在 context
上,這樣它的子孫元件就都可以通過上下文物件獲取到 store
。
新建 Provider.js 檔案
檔案建立在 react-redux/components
資料夾下:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Provider extends Component {
static childContextTypes = {
store: PropTypes.shape({
subscribe: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
getState: PropTypes.func.isRequired
}).isRequired
}
constructor(props) {
super(props);
this.store = props.store;
}
getChildContext() {
return {
store: this.store
}
}
render() {
/**
* 早前返回的是 return Children.only(this.props.children)
* 導致Provider只能包裹一個子元件,後來取消了此限制
* 因此此處,我們直接返回 this.props.children
*/
return this.props.children
}
}
新建一個 index.js 檔案
檔案建立在 react-redux
目錄下:
此檔案只做一件事,即將 connect
和 Provider
匯出
import connect from './components/connect';
import Provider from './components/Provider';
export {
connect,
Provider
}
Provider 的使用
使用時,我們只需要引入 Provider
,將 store
傳遞給 Provider
。
import React, { Component } from 'react';
import { Provider } from '../react-redux';
import store from './store';
import Counter from './Counter';
export default class App extends Component {
render() {
return (
<Provider store={store}>
<Counter />
</Provider>
)
}
}
至此,Provider
的原始碼和使用已經說明清楚了,不過相應的 connect
也需要做一些修改,為了通用性,我們需要從 context
上去獲取 store
,取代之前的匯入。
connect 2.0 版本
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default function connect(mapStateToProps, mapDispatchToProps) {
return function wrapWithConnect(WrappedComponent) {
return class Connect extends Component {
//PropTypes.shape 這部分程式碼與 Provider 中重複,因此後面我們可以提取出來
static contextTypes = {
store: PropTypes.shape({
subscribe: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
getState: PropTypes.func.isRequired
}).isRequired
}
constructor(props, context) {
super(props, context);
this.store = context.store;
//原始碼中是將 store.getState() 給了 this.state
this.state = mapStateToProps(this.store.getState());
this.mappedDispatch = mapDispatchToProps(this.store.dispatch);
}
componentDidMount() {
this.unsub = this.store.subscribe(() => {
const mappedState = mapStateToProps(this.store.getState());
//TODO 做一層淺比較,如果狀態沒有改變,則無需 setState
this.setState(mappedState);
});
}
componentWillUnmount() {
this.unsub();
}
render() {
return (
<WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
)
}
}
}
}
使用 connect
關聯 Counter
與 store
中的資料。
import React, { Component } from 'react';
import { connect } from '../react-redux';
import actions from '../store/actions/counter';
class Counter extends Component {
render() {
return (
<div>
<p>{`number: ${this.props.number}`}</p>
<button onClick={() => { this.props.add(2) }}>+</button>
<button onClick={() => { this.props.minus(2) }}>-</button>
</div>
)
}
}
const mapStateToProps = state => ({
number: state.counter.number
});
const mapDispatchToProps = (dispatch) => ({
add: (num) => {
dispatch(actions.add(num))
},
minus: (num) => {
dispatch(actions.minus(num))
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
store/actions/counter.js 定義如下:
import { INCREMENT, DECREMENT } from '../action-types';
const counter = {
add(number) {
return {
type: INCREMENT,
number
}
},
minus(number) {
return {
type: DECREMENT,
number
}
}
}
export default counter;
至此,我們的 react-redux
庫已經可以使用了,不過很有很多細節問題待處理:
-
mapDispatchToProps
的定義寫起來有點麻煩,不夠簡潔
大家是否還記得redux
中的bindActionCreators
,藉助於此方法,我們可以允許傳遞actionCreator
給connect
,然後在connect
內部進行轉換。 -
connect
和Provider
中的store
的PropType
規則可以提取出來,避免程式碼的冗餘 -
mapStateToProps
和mapDispatchToProps
可以提供預設值mapStateToProps
預設值為state => ({})
; 不關聯state
;mapDispatchToProps
的預設值為dispatch => ({dispatch})
,將store.dispatch
方法作為屬性傳遞給被包裝的屬性。 - 目前,我們僅傳遞了
store.getState()
給mapStateToProps
,但是很可能在篩選過濾需要的state
時,需要依據元件自身的屬性進行處理,因此,可以將元件自身的屬性也傳遞給mapStateToProps
,同樣的原因,也將自身屬性傳遞給mapDispatchToProps
。
connect 3.0 版本
我們將 store
的 PropType 規則提取出來,放在 utils/storeShape.js
檔案中。
淺比較的程式碼放在 utils/shallowEqual.js
檔案中,通用的淺比較函式,此處不列出,有興趣可以直接閱讀下程式碼。
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/**
* mapStateToProps 預設不關聯state
* mapDispatchToProps 預設值為 dispatch => ({dispatch}),將 `store.dispatch` 方法作為屬性傳遞給元件
*/
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });
export default function connect(mapStateToProps, mapDispatchToProps) {
if(!mapStateToProps) {
mapStateToProps = defaultMapStateToProps;
}
if (!mapDispatchToProps) {
//當 mapDispatchToProps 為 null/undefined/false...時,使用預設值
mapDispatchToProps = defaultMapDispatchToProps;
}
return function wrapWithConnect(WrappedComponent) {
return class Connect extends Component {
static contextTypes = {
store: storeShape
};
constructor(props, context) {
super(props, context);
this.store = context.store;
//原始碼中是將 store.getState() 給了 this.state
this.state = mapStateToProps(this.store.getState(), this.props);
if (typeof mapDispatchToProps === 'function') {
this.mappedDispatch = mapDispatchToProps(this.store.dispatch, this.props);
} else {
//傳遞了一個 actionCreator 物件過來
this.mappedDispatch = bindActionCreators(mapDispatchToProps, this.store.dispatch);
}
}
componentDidMount() {
this.unsub = this.store.subscribe(() => {
const mappedState = mapStateToProps(this.store.getState(), this.props);
if (shallowEqual(this.state, mappedState)) {
return;
}
this.setState(mappedState);
});
}
componentWillUnmount() {
this.unsub();
}
render() {
return (
<WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
)
}
}
}
}
現在,我們的 connect
允許 mapDispatchToProps
是一個函式或者是 actionCreators
物件,在 mapStateToProps
和 mapDispatchToProps
預設或者是 null
時,也能表現良好。
不過還有一個問題,connect
返回的所有元件名都是 Connect
,不便於除錯。因此我們可以為其新增 displayName
。
connect 4.0 版本
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/**
* mapStateToProps 預設時,不關聯state
* mapDispatchToProps 預設時,設定其預設值為 dispatch => ({dispatch}),將`store.dispatch` 方法作為屬性傳遞給元件
*/
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
export default function connect(mapStateToProps, mapDispatchToProps) {
if(!mapStateToProps) {
mapStateToProps = defaultMapStateToProps;
}
if(!mapDispatchToProps) {
//當 mapDispatchToProps 為 null/undefined/false...時,使用預設值
mapDispatchToProps = defaultMapDispatchToProps;
}
return function wrapWithConnect (WrappedComponent) {
return class Connect extends Component {
static contextTypes = storeShape;
static displayName = `Connect(${getDisplayName(WrappedComponent)})`;
constructor(props) {
super(props);
//原始碼中是將 store.getState() 給了 this.state
this.state = mapStateToProps(store.getState(), this.props);
if(typeof mapDispatchToProps === 'function') {
this.mappedDispatch = mapDispatchToProps(store.dispatch, this.props);
}else{
//傳遞了一個 actionCreator 物件過來
this.mappedDispatch = bindActionCreators(mapDispatchToProps, store.dispatch);
}
}
componentDidMount() {
this.unsub = store.subscribe(() => {
const mappedState = mapStateToProps(store.getState(), this.props);
if(shallowEqual(this.state, mappedState)) {
return;
}
this.setState(mappedState);
});
}
componentWillUnmount() {
this.unsub();
}
render() {
return (
<WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} />
)
}
}
}
}
至此,react-redux
我們就基本實現了,不過這個程式碼並不完善,比如,ref
丟失的問題,元件的 props
變化時,重新計算 this.state
和 this.mappedDispatch
,沒有進一步進行效能優化等。你可以在此基礎上進一步進行處理。
react-redux
主幹分支的程式碼已經使用 hooks
改寫,後期如果有時間,會輸出一篇新版本的程式碼解析。
最後,使用我們自己編寫的 react-redux
和 redux
編寫了 Todo
的demo,功能正常,程式碼在 在 https://github.com/YvetteLau/Blog
中的 myreact-redux/todo
下。
附上新老 context API
的使用方法:
context
目前有兩個版本的 context API
,舊的 API 將會在所有 16.x 版本中得到支援,但是未來版本中會被移除。
context API(新)
const MyContext = React.createContext(defaultValue);
建立一個 Context
物件。當 React
渲染一個訂閱了這個 Context
物件的元件,這個元件會從元件樹中離自身最近的那個匹配的 Provider
中讀取到當前的 context
值。
注意:只有當元件所處的樹中沒有匹配到 Provider
時,其 defaultValue
引數才會生效。
使用
Context.js
首先建立 Context 物件
import React from 'react';
const MyContext = React.createContext(null);
export default MyContext;
根元件( Pannel.js )
- 將需要共享的內容,設定在
<MyContext.Provider>
的value
中(即 context 值) - 子元件被
<MyContext.Provider>
包裹
import React from 'react';
import MyContext from './Context';
import Content from './Content';
class Pannel extends React.Component {
state = {
theme: {
color: 'rgb(0, 51, 254)'
}
}
render() {
return (
// 屬性名必須叫 value
<MyContext.Provider value={this.state.theme}>
<Content />
</MyContext.Provider>
)
}
}
子孫元件( Content.js )
類元件
- 定義
Class.contextType
:static contextType = ThemeContext
; - 通過
this.context
獲取<ThemeContext.Provider>
中value
的內容(即context
值)
//類元件
import React from 'react';
import ThemeContext from './Context';
class Content extends React.Component {
//定義了 contextType 之後,就可以通過 this.context 獲取 ThemeContext.Provider value 中的內容
static contextType = ThemeContext;
render() {
return (
<div style={{color: `2px solid ${this.context.color}`}}>
//....
</div>
)
}
}
函式元件
- 子元素包裹在
<ThemeContext.Consumer>
中 -
<ThemeContext.Consumer>
的子元素是一個函式,入參context
值(Provider
提供的value
)。此處是{color: XXX}
import React from 'react';
import ThemeContext from './Context';
export default function Content() {
return (
<ThemeContext.Consumer>
{
context => (
<div style={{color: `2px solid ${context.color}`}}>
//....
</div>
)
}
</ThemeContext.Consumer>
)
}
context API(舊)
使用
- 定義根元件的
childContextTypes
(驗證getChildContext
返回的型別) - 定義
getChildContext
方法
根元件( Pannel.js )
import React from 'react';
import PropTypes from 'prop-types';
import Content from './Content';
class Pannel extends React.Component {
static childContextTypes = {
theme: PropTypes.object
}
getChildContext() {
return { theme: this.state.theme }
}
state = {
theme: {
color: 'rgb(0, 51, 254)'
}
}
render() {
return (
// 屬性名必須叫 value
<>
<Content />
</>
)
}
}
子孫元件( Content.js )
- 定義子孫元件的
contextTypes
(宣告和驗證需要獲取的狀態的型別) - 通過 this.context 即可以獲取傳遞過來的上下文內容。
import React from 'react';
import PropTypes from 'prop-types';
class Content extends React.Component {
static contextTypes = PropTypes.object;
render() {
return (
<div style={{color: `2px solid ${this.context.color}`}}>
//....
</div>
)
}
}
參考連結:
- react-redux 原始碼:https://github.com/reduxjs/re...
- 【庖丁解牛React-Redux(二): connect】https://juejin.im/post/59772a...
- 【一起學習造輪子(三):從零開始寫一個React-Redux】https://juejin.im/post/5b32f1...