React中元件間通訊的方式

WindrunnerMax發表於2021-01-30

React中元件間通訊的方式

React中元件間通訊包括父子元件、兄弟元件、隔代元件、非巢狀元件之間通訊。

Props

props適用於父子元件的通訊,props以單向資料流的形式可以很好的完成父子元件的通訊,所謂單向資料流,就是資料只能通過props由父元件流向子元件,而子元件並不能通過修改props傳過來的資料修改父元件的相應狀態,所有的props都使得其父子props之間形成了一個單向下行繫結,父級props的更新會向下流動到子元件中,但是反過來則不行,這樣會防止從子元件意外改變父級元件的狀態,導致難以理解資料的流向而提高了專案維護難度。實際上如果傳入一個基本資料型別給子元件,在子元件中修改這個值的話React中會丟擲異常,如果對於子元件傳入一個引用型別的物件的話,在子元件中修改是不會出現任何提示的,但這兩種情況都屬於改變了父子元件的單向資料流,是不符合可維護的設計方式的。
我們通常會有需要更改父元件值的需求,對此我們可以在父元件自定義一個處理接受變化狀態的邏輯,然後在子元件中如若相關的狀態改變時,就觸發父元件的邏輯處理事件,在Reactprops是能夠接受任意的入參,此時我們通過props傳遞一個函式在子元件觸發並且傳遞值到父元件的例項去修改父元件的state

<!-- 子元件 -->
import React from "react";

class Child extends React.PureComponent{
    render() {
        return (
            <>
                <div>接收父元件的值: {this.props.msg}</div>
                <button onClick={() => this.props.changeMsg("Changed Msg")}>修改父元件的值</button>
            </>
        )
    }
}

export default Child;
<!-- 父元件 -->
import React from "react";
import Child from "./child";

class Parent extends React.PureComponent{

    constructor(props){
        super(props);
        this.state = { msg: "Parent Msg" };
    }

    changeMsg = (msg) => {
        this.setState({ msg });
    }

    render() {
        return (
            <div>
                <Child msg={this.state.msg} changeMsg={this.changeMsg} />
            </div>
        )
    }
}

export default Parent;

Context

React Context適用於父子元件以及隔代元件通訊,React Context提供了一個無需為每層元件手動新增props就能在元件樹間進行資料傳遞的方法。在React應用中資料是通過props屬性自上而下即由父及子進行傳遞的,但這種做法對於某些型別的屬性而言是極其繁瑣的,這些屬性是應用程式中許多元件都需要的,Context提供了一種在元件之間共享此類值的方式,而不必顯式地通過元件樹的逐層傳遞props,實際上React-Router就是使用這種方式傳遞資料,這也解釋了為什麼<Router>要在所有<Route>的外面。。
使用Context是為了共享那些對於一個元件樹而言是全域性的資料,簡單來說就是在父元件中通過Provider來提供資料,然後在子元件中通過Consumer來取得Provider定義的資料,不論子元件有多深,只要使用了Provider那麼就可以取得在Provider中提供的資料,而不是侷限於只能從當前父元件的props屬性來獲取資料,只要在父元件內定義的Provider資料,子元件都可以呼叫。當然如果只是想避免層層傳遞props且傳遞的層數不多的情況下,可以考慮將props進行一個淺拷貝之後將之後元件中不再使用的props刪除後利用Spread操作符即{...handledProps}將其展開進行傳遞,實現類似於Vue$attrs$listenersAPI操作。

import React from "react";

const createNamedContext = name => {
  const context = React.createContext();
  context.Provider.displayName = `${name}.Provider`;
  context.Consumer.displayName = `${name}.Consumer`;
  return context;
}

const context = /*#__PURE__*/ createNamedContext("Share");

export default context;
<!-- 子元件 -->
import React from "react";
import ShareContext from "./ShareContext";

class Child extends React.PureComponent{
    render() {
        return (
            <>
                <ShareContext.Consumer>
                    { /* 基於 context 值進行渲染 */ }
                    {
                        value => <div>SharedValue: {value}</div>
                    }
                </ShareContext.Consumer>
            </>
        )
    }
}

export default Child;
<!-- 父元件 -->
import React from "react";
import Child from "./child";
import ShareContext from "./ShareContext";

class Parent extends React.PureComponent{

    constructor(props){
        super(props);
        this.state = { msg: "Parent Msg" };
    }

    render() {
        return (
            <div>
                <ShareContext.Provider
                    value={100}
                >
                    <Child msg={this.state.msg} />
                </ShareContext.Provider>
            </div>
        )
    }
}

export default Parent;

Refs

Refs適用於父子元件的通訊,Refs提供了一種方式,允許我們訪問DOM節點或在render方法中建立的React元素,在典型的React資料流中,props是父元件與子元件互動的唯一方式,要修改一個子元件,你需要使用新的props來重新渲染它,但是在某些情況下,需要在典型資料流之外強制修改子元件,被修改的子元件可能是一個React元件的例項,也可能是一個DOM元素,渲染元件時返回的是元件例項,而渲染DOM元素時返回是具體的DOM節點,React提供的這個ref屬性,表示為對元件真正例項的引用,其實就是ReactDOM.render()返回的元件例項。此外需要注意避免使用refs來做任何可以通過宣告式實現來完成的事情,通常在可以使用propsstate的情況下勿依賴refs

<!-- 子元件 -->
import React from "react";

class Child extends React.PureComponent{

    render() {
        return (
            <>
                <div>接收父元件的值: {this.props.msg}</div>
            </>
        )
    }
}

export default Child;
<!-- 父元件 -->
import React from "react";
import Child from "./child";

