很感謝https://segmentfault.com/u/cwl提供的答案
React 組件間通訊
說 React 組件間通訊之前,我們先來討論一下 React 組件究竟有多少種層級(jí)間的關(guān)系衣形。假設(shè)我們開發(fā)的項(xiàng)目是一個(gè)純 React 的項(xiàng)目五垮,那我們項(xiàng)目應(yīng)該有如下類似的關(guān)系:
父子:Parent 與 Child_1、Child_2捎谨、Child_1_1勒叠、Child_1_2捐韩、Child_2_1
兄弟:Child_1 與 Child_2库继、Child_1_1 與 Child_2、etc.
針對(duì)這些關(guān)系窜醉,我們將來好好討論一下這些關(guān)系間的通訊方式宪萄。
(在 React 中,React 組件之間的關(guān)系為從屬關(guān)系榨惰,與 DOM 元素之間的父子關(guān)系有所不同拜英,下面只是為了說明方便,將 React 組件的關(guān)系類比成父子關(guān)系進(jìn)行闡述)
父組件向子組件通訊
通訊是單向的琅催,數(shù)據(jù)必須是由一方傳到另一方居凶。在 React 中,父組件可以向子組件通過傳 props 的方式藤抡,向子組件進(jìn)行通訊侠碧。
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>
}
}
如果父組件與子組件之間不止一個(gè)層級(jí),如 Parent 與 Child_1_1 這樣的關(guān)系缠黍,可通過 ... 運(yùn)算符(Object 剩余和展開屬性)弄兜,將父組件的信息,以更簡潔的方式傳遞給更深層級(jí)的子組件。通過這種方式替饿,不用考慮性能的問題语泽,通過 babel 轉(zhuǎn)義后的 ... 運(yùn)算符 性能和原生的一致,且上級(jí)組件 props 與 state 的改變视卢,會(huì)導(dǎo)致組件本身及其子組件的生命周期改變踱卵,
// 通過 ... 運(yùn)算符 向 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>
}
}
子組件向父組件通訊
在上一個(gè)例子中,父組件可以通過傳遞 props 的方式据过,自頂而下向子組件進(jìn)行通訊惋砂。而子組件向父組件通訊,同樣也需要父組件向子組件傳遞 props 進(jìn)行通訊蝶俱,只是父組件傳遞的班利,是作用域?yàn)楦附M件自身的函數(shù),子組件調(diào)用該函數(shù)榨呆,將子組件想要傳遞的信息罗标,作為參數(shù),傳遞到父組件的作用域中积蜻。
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>
}
}
在上面的例子中闯割,我們使用了 箭頭函數(shù),將父組件的 transferMsg 函數(shù)通過 props 傳遞給子組件竿拆,得益于箭頭函數(shù)宙拉,保證子組件在調(diào)用 transferMsg 函數(shù)時(shí),其內(nèi)部 this 仍指向父組件丙笋。
當(dāng)然谢澈,對(duì)于層級(jí)比較深的子組件與父組件之間的通訊,仍可使用 ... 運(yùn)算符御板,將父組件的調(diào)用函數(shù)傳遞給子組件锥忿,具體方法和上面的例子類似。
兄弟組件間通訊
對(duì)于沒有直接關(guān)聯(lián)關(guān)系的兩個(gè)節(jié)點(diǎn)怠肋,就如 Child_1 與 Child_2 之間的關(guān)系敬鬓,他們唯一的關(guān)聯(lián)點(diǎn),就是擁有相同的父組件笙各。參考之前介紹的兩種關(guān)系的通訊方式钉答,如果我們向由 Child_1 向 Child_2 進(jìn)行通訊,我們可以先通過 Child_1 向 Parent 組件進(jìn)行通訊杈抢,再由 Parent 向 Child_2 組件進(jìn)行通訊数尿,所以有以下代碼。
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>
}
}
然而惶楼,這個(gè)方法有一個(gè)問題砌创,由于 Parent 的 state 發(fā)生變化虏缸,會(huì)觸發(fā) Parent 及從屬于 Parent 的子組件的生命周期,所以我們?cè)诳刂婆_(tái)中可以看到嫩实,在各個(gè)組件中的 componentDidUpdate 方法均被觸發(fā)刽辙。
有沒有更好的解決方式來進(jìn)行兄弟組件間的通訊,甚至是父子組件層級(jí)較深的通訊的呢甲献?
觀察者模式
在傳統(tǒng)的前端解耦方面宰缤,觀察者模式作為比較常見一種設(shè)計(jì)模式,大量使用在各種框架類庫的設(shè)計(jì)當(dāng)中晃洒。即使我們?cè)趯?React慨灭,在寫 JSX,我們核心的部分還是 JavaScript球及。
觀察者模式也叫 發(fā)布者-訂閱者模式氧骤,發(fā)布者發(fā)布事件,訂閱者監(jiān)聽事件并做出反應(yīng)吃引,對(duì)于上面的代碼筹陵,我們引入一個(gè)小模塊,使用觀察者模式進(jìn)行改造镊尺。
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(() => {
// 發(fā)布 msg 事件
eventProxy.trigger('msg', 'end');
}, 1000);
}
}
// componentDidUpdate 方法與上例一致
class Child_2 extends Component{
state = {
msg: 'start'
};
componentDidMount() {
// 監(jiān)聽 msg 事件
eventProxy.on('msg', (msg) => {
this.setState({
msg
});
});
}
render() {
return <div>
<p>child_2 component: {this.state.msg}</p>
<Child_2_1 />
</div>
}
}
我們?cè)?child_2 組件的 componentDidMount 中訂閱了 msg 事件朦佩,并在 child_1 componentDidMount 中,在 1s 后發(fā)布了 msg 事件庐氮,child_2 組件對(duì) msg 事件做出相應(yīng)语稠,更新了自身的 state,我們可以看到弄砍,由于在整個(gè)通訊過程中仙畦,只改變了 child_2 的 state,因而只有 child_2 和 child_2_1 出發(fā)了一次更新的生命周期音婶。
而上面代碼中慨畸,神奇的 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 個(gè)函數(shù):
on、one:on 與 one 函數(shù)用于訂閱者監(jiān)聽相應(yīng)的事件厢汹,并將事件響應(yīng)時(shí)的函數(shù)作為參數(shù)螟深,on 與 one 的唯一區(qū)別就是,使用 one 進(jìn)行訂閱的函數(shù)烫葬,只會(huì)觸發(fā)一次界弧,而 使用 on 進(jìn)行訂閱的函數(shù)凡蜻,每次事件發(fā)生相應(yīng)時(shí)都會(huì)被觸發(fā)。
trigger:trigger 用于發(fā)布者發(fā)布事件垢箕,將除第一參數(shù)(事件名)的其他參數(shù)划栓,作為新的參數(shù),觸發(fā)使用 one 與 on 進(jìn)行訂閱的函數(shù)条获。
off:用于解除所有訂閱了某個(gè)事件的所有函數(shù)忠荞。
Flux 與 Redux
Flux 作為 Facebook 發(fā)布的一種應(yīng)用架構(gòu),他本身是一種模式帅掘,而不是一種框架委煤,基于這個(gè)應(yīng)用架構(gòu)模式,在開源社區(qū)上產(chǎn)生了眾多框架修档,其中最受歡迎的就是我們即將要說的 Redux碧绞。更多關(guān)于 Flux 和 Redux 的介紹這里就不一一展開,有興趣的同學(xué)可以好好看看 Flux 官方介紹吱窝、Flux 架構(gòu)入門教程–阮一峰等相關(guān)資料讥邻。
下面將來好好聊聊 Redux 在組件間通訊的方式。
Flux 需要四大部分組成:Dispatcher癣诱、Stores计维、Views/Controller-Views、Actions撕予,其中的 Views/Controller-Views 可以理解為我們上面所說的 Parent 組件鲫惶,其作用是從 state 當(dāng)中獲取到相應(yīng)的數(shù)據(jù),并將其傳遞給他的子組件(descendants)实抡。而另外 3 個(gè)部分欠母,則是由 Redux 來提供了。
// 該例子主要對(duì)各組件的 componentDidMount 進(jìn)行改造吆寨,其余部分一致
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>
}
}
在上面的例子中赏淌,我們將一個(gè)名為 reducer 的函數(shù)作為參數(shù),生成我們所需要的 store啄清,reducer 接受兩個(gè)參數(shù)六水,一個(gè)是存儲(chǔ)在 store 里面的 state,另一個(gè)是每一次調(diào)用 dispatch 所傳進(jìn)來的 action辣卒。reducer 的作用掷贾,就是對(duì) dispatch 傳進(jìn)來的 action 進(jìn)行處理,并將結(jié)果返回荣茫。而里面的 state 可以通過 store 里面的 getState 方法進(jìn)行獲得想帅,其結(jié)果與最后一次通過 reducer 處理后的結(jié)果保持一致。
在 child_1 組件中啡莉,我們每隔 1s 通過 store 的 dispatch 方法港准,向 store 傳入包含有 type 字段的 action旨剥,reducer 直接將 action 進(jìn)行返回。
而在 child_2 與 child_2_1 組件中浅缸,通過 store 的 subscribe 方法轨帜,監(jiān)聽 store 的變化,觸發(fā) dispatch 后衩椒,所有通過 subscribe 進(jìn)行監(jiān)聽的函數(shù)都會(huì)作出相應(yīng)阵谚,根據(jù)當(dāng)前通過 store.getState() 獲取到的結(jié)果進(jìn)行處理,對(duì)當(dāng)前組件的 state 進(jìn)行設(shè)置烟具。所以我們可以在控制臺(tái)上看到各個(gè)組件更新及存儲(chǔ)在 store 中 state 的情況:
在 Redux 中梢什,store 的作用,與 MVC 中的 Model 類似朝聋,可以將我們項(xiàng)目中的數(shù)據(jù)傳遞給 store嗡午,交給 store 進(jìn)行處理,并可以實(shí)時(shí)通過 store.getState() 獲取到存儲(chǔ)在 store 中的數(shù)據(jù)冀痕。我們對(duì)上面例子的 reducer 及各個(gè)組件的 componentDidMount 做點(diǎn)小修改荔睹,看看 store 的這一個(gè)特性。
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
});
}
});
}
}
我們對(duì)創(chuàng)建 store 時(shí)所傳進(jìn)去的 reducer 進(jìn)行修改言蛇。reducer 中僻他,其參數(shù) state 為當(dāng)前 store 的值,我們對(duì)不同的 action 進(jìn)行處理腊尚,并將處理后的結(jié)果存儲(chǔ)在 state 中并進(jìn)行返回吨拗。此時(shí),通過 store.getState() 獲取到的婿斥,就是我們處理完成后的 state劝篷。
Redux 內(nèi)部的實(shí)現(xiàn),其實(shí)也是基于觀察者模式的民宿,reducer 的調(diào)用結(jié)果娇妓,存儲(chǔ)在 store 內(nèi)部的 state 中,并在每一次 reducer 的調(diào)用中并作為參數(shù)傳入活鹰。所以在 child_1 組件第 2s 的 dispatch 后哈恰,child_2 與 child_2_1 組件通過 subscribe 監(jiān)聽的函數(shù),其通過 getState 獲得的值志群,都包含有 child_2 與 child_2_1 字段的着绷,這就是為什么第 2s 后的響應(yīng),child_2 也進(jìn)行了一次生命周期赖舟。所以在對(duì) subscribe 響應(yīng)后的處理蓬戚,最好還是先校對(duì)通過 getState() 獲取到的 state 與當(dāng)前組件的 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
});
}
});
}
加上這樣的校驗(yàn)宾抓,各個(gè)組件的生命周期的觸發(fā)就符合我們的預(yù)期了子漩。
Redux 對(duì)于組件間的解耦提供了很大的便利,如果你在考慮該不該使用 Redux 的時(shí)候石洗,社區(qū)里有一句話說幢泼,“當(dāng)你不知道該不該使用 Redux 的時(shí)候,那就是不需要的”讲衫。Redux 用起來一時(shí)爽缕棵,重構(gòu)或者將項(xiàng)目留給后人的時(shí)候,就是個(gè)大坑涉兽,Redux 中的 dispatch 和 subscribe 方法遍布代碼的每一個(gè)角落招驴。剛剛的例子不是最好的,F(xiàn)lux 設(shè)計(jì)中的 Controller-Views 概念就是為了解決這個(gè)問題出發(fā)的枷畏,將所有的 subscribe 都置于 Parent 組件(Controller-Views)别厘,由最上層組件控制下層組件的表現(xiàn),然而拥诡,這不就是我們所說的 子組件向父組件通訊 這種方式了触趴。