根據組件之間的嵌套關係(即層級關係)可分爲4種通訊方式:父子、兄弟、跨級和無級。html
在React中,數據是自頂向下單向流動的,而父組件經過props向子組件傳遞須要的信息是組件之間最多見的通訊方式,以下代碼所示,父組件Parent向子組件Child傳遞了一個name屬性,其值爲一段字符串「strick」。git
class Parent extends React.Component { render() { return <Child name="strick">子組件</Child>; } } class Child extends React.Component { render() { return <input name={this.props.name} type="text" />; } }
當須要子組件向父組件傳遞信息時,也能經過組件的props實現,只是要多傳一個回調函數,以下所示。緩存
class Parent extends React.Component { callback(value) { console.log(value); //輸出從子組件傳遞過來的值 } render() { return <Child callback={this.callback} />; } } class Child extends React.Component { constructor(props) { super(props); this.state = { name: "" }; } handle(e) { this.props.callback(e.target.value); //調用父組件的回調函數 this.setState({ name: e.target.value }); //更新文本框中的值 } render() { return <input value={this.state.name} type="text" onChange={this.handle.bind(this)} />; } }
父組件Parent會傳給子組件Child一個callback()方法,子組件中的文本框註冊了一個onChange事件,在事件處理程序handle()中將回調父組件的callback()方法,並把文本框的值傳遞過去,以此達到反向通訊的效果。app
當兩個組件擁有共同的父組件時,就稱它們爲兄弟組件,注意,它們能夠不在一個層級上,如圖6所示,C與D或E都是兄弟關係。dom
圖6 組件樹ide
兄弟之間不能直接通訊,須要藉助狀態提高的方式間接實現信息的傳遞,即把組件之間要共享的狀態提高至最近的父組件中,由父組件來統一管理。而任意一個兄弟組件可經過從父組件傳來的回調函數更新共享狀態,新的共享狀態再經過父組件的props回傳給子組件,從而完成一次兄弟之間的通訊。在下面的例子中,會有兩個文本框(如圖7所示),當向其中一個輸入數字時,鄰近的文本框會隨之改變,要麼加一,要麼減一。函數
圖7 兩個文本框工具
class Parent extends React.Component { constructor(props) { super(props); this.state = { type: "p", digit: 0 }; this.plus = this.plus.bind(this); this.minus = this.minus.bind(this); } plus(digit) { this.setState({ type: "p", digit }); } minus(digit) { this.setState({ type: "m", digit }); } render() { let { type, digit } = this.state; let pdigit = type == "p" ? digit : (digit+1); let mdigit = type == "m" ? digit : (digit-1); return ( <> <Child type="p" digit={pdigit} onDigitChange={this.plus} /> <Child type="m" digit={mdigit} onDigitChange={this.minus} /> </> ); } } class Child extends React.Component { constructor(props) { super(props); this.handle = this.handle.bind(this); } handle(e) { this.props.onDigitChange(+e.target.value); } render() { return ( <input value={this.props.digit} type="text" onChange={this.handle} /> ); } }
上面代碼實現了一次完整的兄弟之間的通訊,具體過程以下所列。this
(1)首先在父組件Parent中定義兩個兄弟組件Child,其中type屬性爲「p」的子組件用於遞增,綁定了plus()方法;type屬性爲「m」的子組件用於遞減,綁定了minus()方法。spa
(2)而後在子組件Child中接收傳遞過來的digit屬性和onDigitChange()方法,前者會做爲文本框的值,後者會在事件處理程序onChange()中被調用。
(3)若是在遞增文本框中修改數值,那麼就將新值傳給plus()方法。遞減文本框的處理過程與之相似,只是將plus()方法替換成minus()方法。
(4)最後更新父組件中的兩個狀態:type和digit,完成信息的傳遞。
在一棵組件樹中,當多個組件須要跨級通訊時,所處的層級越深,那麼須要過渡的中間層就越多,完成一次通訊將變得很是繁瑣,而在數據傳遞過程當中那些做爲橋樑的組件,其代碼也將變得冗餘且臃腫。
在React中,還可用Context實現跨級通訊。Context能存放組件樹中須要全局共享的數據,也就是說,一個組件能夠藉助Context跨越層級直接將數據傳遞給它的後代組件。如圖8所示,左邊的數據會經過組件的props逐級顯式地傳遞,右邊的數據會經過Context讓全部組件均可訪問。
圖8 props和context
隨着React v16.3的發佈,引入了一種全新的Context,修正了舊版本中較爲棘手的問題,接下來的篇幅將着重分析這兩個版本的Context。
1)舊的Context
在舊版本的Context中,首先要在頂層組件內添加getChildContext()方法和靜態屬性childContextTypes,前者用於生成一個context對象(即初始化Context須要攜帶的數據),後者經過prop-types庫限制該對象的屬性的數據類型,二者缺一不可。在下面的示例中,Grandpa是頂層組件,Son是中間組件,要傳遞的是一個包含name屬性的對象。
//頂層組件 class Grandpa extends React.Component { getChildContext() { return { name: "strick" }; } render() { return <Son />; } } Grandpa.childContextTypes = { name: PropTypes.string }; //中間組件 class Son extends React.Component { render() { return <Grandson />; } }
而後給後代組件(例以下面的Grandson)添加靜態屬性contextTypes,限制要接收的屬性的數據類型,最後就能經過讀取this.context獲得由頂層組件提供的數據。
class Grandson extends React.Component { render() { return <p>{this.context.name}</p>; } } Grandson.contextTypes = { name: PropTypes.string };
從上面的示例中能夠看出,跨級通訊的準備工做並不簡單,須要在兩處作不一樣的配置。React官方建議慎用舊版的Context,由於它至關於JavaScript中的全局變量,容易形成數據流混亂、重名覆蓋等各類反作用,而且在將來的React版本中有可能被廢棄。
雖然在功能上Context實現了跨級通訊,但本質上數據仍是像props同樣逐級傳遞的,所以若是某個中間組件的shouldComponentUpdate()方法返回false的話,就會阻止下層的組件更新Context中的數據。接下來會演示這個致命的缺陷,沿用上一個示例,對兩個組件作些調整。在Grandpa組件中,先讓Context保存組件的name狀態,再新增一個按鈕,併爲其註冊一個能更新組件狀態的點擊事件;在Son組件中,添加shouldComponentUpdate()方法,它的返回值是false。在把Grandpa組件掛載到DOM中後,點擊按鈕就能發現Context的更新傳播終止於Son組件。
class Grandpa extends React.Component { constructor(props) { super(props); this.state = { name: "strick" }; this.click = this.click.bind(this); } getChildContext() { return { name: this.state.name }; } click() { this.setState({ name: "freedom" }); } render() { return ( <> <Son /> <button onClick={this.click}>提交</button> </> ); } } class Son extends React.Component { shouldComponentUpdate() { return false; } render() { return <Grandson />; } }
2)新的Context
這個版本的Context不只採用了更符合React風格的聲明式寫法,還能夠直接將數據傳遞給後代組件而不用逐級傳遞,一舉衝破了shouldComponentUpdate()方法的限制。下面仍然使用上一節的三個組件,完成一次新的跨級通訊。
const NameContext = React.createContext({name: "strick"}); class Grandpa extends React.Component { render() { return ( <NameContext.Provider value={{name: "freedom"}}> <Son /> </NameContext.Provider> ); } } class Son extends React.Component { render() { return <Grandson />; } } class Grandson extends React.Component { render() { return ( <NameContext.Consumer>{context => <p>{context.name}</p>}</NameContext.Consumer> ); } }
經過上述代碼可知,新的Context由三部分組成:
(1)React.createContext()方法,接收一個可選的defaultValue參數,返回一個Context對象(例如NameContext),包含兩個屬性:Provider和Consumer,它們是一對相呼應的組件。
(2)Provider,來源組件,它的value屬性就是要傳送的數據,Provider可關聯多個來自於同一個Context對象的Consumer,像NameContext.Provider只能與NameContext.Consumer配合使用。
(3)Consumer,目標組件,出如今Provider以後,可接收一個返回React元素的函數,若是Consumer能找到對應的Provider,那麼函數的參數就是Provider的value屬性,不然就讀取defaultValue的值。
注意,Provider組件會經過Object.is()對其value屬性的新舊值作比較,以此肯定是否更新做爲它後代的Consumer組件。
當兩個沒有嵌套關係(即無級)的組件須要通訊時,能夠藉助消息隊列實現。下面是一個用觀察者模式實現的簡易消息隊列庫,其處理過程相似於事件系統,若是將消息當作事件,那麼訂閱消息就是綁定事件,而發佈消息就是觸發事件。
class EventEmitter { constructor() { this.events = {}; } sub(event, listener) { //訂閱消息 if (!this.events[event]) { this.events[event] = { listeners: [] }; } this.events[event].listeners.push(listener); } pub(name, ...params) { //發佈消息 for (const listener of this.events[name].listeners) { listener.apply(this, params); } } }
EventEmitter只包含了三個方法,它們的功能以下所列:
(1)構造函數,初始化了一個用於緩存各種消息的容器。
(2)sub()方法,將回調函數用消息名稱分類保存。
(3)pub()方法,依次執行了指定名稱下的消息集合。
下面用一個示例演示無級通訊,在Sub組件的構造函數中,會訂閱一次消息,消息名稱爲"TextBox",回調函數會接收一個參數,並將其輸出到控制檯。
let emitter = new EventEmitter(); class Sub extends React.Component { constructor(props) { super(props); emitter.sub("TextBox", value => console.log(value)); } render() { return <p>訂閱消息</p>; } }
在下面的Pub組件中,爲文本框註冊了onChange事件,在事件處理程序handle()中發佈名爲"TextBox"的消息集合,並將文本框中的值做爲參數傳遞到回調函數中。
class Pub extends React.Component { constructor(props) { super(props); this.state = { value: "" }; } handle(e) { const value = e.target.value; emitter.pub("TextBox", value); this.setState({ value }); } render() { return <input value={this.state.value} onChange={this.handle.bind(this)} />; } }
Sub組件和Pub組件會像下面這樣,以兄弟的關係掛載到DOM中。當修改文本框中的內容時,就會觸發消息的發佈,從而完成了一次它們之間的通訊。
ReactDOM.render( <> <Sub /> <Pub /> </>, document.getElementById("container") );
當業務邏輯複雜到必定程度時,普通的消息隊列可能就捉襟見肘了,此時能夠考慮引入Mobx、Redux等專門的狀態管理工具來實現組件之間的通訊。