class Parent extends React.PureComponent{

    constructor(props){
        super(props);
        this.state = { msg: "Parent Msg" };
        this.child = React.createRef();
    }

    componentDidMount(){
        console.log(this.child.current); // Child {props: {…}, context: {…}, ...}
    }

    render() {
        return (
            <div>
                <Child msg={this.state.msg} ref={this.child} />
            </div>
        )
    }
}

export default Parent;

EventBus

EventBus可以適用於任何情況的元件通訊,在專案規模不大的情況下,完全可以使用中央事件匯流排EventBus 的方式,EventBus可以比較完美地解決包括父子元件、兄弟元件、隔代元件之間通訊,實際上就是一個觀察者模式,觀察者模式建立了一種物件與物件之間的依賴關係,一個物件發生改變時將自動通知其他物件,其他物件將相應做出反應。所以發生改變的物件稱為觀察目標,而被通知的物件稱為觀察者,一個觀察目標可以對應多個觀察者,而且這些觀察者之間沒有相互聯絡,可以根據需要增加和刪除觀察者,使得系統更易於擴充套件。首先我們需要實現一個訂閱釋出類作為單例模組匯出,每個需要的元件再進行import,當然作為Mixins全域性靜態橫切也可以,或者使用event庫,此外務必注意在元件銷燬的時候解除安裝訂閱的事件呼叫,否則會造成記憶體洩漏。

// event-bus.js
var PubSub = function() {
    this.handlers = {};
}

PubSub.prototype = {
    constructor: PubSub,
    on: function(key, handler) { // 訂閱
        if(!(key in this.handlers)) this.handlers[key] = [];
        if(!this.handlers[key].includes(handler)) {
             this.handlers[key].push(handler);
             return true;
        }
        return false;
    },

    once: function(key, handler) { // 一次性訂閱
        if(!(key in this.handlers)) this.handlers[key] = [];
        if(this.handlers[key].includes(handler)) return false;
        const onceHandler = (...args) => {
            handler.apply(this, args);
            this.off(key, onceHandler);
        }
        this.handlers[key].push(onceHandler);
        return true;
    },

    off: function(key, handler) { // 解除安裝
        const index = this.handlers[key].findIndex(item => item === handler);
        if (index < 0) return false;
        if (this.handlers[key].length === 1) delete this.handlers[key];
        else this.handlers[key].splice(index, 1);
        return true;
    },

    commit: function(key, ...args) { // 觸發
        if (!this.handlers[key]) return false;
        console.log(key, "Execute");
        this.handlers[key].forEach(handler => handler.apply(this, args));
        return true;
    },

}

export default new PubSub();
<!-- 子元件 -->
import React from "react";
import eventBus from "./event-bus";


class Child extends React.PureComponent{

    render() {
        return (
            <>
                <div>接收父元件的值: {this.props.msg}</div>
                <button onClick={() => eventBus.commit("ChangeMsg", "Changed Msg")}>修改父元件的值</button>
            </>
        )
    }
}

export default Child;
<!-- 父元件 -->
import React from "react";
import Child from "./child";
import eventBus from "./event-bus";

class Parent extends React.PureComponent{

    constructor(props){
        super(props);
        this.state = { msg: "Parent Msg" };
        this.child = React.createRef();
    }

    changeMsg = (msg) => {
        this.setState({ msg });
    }

    componentDidMount(){
        eventBus.on("ChangeMsg", this.changeMsg);
    }

    componentWillUnmount(){
        eventBus.off("ChangeMsg", this.changeMsg);

    }

    render() {
        return (
            <div>
                <Child msg={this.state.msg} ref={this.child} />
            </div>
        )
    }
}

export default Parent;

Redux

Redux同樣可以適用於任何情況的元件通訊,Redux中提出了單一資料來源Store用來儲存狀態資料,所有的元件都可以通過Action修改Store,也可以從Store中獲取最新狀態,使用了redux就可以解決多個元件的共享狀態管理以及元件之間的通訊問題。

import { createStore } from "redux";

/**
 * 這是一個 reducer,形式為 (state, action) => state 的純函式。
 * 描述了 action 如何把 state 轉變成下一個 state。
 *
 * state 的形式取決於你,可以是基本型別、陣列、物件、
 * 甚至是 Immutable.js 生成的資料結構。惟一的要點是
 * 當 state 變化時需要返回全新的物件,而不是修改傳入的引數。
 *
 * 下面例子使用 `switch` 語句和字串來做判斷,但你可以寫幫助類(helper)
 * 根據不同的約定(如方法對映)來判斷,只要適用你的專案即可。
 */
function counter(state = 0, action) {
    switch (action.type) {
        case "INCREMENT": return state + 1;
        case "DECREMENT": return state - 1;
        default: return state;
  }
}

// 建立 Redux store 來存放應用的狀態。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

// 可以手動訂閱更新,也可以事件繫結到檢視層。
store.subscribe(() => console.log(store.getState()));

// 改變內部 state 惟一方法是 dispatch 一個 action。
// action 可以被序列化,用日記記錄和儲存下來,後期還可以以回放的方式執行
store.dispatch({ type: "INCREMENT" });
// 1
store.dispatch({ type: "INCREMENT" });
// 2
store.dispatch({ type: "DECREMENT" });
// 1

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://zhuanlan.zhihu.com/p/76996552
https://www.jianshu.com/p/fb915d9c99c4
https://juejin.cn/post/6844903828945387528
https://segmentfault.com/a/1190000023585646
https://github.com/andyChenAn/frontEnd/issues/46
https://blog.csdn.net/weixin_42262436/article/details/88852369

相關文章