React元件之間傳值
處理 React 元件之間的交流方式,主要取決於元件之間的關係,然而這些關係的約定人就是你。
我不會講太多關於 data-stores、data-adapters 或者 data-helpers 之類的話題。我下面只專注於 React 元件本身的交流方式的講解。
React 元件之間交流的方式,可以分為以下 3 種:
-
【父元件】向【子元件】傳值;
-
【子元件】向【父元件】傳值;
-
沒有任何巢狀關係的元件之間傳值(PS:比如:兄弟元件之間傳值)
一、【父元件】向【子元件】傳值
初步使用
這個是相當容易的,在使用 React 開發的過程中經常會使用到,主要是利用 props 來進行交流。例子如下:
// 父元件 var MyContainer = React.createClass({ getInitialState: function () { return { checked: true }; }, render: function() { return ( <ToggleButton text="Toggle me" checked={this.state.checked} /> ); } }); // 子元件 var ToggleButton = React.createClass({ render: function () { // 從【父元件】獲取的值 var checked = this.props.checked, text = this.props.text; return ( <label>{text}: <input type="checkbox" checked={checked} /></label> ); } });
進一步討論
如果元件巢狀層次太深,那麼從外到內元件的交流成本就變得很高,通過 props 傳遞值的優勢就不那麼明顯了。(PS:所以我建議儘可能的減少元件的層次,就像寫 HTML 一樣,簡單清晰的結構更惹人愛)
// 父元件 var MyContainer = React.createClass({ render: function() { return ( <Intermediate text="where is my son?" /> ); } }); // 子元件1:中間巢狀的元件 var Intermediate = React.createClass({ render: function () { return ( <Child text={this.props.text} /> ); } }); // 子元件2:子元件1的子元件 var Child = React.createClass({ render: function () { return ( <span>{this.props.text}</span> ); } });
二、【子元件】向【父元件】傳值
接下來,我們介紹【子元件】控制自己的 state 然後告訴【父元件】的點選狀態,然後在【父元件】中展示出來。因此,我們新增一個 change 事件來做互動。
// 父元件
var MyContainer = React.createClass({
getInitialState: function () {
return {
checked: false
};
},
onChildChanged: function (newState) {
this.setState({
checked: newState
});
},
render: function() {
var isChecked = this.state.checked ? 'yes' : 'no';
return (
<div>
<div>Are you checked: {isChecked}</div>
<ToggleButton text="Toggle me"
initialChecked={this.state.checked}
callbackParent={this.onChildChanged}
/>
</div>
);
}
});
// 子元件
var ToggleButton = React.createClass({
getInitialState: function () {
return {
checked: this.props.initialChecked
};
},
onTextChange: function () {
var newState = !this.state.checked;
this.setState({
checked: newState
});
// 這裡要注意:setState 是一個非同步方法,所以需要操作快取的當前值
this.props.callbackParent(newState);
},
render: function () {
// 從【父元件】獲取的值
var text = this.props.text;
// 元件自身的狀態資料
var checked = this.state.checked;
return (
<label>{text}: <input type="checkbox" checked={checked} onChange={this.onTextChange} /></label>
);
}
});
我覺得原文作者用程式碼不是很直觀,接下來我話一個流程走向簡圖來直觀描述一下這個過程:
這樣做其實是依賴 props 來傳遞事件的引用,並通過回撥的方式來實現的,這樣實現不是特別好,但是在沒有任何工具的情況下也是一種簡單的實現方式
這裡會出現一個我們在之前討論的問題,就是元件有多層巢狀的情況下,你必須要一次傳入回撥函式給 props 來實現子元件向父元件傳值或者操作。
Tiny-Tip: React Event System
在 onChange 事件或者其他 React 事件中,你能夠獲取以下東西:
-
【this】:指向你的元件
-
【一個引數】:這個引數是一個 React 合成事件,SyntheticEvent。
React 對所有事件的管理都是自己實現的,與我們之前使用的 onclick、onchange 事件不一樣。從根本上來說,他們都是繫結到 body 上。
document.on('change', 'input[data-reactid=".0.2"]', function () {...});
上面這份程式碼不是來自於 React,只是打一個比方而已。
如果我沒有猜錯的話,React 真正處理一個事件的程式碼如下:
var listenTo = ReactBrowserEventEmitter.listenTo;
...
function putListener(id, registrationName, listener, transaction) {
...
var container = ReactMount.findReactContainerForID(id);
if (container) {
var doc = container.nodeType === ELEMENT_NODE_TYPE ? container.ownerDocument : container;
listenTo(registrationName, doc);
}
...
}
// 在監聽事件的內部,我們能發現如下:
target.addEventListener(eventType, callback, false);
這裡有所有 React 支援的事件:中文文件-事件系統
多個子元件使用同一個回撥的情況
// 父元件
var MyContainer = React.createClass({
getInitialState: function () {
return {
totalChecked: 0
};
},
onChildChanged: function (newState) {
var newToral = this.state.totalChecked + (newState ? 1 : -1);
this.setState({
totalChecked: newToral
});
},
render: function() {
var totalChecked = this.state.totalChecked;
return (
<div>
<div>How many are checked: {totalChecked}</div>
<ToggleButton text="Toggle me"
initialChecked={this.state.checked}
callbackParent={this.onChildChanged}
/>
<ToggleButton text="Toggle me too"
initialChecked={this.state.checked}
callbackParent={this.onChildChanged}
/>
<ToggleButton text="And me"
initialChecked={this.state.checked}
callbackParent={this.onChildChanged}
/>
</div>
);
}
});
// 子元件
var ToggleButton = React.createClass({
getInitialState: function () {
return {
checked: this.props.initialChecked
};
},
onTextChange: function () {
var newState = !this.state.checked;
this.setState({
checked: newState
});
// 這裡要注意:setState 是一個非同步方法,所以需要操作快取的當前值
this.props.callbackParent(newState);
},
render: function () {
// 從【父元件】獲取的值
var text = this.props.text;
// 元件自身的狀態資料
var checked = this.state.checked;
return (
<label>{text}: <input type="checkbox" checked={checked} onChange={this.onTextChange} /></label>
);
}
});
這是非常容易理解的,在父元件中我們增加了一個【totalChecked】來替代之前例子中的【checked】,當子元件改變的時候,使用同一個子元件的回撥函式給父元件返回值。
三、沒有任何巢狀關係的元件之間傳值
如果元件之間沒有任何關係,元件巢狀層次比較深(個人認為 2 層以上已經算深了),或者你為了一些元件能夠訂閱、寫入一些訊號,不想讓元件之間插入一個元件,讓兩個元件處於獨立的關係。對於事件系統,這裡有 2 個基本操作步驟:訂閱(subscribe)/監聽(listen)一個事件通知,併發送(send)/觸發(trigger)/釋出(publish)/傳送(dispatch)一個事件通知那些想要的元件。
下面講介紹 3 種模式來處理事件,你能點選這裡來比較一下它們。
簡單總結一下:
(1) Event Emitter/Target/Dispatcher
特點:需要一個指定的訂閱源
// to subscribe
otherObject.addEventListener(‘click’, function() { alert(‘click!’); });
// to dispatch
this.dispatchEvent(‘click’);
(2) Publish / Subscribe
特點:觸發事件的時候,你不需要指定一個特定的源,因為它是使用一個全域性物件來處理事件(其實就是一個全域性
廣播的方式來處理事件)
// to subscribe
globalBroadcaster.subscribe(‘click’, function() { alert(‘click!’); });
// to dispatch
globalBroadcaster.publish(‘click’);
(3) Signals
特點:與Event Emitter/Target/Dispatcher相似,但是你不要使用隨機的字串作為事件觸發的引用。觸發事件的每一個物件都需要一個確切的名字(就是類似硬編碼類的去寫事件名字),並且在觸發的時候,也必須要指定確切的事件。(看例子吧,很好理解)
// to subscribe
otherObject.clicked.add(function() { alert(‘click’); });
// to dispatch
this.clicked.dispatch();
如果你只想簡單的使用一下,並不需要其他操作,可以用簡單的方式來實現:
// 簡單實現了一下 subscribe 和 dispatch
var EventEmitter = {
_events: {},
dispatch: function (event, data) {
if (!this._events[event]) { // 沒有監聽事件
return;
}
for (var i = 0; i < this._events[event].length; i++) {
this._events[event][i](data);
}
},
subscribe: function (event, callback) {
// 建立一個新事件陣列
if (!this._events[event]) {
this._events[event] = [];
}
this._events[event].push(callback);
}
};
otherObject.subscribe('namechanged', function(data) { alert(data.name); });
this.dispatch('namechanged', { name: 'John' });
如果你想使用 Publish/Subscribe 模型,可以使用:PubSubJS
React 團隊使用的是:js-signals 它基於 Signals 模式,用起來相當不錯。
Events in React
使用 React 事件的時候,必須關注下面兩個方法:
componentDidMount
componentWillUnmount
在處理事件的時候,需要注意:
在 componentDidMount 事件中,如果元件掛載(mounted)完成,再訂閱事件;當元件解除安裝(unmounted)的時候,在 componentWillUnmount 事件中取消事件的訂閱。
(如果不是很清楚可以查閱 React 對生命週期介紹的文件,裡面也有描述。原文中介紹的是 componentWillMount 個人認為應該是掛載完成後訂閱事件,比如Animation這個就必須掛載,並且不能動態的新增,謹慎點更好)
因為元件的渲染和銷燬是由 React 來控制的,我們不知道怎麼引用他們,所以EventEmitter 模式在處理元件的時候用處不大。
pub/sub 模式可以使用,你不需要知道引用。
下面來一個例子:實現有多個 product 元件,點選他們的時候,展示 product 的名字。
(我在例子中引入了之前推薦的 PubSubJS 庫,如果你覺得引入代價太大,也可以手寫一個簡版,還是比較容易的,很好用哈,大家也可以體驗,但是我還是不推薦全域性廣播的方式)
// 定義一個容器
var ProductList = React.createClass({
render: function () {
return (
<div>
<ProductSelection />
<Product name="product 1" />
<Product name="product 2" />
<Product name="product 3" />
</div>
);
}
});
// 用於展示點選的產品資訊容器
var ProductSelection = React.createClass({
getInitialState: function() {
return {
selection: 'none'
};
},
componentDidMount: function () {
this.pubsub_token = PubSub.subscribe('products', function (topic, product) {
this.setState({
selection: product
});
}.bind(this));
},
componentWillUnmount: function () {
PubSub.unsubscribe(this.pubsub_token);
},
render: function () {
return (
<p>You have selected the product : {this.state.selection}</p>
);
}
});
var Product = React.createClass({
onclick: function () {
PubSub.publish('products', this.props.name);
},
render: function() {
return <div onClick={this.onclick}>{this.props.name}</div>;
}
});
ES6: yield and js-csp
(這裡我寫一個簡單的 DEMO 介紹一下這種新的傳遞方式,其實大同小異)
function* list() {
for(var i = 0; i < arguments.length; i++) {
yield arguments[i];
}
return "done.";
}
var o = list(1, 2, 3);
var cur = o.next;
while(!cur.done) {
cur = o.next();
console.log(cur);
}
以上例子來自於屈屈的一篇部落格:ES6 中的生成器函式介紹 屈屈是一個大牛,大家可以經常關注他的部落格。
通常來說,你有一個佇列,物件在裡面都能找到一個引用,在定義的時候鎖住,當發生的時候,立即開啟鎖執行。js-csp 是一種解決辦法,也許以後還會有其他解決辦法。
結尾
在實際應用中,按照實際要解決的需求選擇解決辦法。對於小應用程式,你可以使用 props 和回撥的方法進行元件之間的資料交換。你可以通過 pub/sub 模式,以避免汙染你的元件。在這裡,我們不是在談論資料,只是元件。對於資料的請求、資料的變化等場景,可以使用 Facebook 的 Flux、Relay、GraphQL 來處理,都非常的好用。
文中的每一個例子我都驗證過了,主要使用最原始的引入檔案方式,建立服務使用的 http-server 包,大家也可以嘗試自己來一次。