React 組件間通訊


React 組件間通訊

說 React 組件間通訊之前,我們先來討論一下 React 組件究竟有多少種層級間的關系。假設我們開發的項目是一個純 React 的項目,那我們項目應該有如下類似的關系:

父子:Parent 與 Child_1、Child_2、Child_1_1、Child_1_2、Child_2_1

兄弟:Child_1 與 Child_2、Child_1_1 與 Child_2、etc.

針對這些關系,我們將來好好討論一下這些關系間的通訊方式。

(在 React 中,React 組件之間的關系為從屬關系,與 DOM 元素之間的父子關系有所不同,下面只是為了說明方便,將 React 組件的關系類比成父子關系進行闡述)

父組件向子組件通訊

通訊是單向的,數據必須是由一方傳到另一方。在 React 中,父組件可以向子組件通過傳 props 的方式,向子組件進行通訊。

 
class Parent extends Component{
state = {
msg: 'start'
};

componentDidMount() {
setTimeout(() => {
this.setState({
msg: 'end'
});
}, 1000);
}

render() {
return <Child_1 msg={this.state.msg} />;
}
}

class Child_1 extends Component{
render() {
return <p>{this.props.msg}</p>
}
}

如果父組件與子組件之間不止一個層級,如 Parent 與 Child_1_1 這樣的關系,可通過 ... 運算符(Object 剩余和展開屬性),將父組件的信息,以更簡潔的方式傳遞給更深層級的子組件。通過這種方式,不用考慮性能的問題,通過 babel 轉義后的 ... 運算符 性能和原生的一致,且上級組件 props 與 state 的改變,會導致組件本身及其子組件的生命周期改變,

 
// 通過 ... 運算符 向 Child_1_1 傳遞 Parent 組件的信息
class Child_1 extends Component{
render() {
return <div>
<p>{this.props.msg}</p>
<Child_1_1 {...this.props}/>
</div>
}
}

class Child_1_1 extends Component{
render() {
return <p>{this.props.msg}</p>
}
}

子組件向父組件通訊

在上一個例子中,父組件可以通過傳遞 props 的方式,自頂而下向子組件進行通訊。而子組件向父組件通訊,同樣也需要父組件向子組件傳遞 props 進行通訊,只是父組件傳遞的,是作用域為父組件自身的函數,子組件調用該函數,將子組件想要傳遞的信息,作為參數,傳遞到父組件的作用域中。

 
class Parent extends Component{
state = {
msg: 'start'
};

transferMsg(msg) {
this.setState({
msg
});
}

render() {
return <div>
<p>child msg: {this.state.msg}</p>
<Child_1 transferMsg = {msg => this.transferMsg(msg)} />
</div>;
}
}

class Child_1 extends Component{
componentDidMount() {
setTimeout(() => {
this.props.transferMsg('end')
}, 1000);
}

render() {
return <div>
<p>child_1 component</p>
</div>
}
}

在上面的例子中,我們使用了 箭頭函數,將父組件的 transferMsg 函數通過 props 傳遞給子組件,得益於箭頭函數,保證子組件在調用 transferMsg 函數時,其內部 this 仍指向父組件。

當然,對於層級比較深的子組件與父組件之間的通訊,仍可使用 ... 運算符,將父組件的調用函數傳遞給子組件,具體方法和上面的例子類似。

兄弟組件間通訊

對於沒有直接關聯關系的兩個節點,就如 Child_1 與 Child_2 之間的關系,他們唯一的關聯點,就是擁有相同的父組件。參考之前介紹的兩種關系的通訊方式,如果我們向由 Child_1 向 Child_2 進行通訊,我們可以先通過 Child_1 向 Parent 組件進行通訊,再由 Parent 向 Child_2 組件進行通訊,所以有以下代碼。

 
class Parent extends Component{
state = {
msg: 'start'
};

transferMsg(msg) {
this.setState({
msg
});
}

componentDidUpdate() {
console.log('Parent update');
}

render() {
return (
<div>
<Child_1 transferMsg = {msg => this.transferMsg(msg)} />
<Child_2 msg = {this.state.msg} />
</div>
);
}
}

class Child_1 extends Component{
componentDidMount() {
setTimeout(() => {
this.props.transferMsg('end')
}, 1000);
}

componentDidUpdate() {
console.log('Child_1 update');
}

render() {
return <div>
<p>child_1 component</p>
</div>
}
}

class Child_2 extends Component{
componentDidUpdate() {
console.log('Child_2 update');
}

render() {
return <div>
<p>child_2 component: {this.props.msg}</p>
<Child_2_1 />
</div>
}
}

class Child_2_1 extends Component{
componentDidUpdate() {
console.log('Child_2_1 update');
}

render() {
return <div>
<p>child_2_1 component</p>
</div>
}
}

然而,這個方法有一個問題,由於 Parent 的 state 發生變化,會觸發 Parent 及從屬於 Parent 的子組件的生命周期,所以我們在控制台中可以看到,在各個組件中的 componentDidUpdate 方法均被觸發。

