示例程式碼請點這裡
上一次我們簡單瞭解了一下 redux(文章在這裡),今天我們來結合 React,實現自己的 React-redux。
一、建立專案
我們用 create-react-app 建立一個新專案,刪除 src 下的冗餘部分,新增自己的檔案,如下:
# 修改後的目錄結構
++ src
++++ component
++++++ Head
-------- Head.js
++++++ Body
-------- Body.js
++++++ Button
-------- Button.js
---- App.js
---- index.css
---- index.js
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
// App.js
import React, { Component } from 'react';
import Head from './component/Head/Head';
import Body from './component/Body/Body';
export default class App extends Component {
render() {
return (
<div className="App">
<Head />
<Body />
</div>
);
}
}
# Head.js
import React, { Component } from 'react';
export default class Head extends Component {
render() {
return (
<div className="head">Head</div>
);
}
}
# Body.js
import React, { Component } from 'react';
import Button from '../Button/Button';
export default class Body extends Component {
render() {
return (
<div>
<div className="body">Body</div>
<Button />
</div>
);
}
}
# Button.js
import React, { Component } from 'react';
export default class Button extends Component {
render() {
return (
<div className="button">
<div className="btn">改變 head</div>
<div className="btn">改變 body</div>
</div>
);
}
}
複製程式碼
以上程式碼並不複雜,我們再來給他們寫點樣式,最後看下效果:
我們看到,現在 head ,和 body 內的文案都是我們寫死的,這樣並不利於我們的開發,因為這些值我們無法改變,現在我們想點選下邊按鈕的時候,改變相應的文案,以現在的程式碼我們是無法實現的。
當然,我們可以通過一系列 props 的傳遞,來達到我們的目的,可是,那樣會相當繁瑣,因為不僅涉及到父子元件的值傳遞,還有和兄弟元件的子元件之間的值傳遞。
此時,我們需要一個全域性共享的 store ,讓我們可以在任何地方都能輕鬆的訪問,可以十分便捷的完成資料的獲取和修改。
二、context
在 React 中,為我們提供了 context 這個 API 來解決這樣的巢狀場景(context具體介紹在這裡,在 React 16.3 以上的版本,context 已經有了更新,具體請看這裡)。
context 為我們提供了一個全域性共享的狀態,在任何後代元件中,都可以很輕鬆的訪問頂級元件的 store。
我們這樣修改我們的程式碼:
# App.js
import PropTypes from 'prop-types';
...
export default class App extends Component {
static childContextTypes = {
store: PropTypes.object
}
getChildContext () {
const state = {
head: '我是全域性 head',
body: '我是全域性 body',
headBtn: '修改 head',
bodyBtn: '修改 body'
}
return { store: state };
}
render() {
...
}
}
# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Head extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
this._upState();
}
_upState(){
const { store } = this.context;
this.setState({
...store
})
}
render() {
return (
<div className="head">{this.state.head}</div>
);
}
}
# body.js
import PropTypes from 'prop-types';
...
export default class Body extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
this._upState();
}
_upState(){
const { store } = this.context;
this.setState({
...store
})
}
render() {
return (
<div>
<div className="body">{this.state.body}</div>
<Button />
</div>
);
}
}
# Button.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Button extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
this._upState();
}
_upState(){
const { store } = this.context;
this.setState({
...store
})
}
render() {
return (
<div className="button">
<div className="btn">{this.state.headBtn}</div>
<div className="btn">{this.state.bodyBtn}</div>
</div>
);
}
}
複製程式碼
檢視頁面,我們可以看到,在頂層元件中的全域性 store 已經被各個後代元件訪問到:
我們再來梳理下使用 context 的步驟:1、在頂層元件中通過 childContextTypes 規定資料型別。
2、在頂層元件中通過 getChildContext 設定資料。
3、在後代元件中通過 contextTypes 規定資料型別。
4、在後代元件中通過 context 引數獲取資料。
通過以上步驟,我們建立了一個全域性共享的 store 。你可能會有疑問,為什麼在後代元件中我們定義了 _upState 方法,而沒有把內容直接寫在生命週期中,這個問題先不回答,在下面,你將會看到為什麼。現在,我們來把這個 store 和我們之前寫的 redux 進行結合(有關 redux 的部分,請看上一篇文章,?這裡 。
三、React-redux
我們來新建 redux 資料夾,完成我們的 redux(關於以下程式碼含義,請看上一篇文章):
# index.js
export * from './createStore';
export * from './storeChange';
# createStore.js
export const createStore = (state, storeChange) => {
const listeners = [];
let store = state || {};
const subscribe = (listen) => listeners.push(listen);
const dispatch = (action) => {
const newStore = storeChange(store, action);
store = newStore;
listeners.forEach(item => item())
};
const getStore = () => {
return store;
}
return { store, dispatch, subscribe, getStore }
}
# storeChange.js
export const storeChange = (store, action) => {
switch (action.type) {
case 'HEAD':
return {
...store,
head: action.head
}
case 'BODY':
return {
...store,
body: action.body
}
default:
return { ...store }
}
}
複製程式碼
通過以上程式碼,我們完成了 redux ,其中 createStore.js 的程式碼,幾乎完全和上一篇內容相同,只是略作了修改,有興趣的朋友可以自己看下。現在我們來和 context 結合:
# App.js
...
import { createStore, storeChange } from './redux';
export default class App extends Component {
static childContextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
getChildContext () {
const state = {
head: '我是全域性 head',
body: '我是全域性 body',
headBtn: '修改 head',
bodyBtn: '修改 body'
}
const { store, dispatch, subscribe, getStore } = createStore(state,storeChange)
return { store, dispatch, subscribe, getStore };
}
render() {
...
}
}
# Head.js
...
export default class Head extends Component {
static contextTypes = {
store: PropTypes.object,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
...
componentWillMount(){
const { subscribe } = this.context;
this._upState();
subscribe(() => this._upState())
}
_upState(){
const { getStore } = this.context;
this.setState({
...getStore()
})
}
render() {
...
}
}
# Body.js
...
export default class Body extends Component {
static contextTypes = {
// 和 Head.js 相同
}
...
componentWillMount(){
// 和 Head.js 相同
}
_upState(){
// 和 Head.js 相同
}
render() {
return (
<div>
<div className="body">{this.state.body}</div>
<Button />
</div>
);
}
}
# Button.js
...
export default class Button extends Component {
static contextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
// 和 Head.js 相同
}
_upState(){
// 和 Head.js 相同
}
render() {
...
}
}
複製程式碼
以上程式碼,我們用 createStore 方法,建立出全域性的 store。並且把 store、 dispatch、subscribe 通過 context傳遞, 讓各個後代元件可以輕易的獲取到這些全域性的屬性。最後我們用 setState 來改變各個後代元件的 state ,並給 subscribe 中新增了監聽函式,當 store 發生改變時,讓元件重新獲取到 store, 重新渲染。在這裡,我們看到了 _upState 的用處,它讓我們很方便的新增 store 改變後的回撥。
觀察頁面,我們發現頁面並沒有異常,在後代頁面依舊可以訪問到 context。這樣,是不是說明我們結合成功了呢?先別急,讓我們來改變下資料試一下。我們修改 Button.js 給按鍵新增點選事件,來改變 store :
# Button.js
...
changeContext(type){
const { dispatch } = this.context;
dispatch({
type: type,
head: '我是修改後的資料'
});
}
render() {
return (
<div className="button">
<div className="btn" onClick={() => this.changeContext('HEAD')}>{this.state.headBtn}</div>
<div className="btn" onClick={() => this.changeContext('BODY')}>{this.state.bodyBtn}</div>
</div>
);
}
複製程式碼
點選按鍵,我們看到:
資料成功重新整理。 至此,我們已經成功的將自己的 redux 和 react 結合了起來。四、優化
1、connect
雖然我們實現了 redux 和 react 的結合,但是我們看到,上面的程式碼是有很多問題的,比如:
1)有大量的重複邏輯
在各個後代元件中,我們都是在 context 中獲取 store ,然後更新各自的 state ,還同樣的新增了監聽事件。
2)程式碼幾乎不可複用
在各個後代元件中,對 context 的依賴過強。假設你的同事想用下 Body 元件,可是他的程式碼中並沒有設定 context 那麼 Body 元件就是不可用的。
關於這些問題,我們可以通過高階元件來解決(關於高階元件的問題,大家請點這裡或者這裡),我們可以把重複的程式碼邏輯,封裝起來,我們給這個封裝好的方法起個名字叫 connect 。 這只是一個名字而已,大家不必糾結,如果你願意,你完全可以管它叫做 aaa。
我們在 redux 資料夾下新建一個 connect 檔案:
# connect.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export const connect = (Comp) => {
class Connect extends Component {
render(){
return (
<div className="connect">
<Comp />
</div>
);
}
}
return Connect;
}
複製程式碼
我們看到,connect 是一個高階元件,它接收一個元件,然後返回處理後的元件。我們 Head 元件來驗證一下這個高階元件是否可用:
# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from '../../redux';
class Head extends Component {
...
}
export default connect(Head);
複製程式碼
重新整理頁面我們可以知道,connect 正在發揮它應有的功能,已經成功的在 Head 元件外層套了一層 div:
由此,我們是不是可以讓 connect 做更多的事,比如,把有關 context 的東西都交給它,我們試著這樣改造 connect 和 Head:# connect.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export const connect = (Comp) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
constructor (props) {
super(props)
this.state = {};
}
componentWillMount(){
const { subscribe } = this.context;
this._upState();
subscribe(() => this._upState())
}
_upState(){
const { getStore } = this.context;
this.setState({
...getStore()
})
}
render(){
return (
<div className="connect">
<Comp {...this.state} />
</div>
);
}
}
return Connect;
}
# Head.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from '../../redux';
class Head extends Component {
render() {
return (
<div className="head">{this.props.head}</div> // 從 props 中取值
);
}
}
export default connect(Head);
複製程式碼
我們看到,改造後的 Head 元件變得非常精簡,我們只需要關心具體的業務邏輯,而任何於 context 有關的操作都被轉移到了 connect 中去。我們按照同樣的方式改造 Body 和 Button 元件:
# Body.js
...
class Body extends Component {
render() {
return (
<div>
<div className="body">{this.props.body}</div>
<Button />
</div>
);
}
}
export default connect(Body)
# Button.js
...
class Button extends Component {
changeContext(type, value){
const { dispatch } = this.context; // context 已經不存在了
dispatch({
type: type,
head: value
});
}
render() {
return (
<div className="button">
<div className="btn" onClick={() => this.changeContext('HEAD', '我是改變的資料1')}>{this.props.headBtn}</div>
<div className="btn" onClick={() => this.changeContext('HEAD', '我是改變的資料2')}>{this.props.bodyBtn}</div>
</div>
);
}
}
export default connect(Button)
複製程式碼
重新整理頁面,並沒有什麼問題,一切似乎都很美好,可是當我們點選按鍵時,錯誤降臨。 我們發現,在 Button 中,dispatch 是無法獲取到的,我們現在唯一的資料來源都是通過 props ,而在 connect 中,我們並沒有處理 dispatch ,那麼,我們繼續改造我們的 connect:
# Button.js
...
const { dispatch } = this.props; // 從 props 中取值
...
# connect.js
...
export const connect = (Comp) => {
class Connect extends Component {
...
constructor (props) {
super(props)
this.state = {
dispatch: () => {}
};
}
componentWillMount(){
const { subscribe, dispatch } = this.context; // 取出 dispatch
this.setState({
dispatch
})
this._upState();
subscribe(() => this._upState())
}
...
}
return Connect;
}
複製程式碼
現在看來,一切似乎都已經解決。讓我們再來一起回顧下我們究竟做了什麼:
1)我們封裝了 connect ,把所有有關的 connect 的操作都交給他來負責。
2)我們改造了後代元件,讓它們從 props 中來獲取資料,不再依賴 context。
現在,再來對照之前我們提出的問題,發現,我們已經很好的解決了它們。
可是,這樣真的就可以了嗎?
我們再來觀察 connect 中的程式碼,我們發現,所有的 PropTypes 都是我們固定寫死的,缺乏靈活性,也不太利於我們開發,畢竟,每個元件所要獲取的資料都不盡相同,如果能讓 connect 再接收一個引數,來規定 PropTypes 那再好不過了。
根據這個需求,我們來繼續改造我們的程式碼:
# connect.js
...
export const connect = (Comp, propsType) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func,
...propsType
}
...
}
return Connect;
}
# Head.js
...
const propsType = {
store: PropTypes.object,
}
export default connect(Head, propsType);
複製程式碼
以上,我們重新改造了 connect ,讓他接收兩個引數,把一些固定要傳遞的屬性,我們可以寫死,然後再新增進我們在每個元件內部單獨定義的 propsType。
2、Provider
我們看到,在所有的後代元件中,已經分離出了有關 context 的操作,但是,在 App.js 中,依舊還有和 context 相關的內容。其實,在 App 中用到 context 只是為了把 store 存放進去,好讓後代元件可以從中獲取資料。那麼,我們完全可以通過容器元件來進行狀態提升,把這部分髒活從 App 元件中分離出來,提升到新建的容器元件中。我們只需要給他傳入需要存放進 context 的 store 就可以了。
依據之前的想法,我們在 redux 資料夾下新建一個 Provider,並把所有和業務無關的程式碼從 App 中取出:
# Provider
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { createStore, storeChange } from '../redux';
export class Provider extends Component {
static childContextTypes = {
store: PropTypes.object,
dispatch: PropTypes.func,
subscribe: PropTypes.func,
getStore: PropTypes.func
}
getChildContext () {
const state = this.props.store;
const { store, dispatch, subscribe, getStore } = createStore(state,storeChange)
return { store, dispatch, subscribe, getStore };
}
render(){
return (
<div className="provider">{this.props.children}</div>
);
}
}
# App.js
...
export default class App extends Component {
render() {
return (
<div className="App">
<Head />
<Body />
</div>
);
}
}
# index.js
...
import { Provider } from './redux'
const state = {
head: '我是全域性 head',
body: '我是全域性 body',
headBtn: '修改 head',
bodyBtn: '修改 body'
}
ReactDOM.render(
<Provider store={state}>
<App />
</Provider>,
document.getElementById('root')
);
複製程式碼
經過改造的 App 元件也變得非常清爽。
我們在 index.js 中定義了全域性 store ,通過容器元件 Provider 塞入 context 中,讓所有的後代元件都可以輕鬆獲取到,而在 App 元件中,我們只需要關注具體的業務邏輯就好。
最後的話
本文通過一些簡單的程式碼示例,完成了一個自己的 react-redux ,當然,以上程式碼還過於簡陋,存在很多問題,和我們常用的 react-redux 庫也有些許區別,我們重點在於瞭解它們內部的一些原理。
如有描述不正確的地方,歡迎大家指正!