react元件通訊通識篇
父元件向子元件通訊是我們開發中非常常見,其方式也一般是props屬性最直接方便,當然這裡指的是直接就是父子元件關係的。
這裡只希望提示大家一點,如果你有需要,不要直接修改傳入屬性的值,如果你嘗試直接修改會有提示不建議不應該這樣操作,這個變數是隻讀的。如果你切實需要,你可以在state中接受並改變。
子向父元件通訊
通訊的方式主要有兩種,分別是自定義事件以及回撥函式。其中回撥函式非常方便,而且可以拿到執行時的所有狀態;其中自定義事件比較麻煩,我們一般使用比較少。
在下面的例子中,我使用的方式是回撥函式。
// listItem.jsx render() { const { title } = this.state; const { click } = this.props; const title2 = title + "temp"; return ( <h2 onClick={click} onMouseDown={this.down}> {title2} </h2> ); } // list.jsx render() { const { list } = this.state; const style = { height: 30 }; return ( <div style={style}> {list.map(item => ( <ListItem {...item} click={this.click.bind(this)} error={this.error} /> ))} </div> ); } 複製程式碼
備註:如果你希望傳遞事件以外側引數,你需要bind繫結傳參或者寫成簡單的箭頭函式。否則,直接寫函式以及引數會編譯成函式直接執行。(比如傳參text,onClick = {text => this.click(text), 或者寫成,onClick={this.click.bind(this,text)}).
兄弟元件通訊
兄弟元件如果是同一個父元件可以藉助父元件進行通訊,這裡不多描述,這裡分享的是基於事件釋出訂閱機制的實現,我們藉助nodejs的Events模組。
根目錄下建立一個單例的事件,然後匯出,為了避免浪費,我們全域性使用一個單例檔案events.js。
import {EventEmitter} from 'events'; export default new EventEmitter(); 複製程式碼
// a元件中觸發 import emitter from '../events'; emitter.emit('clickMsg','點選了') // b元件監聽 commpontentDidMount(){ this.clickMsg = emitter.on('clickMsg',data => { console.log(data) }) } compontWillUnmount(){ emitter.removeListener(this.clickMsg) } 複製程式碼
備註:事件的執行是否會多次執行?目前遇到的一個問題,日誌會執行多次,檔案中的說明,Hook a console constructor and forward messages to a callback 。
跨級元件通訊 -- context
跨級元件我們也可以認為是父子元件的疊加,因此可以使用多層父子元件的資料傳遞,但這樣比較繁瑣,因此更建議的方式是是通過context的方式,來實現。它的原理也比較簡單,就是基於同一個父元件的前提下,它的任何層級的子元件都可以拿到這個父附件的狀態,並以此進行通訊。
在之前的官網版本中,一直具有這個功能,不過不建議使用而已,但隨著版本迭代,慢慢發現這個功能具有較大的便利性。
// 定義一個context的全域性物件:需要注意的是你在provider的地方使用它的時候也必須符合基本的資料結構 import React from "react"; export const ThemeContext React.createContext({ color: "black", label: "名字" }); // 父或者頂層容器設計 import { ThemeContext} from "../themeContext"; constructor(props){ super(props); this.state = { theme:{ color:'pink', label:'你的名字' } } } render(){ return ( <ThemeContext.Provider value={this.state.theme}> </ThemeContext.Provider> ) } // 任意層級的子元件 import {ThemeContext} from "../../themeContext"; render(){ <ThemeContext.Consumer> {({ color, label }) => <label style={{ color: color }}>{label}</label>} </ThemeContext.Consumer> } 複製程式碼
- 動態context:我們可以直接在provider的時候動態的提供或者修改其預設值。
- 使用建議:不要盲目使用context,僅當多個元件使用同一個公用資料時才建議引入context解決這個問題。
- 特點: 提供一個全域性或者任意元件可訪問的context物件,然後元件通過修改或者消費這個context物件來實現使用。
新舊context的對比
- 原先的context需要顯性的宣告type,獲取子
- 原先的傳遞方式是級聯傳遞,新的方式是直接傳遞,效率更高。

- 原先的方式通過級聯的方式,如果某以及willUpdate中攔截返回false,那麼其子都不能正常獲取,新版的沒有這個問題。
ant-design form元件中的使用
在antd 的form元件中,我們也看到對應的context的程式碼部分:檔案地址: 連結
// context 設定的預設值 import createReactContext, { Context } from 'create-react-context'; import { ColProps } from '../grid/col'; export interface FormContextProps { vertical: boolean; colon?: boolean; labelAlign?: string; labelCol?: ColProps; wrapperCol?: ColProps; } export const FormContext: Context<FormContextProps> = createReactContext({ labelAlign: 'right', vertical: false, }); // 引入預設的context import { FormContext } from './context'; // 獲取傳入的屬性,然後解構,通過context provider的方式,state傳值提供給包含在form元件中的元件 render() { const { wrapperCol, labelAlign, labelCol, layout, colon } = this.props; return ( <FormContext.Provider value={{ wrapperCol, labelAlign, labelCol, vertical: layout === 'vertical', colon }} > <ConfigConsumer>{this.renderForm}</ConfigConsumer> </FormContext.Provider> ); } 複製程式碼
父元件呼叫子元件事件
雖然上面的方式提到了各種通訊機制,但對於父元件需要主動觸發子元件的某個事件還是沒有相應的方法。我們在下面的例子中給出這種需求的場景。比如:在容器元件中,我們需要主動重新整理列表中的資料,而列表元件的資料是自己控制獲取的。
備註:當然有的人會說,既然有如此的需求,為什麼不把子元件的資料或者方法通過父元件的屬性傳入,把邏輯在父元件中維護,這樣就沒有這樣的問題。固然是一種很不錯的方法,但也確實存在有些時候,我們希望子元件更多的具有一些功能,而不重度依賴父元件,只是將自己的事件暴露給父元件。其實這種設計思路在vue中,在開源的ui框架中很多,稱為自定義事件,我們除了屬性傳入控制組件之外,還可以通過元件的自定義事件,事件呼叫實現改變元件的機制。
方式一:refs 方式
class List extends React.Component{ constructor(props){ this.state ={ desc:'' } } xxxMethod(){ const desc = Math.random(); this.setState({ desc }) } render(){ const {list} = this.props; return ( <div> <button onClick={this.xxxMethod.bind(this)}>點選變動標題描述</button> {list.map(text =>(<h2>{text}{desc}</h2>))} </div> ) } } class parent extends React.Component{ refresh(){ // 可以呼叫子元件的任意方法 this.refs.List.xxxMethod(); } //父元件 render (){ return ( <div> <button onClick={this.refresh.bind(this)}>點選變動標題描述</button> <List ref="List" /> </div>) } } 複製程式碼
方式二 通過主動呼叫返回this
通過回撥事件傳回this,然後賦值給父元件。看上去這段和ref的直接使用沒有任何差別,不過能確定的是通過這種方式,我們能準確的拿到子元件的this例項。它為什麼是合理存在的呢?因為有些情況下,我們通過ref拿到的不是我們想要的子元件中的屬性或者方法。
class Child extends React.Component{ constructor(props){ super(props); } componentDidMount(){ this.props.onRef(this) } console(){ console.log('子元件的方法'); } render(){ return (<div>子元件</div>) } } class Parent extends React.Component{ onRef(comp){ this.child = comp; } console(){ this.child.console() } render(){ return ( <div> <button onClick={this.console.bind(this)}>點選執行子元件方法</button> <Child onRef={this.onRef.bind(this)}/> </div> ) } } 複製程式碼
方式三:HOC
通過高階元件的方式,我們可以一次性解決類似的需求,而不是每次都手動的寫this的暴露方法。在你需要的位置引入,然後父子元件分別寫上@withRef。
import React from "react"; export default WrappedComponent => { return class withRef extends React.Component { static displayName = `withRef(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`; render() { // 這裡重新定義一個props的原因是: // 你直接去修改this.props.ref在react開發模式下會報錯,不允許你去修改 const props = { ...this.props }; // 在這裡把getInstance賦值給ref, // 傳給`WrappedComponent`,這樣就getInstance能獲取到`WrappedComponent`例項 // 感謝評論區的[yangshenghaha]同學的完善 props.ref = el => { this.props.getInstance && this.props.getInstance(el); this.props.ref && this.props.ref(el); }; return <WrappedComponent {...props} />; } }; }; 複製程式碼
如果你的語法提示錯誤,不支援裝飾器,你可以通過下面的設定:安裝包依賴,npm install @babel/plugin-proposal-decorators。然後需要在package.json中設定外掛的配置:
"babel": { "plugins": [ [ "@babel/plugin-proposal-decorators", { "legacy": true } ] ], "presets": [ "react-app" ] }, 複製程式碼
小結
通過本節,我們熟悉了react基本的父子元件、兄弟元件、跨級元件之間如何進行通訊,我們需要知道的額外一點是,通訊指的不僅僅是資料的通訊,也有事件的通訊,比如主動的根據時機去喚起父元件或者子元件的事件,在元件中我們可以很方便的通過屬性呼叫父元件的方法或者傳遞的資料,我們也需要知道如何將子元件的事件暴露給父元件使用。
其實重點在下一篇,元件抽象,當然就是說的高階元件,只不過這次從《深入react技術棧》中得到了更多更全面的啟示。點選跳轉:react元件抽象