有沒有更好的解決方式來進行兄弟組件間的通訊,甚至是父子組件層級較深的通訊的呢?

觀察者模式

在傳統的前端解耦方面,觀察者模式作為比較常見一種設計模式,大量使用在各種框架類庫的設計當中。即使我們在寫 React,在寫 JSX,我們核心的部分還是 JavaScript。

觀察者模式也叫 發布者-訂閱者模式,發布者發布事件,訂閱者監聽事件並做出反應,對於上面的代碼,我們引入一個小模塊,使用觀察者模式進行改造。

 
import eventProxy from '../eventProxy'

class Parent extends Component{
render() {
return (
<div>
<Child_1/>
<Child_2/>
</div>
);
}
}
// componentDidUpdate 與 render 方法與上例一致
class Child_1 extends Component{
componentDidMount() {
setTimeout(() => {
// 發布 msg 事件
eventProxy.trigger('msg', 'end');
}, 1000);
}
}
// componentDidUpdate 方法與上例一致
class Child_2 extends Component{
state = {
msg: 'start'
};

componentDidMount() {
// 監聽 msg 事件
eventProxy.on('msg', (msg) => {
this.setState({
msg
});
});
}

render() {
return <div>
<p>child_2 component: {this.state.msg}</p>
<Child_2_1 />
</div>
}
}

我們在 child_2 組件的 componentDidMount 中訂閱了 msg 事件,並在 child_1 componentDidMount 中,在 1s 后發布了 msg 事件,child_2 組件對 msg 事件做出相應,更新了自身的 state,我們可以看到,由於在整個通訊過程中,只改變了 child_2 的 state,因而只有 child_2 和 child_2_1 出發了一次更新的生命周期。

而上面代碼中,神奇的 eventProxy.js 究竟是怎樣的一回事呢?

 
// eventProxy.js
'use strict';
const eventProxy = {
onObj: {},
oneObj: {},
on: function(key, fn) {
if(this.onObj[key] === undefined) {
this.onObj[key] = [];
}

this.onObj[key].push(fn);
},
one: function(key, fn) {
if(this.oneObj[key] === undefined) {
this.oneObj[key] = [];
}

this.oneObj[key].push(fn);
},
off: function(key) {
this.onObj[key] = [];
this.oneObj[key] = [];
},
trigger: function() {
let key, args;
if(arguments.length == 0) {
return false;
}
key = arguments[0];
args = [].concat(Array.prototype.slice.call(arguments, 1));

if(this.onObj[key] !== undefined
&& this.onObj[key].length > 0) {
for(let i in this.onObj[key]) {
this.onObj[key][i].apply(null, args);
}
}
if(this.oneObj[key] !== undefined
&& this.oneObj[key].length > 0) {
for(let i in this.oneObj[key]) {
this.oneObj[key][i].apply(null, args);
this.oneObj[key][i] = undefined;
}
this.oneObj[key] = [];
}
}
};

export default eventProxy;

eventProxy 中,總共有 on、one、off、trigger 這 4 個函數:

  • on、one:on 與 one 函數用於訂閱者監聽相應的事件,並將事件響應時的函數作為參數,on 與 one 的唯一區別就是,使用 one 進行訂閱的函數,只會觸發一次,而 使用 on 進行訂閱的函數,每次事件發生相應時都會被觸發。
  • trigger:trigger 用於發布者發布事件,將除第一參數(事件名)的其他參數,作為新的參數,觸發使用 one 與 on 進行訂閱的函數。
  • off:用於解除所有訂閱了某個事件的所有函數。

Flux 與 Redux

Flux 作為 Facebook 發布的一種應用架構,他本身是一種模式,而不是一種框架,基於這個應用架構模式,在開源社區上產生了眾多框架,其中最受歡迎的就是我們即將要說的 Redux。更多關於 Flux 和 Redux 的介紹這里就不一一展開,有興趣的同學可以好好看看 Flux 官方介紹Flux 架構入門教程–阮一峰等相關資料。
下面將來好好聊聊 Redux 在組件間通訊的方式。

Flux 需要四大部分組成:Dispatcher、Stores、Views/Controller-Views、Actions,其中的 Views/Controller-Views 可以理解為我們上面所說的 Parent 組件,其作用是從 state 當中獲取到相應的數據,並將其傳遞給他的子組件(descendants)。而另外 3 個部分,則是由 Redux 來提供了。

 
// 該例子主要對各組件的 componentDidMount 進行改造,其余部分一致
import {createStore} from 'redux'

function reducer(state = {}, action) {
return action;
}

let store = createStore(reducer);

class Child_1 extends Component{
componentDidMount() {
setTimeout(() => {
store.dispatch({
type: 'child_2',
data: 'hello'
})
}, 1000);

setTimeout(() => {
store.dispatch({
type: 'child_2_1',
data: 'bye'
})
}, 2000);
}
}

class Child_2 extends Component{
state = {
msg: 'start'
};

componentDidUpdate() {
console.log('Child_2 update', store.getState());
}

componentDidMount() {
store.subscribe(() => {
let state = store.getState();
if (state.type === 'child_2') {
this.setState({
msg: state.data
});
}
});
}
}

class Child_2_1 extends Component{
state = {
msg: 'start'
};

componentDidUpdate() {
console.log('Child_2_1 update', store.getState());
}


componentDidMount() {
store.subscribe(() => {
let state = store.getState();
if (state.type === 'child_2_1') {
this.setState({
msg: state.data
});
}
});
}

render() {
return <div>
<p>child_2_1 component: {this.state.msg}</p>
</div>
}
}

在上面的例子中,我們將一個名為 reducer 的函數作為參數,生成我們所需要的 store,reducer 接受兩個參數,一個是存儲在 store 里面的 state,另一個是每一次調用 dispatch 所傳進來的 action。reducer 的作用,就是對 dispatch 傳進來的 action 進行處理,並將結果返回。而里面的 state 可以通過 store 里面的 getState 方法進行獲得,其結果與最后一次通過 reducer 處理后的結果保持一致。

在 child_1 組件中,我們每隔 1s 通過 store 的 dispatch 方法,向 store 傳入包含有 type 字段的 action,reducer 直接將 action 進行返回。

而在 child_2 與 child_2_1 組件中,通過 store 的 subscribe 方法,監聽 store 的變化,觸發 dispatch 后,所有通過 subscribe 進行監聽的函數都會作出相應,根據當前通過 store.getState() 獲取到的結果進行處理,對當前組件的 state 進行設置。所以我們可以在控制台上看到各個組件更新及存儲在 store 中 state 的情況:

在 Redux 中,store 的作用,與 MVC 中的 Model 類似,可以將我們項目中的數據傳遞給 store,交給 store 進行處理,並可以實時通過 store.getState() 獲取到存儲在 store 中的數據。我們對上面例子的 reducer 及各個組件的 componentDidMount 做點小修改,看看 store 的這一個特性。

 
import {createStore} from 'redux'

function reducer(state = {}, action) {
switch (action.type) {
case 'child_2':
state.child_2 = action.data + ' child_2';
return state;
case 'child_2_1':
state.child_2_1 = action.data + ' child_2_1';
return state;
default:
return state
}
}

let store = createStore(reducer);

class Child_1 extends Component{
componentDidMount() {
setTimeout(() => {
store.dispatch({
type: 'child_2',
data: 'hello'
})
}, 1000);

setTimeout(() => {
store.dispatch({
type: 'child_2_1',
data: 'bye'
})
}, 2000);
}
}

class Child_2 extends Component{
componentDidMount() {
store.subscribe(() => {
let state = store.getState();

if (state.hasOwnProperty('child_2')) {
this.setState({
msg: state.child_2
});
}
});
}
}

class Child_2_1 extends Component{
componentDidMount() {
store.subscribe(() => {
let state = store.getState();

if (state.hasOwnProperty('child_2_1')) {
this.setState({
msg: state.child_2_1
});
}
});
}
}

我們對創建 store 時所傳進去的 reducer 進行修改。reducer 中,其參數 state 為當前 store 的值,我們對不同的 action 進行處理,並將處理后的結果存儲在 state 中並進行返回。此時,通過 store.getState() 獲取到的,就是我們處理完成后的 state。

Redux 內部的實現,其實也是基於觀察者模式的,reducer 的調用結果,存儲在 store 內部的 state 中,並在每一次 reducer 的調用中並作為參數傳入。所以在 child_1 組件第 2s 的 dispatch 后,child_2 與 child_2_1 組件通過 subscribe 監聽的函數,其通過 getState 獲得的值,都包含有 child_2 與 child_2_1 字段的,這就是為什么第 2s 后的響應,child_2 也進行了一次生命周期。所以在對 subscribe 響應后的處理,最好還是先校對通過 getState() 獲取到的 state 與當前組件的 state 是否相同。

 
// child_2
componentDidMount() {
store.subscribe(() => {
let state = store.getState();

if (state.hasOwnProperty('child_2')
&& state.child_2 !== this.state.msg) {
this.setState({
msg: state.child_2
});
}
});
}

加上這樣的校驗,各個組件的生命周期的觸發就符合我們的預期了。

小結

Redux 對於組件間的解耦提供了很大的便利,如果你在考慮該不該使用 Redux 的時候,社區里有一句話說,“當你不知道該不該使用 Redux 的時候,那就是不需要的”。Redux 用起來一時爽,重構或者將項目留給后人的時候,就是個大坑,Redux 中的 dispatch 和 subscribe 方法遍布代碼的每一個角落。剛剛的例子不是最好的,Flux 設計中的 Controller-Views 概念就是為了解決這個問題出發的,將所有的 subscribe 都置於 Parent 組件(Controller-Views),由最上層組件控制下層組件的表現,然而,這不就是我們所說的 子組件向父組件通訊 這種方式了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM