1 組件間通信
父組件向子組件通信
React規(guī)定了明確的單向數(shù)據(jù)流,利用props將數(shù)據(jù)從父組件傳遞給子組件。故我們可以利用props,讓父組件給子組件通信。故父組件向子組件通信還是很容易實(shí)現(xiàn)的。引申一點(diǎn),父組件怎么向?qū)O子組件通信呢?可以利用props進(jìn)行層層傳遞,使用ES6的...運(yùn)算符可以用很簡(jiǎn)潔的方式把props傳遞給孫子組件。這里我們就不舉例了。
要注意的一點(diǎn)是,setProps,replaceProps兩個(gè)API已經(jīng)被廢棄了,React建議我們?cè)陧攲邮褂肦eactDOM.reader()進(jìn)行props更新。
React數(shù)據(jù)流是單向的,只能從父組件傳遞到子組件。那么子組件怎么向父組件通信呢?其實(shí)仍然可以利用props。父組件利用props傳遞方法給子組件,子組件回調(diào)這個(gè)方法的同時(shí),將數(shù)據(jù)傳遞進(jìn)去,使得父組件的相關(guān)方法得到回調(diào),這個(gè)時(shí)候就可以把數(shù)據(jù)從子組件傳遞給父組件了。看一個(gè)例子。
class Parent extends React.Component {
handleChildMsg(msg) {
// 父組件處理消息
console.log("parent: " + msg);
}
render() {
return (
<div>
<Child transferMsg = {msg => this.handleChildMsg(msg)} />
</div>
);
}
}
class Child extends React.Component {
componentDidMount() {
// 子組件中調(diào)用父組件的方法,將數(shù)據(jù)以參數(shù)的方式傳遞給父組件,這樣父組件方法就得到回調(diào)了,也收到數(shù)據(jù)了
this.props.transferMsg("child has mounted");
}
render() {
return (
<div>child</div>
)
}
}
這個(gè)例子應(yīng)該很清楚了,通過(guò)回調(diào)的方式,可以將數(shù)據(jù)從子組件傳遞給父組件。引申一下,孫子組件怎么把數(shù)據(jù)傳遞給父組件呢?同樣可以利用props層層回調(diào)。利用ES6的...運(yùn)算符也可以用比較簡(jiǎn)潔的方式完成props層層回調(diào)。
兄弟組件通信 — 發(fā)布/訂閱
兄弟組件可以利用父組件進(jìn)行中轉(zhuǎn),將數(shù)據(jù)先由child1傳給parent,然后parent傳給child2. 這個(gè)方法顯然耦合比較嚴(yán)重,傳遞次數(shù)過(guò)多,容易引發(fā)父組件不必要的生命周期回調(diào),甚至影響其他子組件,故強(qiáng)烈建議不要使用這個(gè)方式。
我們可以利用觀察者模式來(lái)解決這個(gè)問(wèn)題。觀察者模式采用發(fā)布/訂閱的方法,可以將消息發(fā)送者和接收者完美解耦。React中可以引入eventProxy模塊,利用eventProxy.trigger()方法發(fā)布消息,eventProxy.on()方法監(jiān)聽(tīng)并接收消息。eventProxy我們就不展開(kāi)講了。下面看一個(gè)例子
import eventProxy from '../eventProxy'
class Child1 extends React.Component {
componentDidMount() {
// 發(fā)布者,發(fā)出消息
eventProxy.trigger('msg', 'child1 has been mounted');
}
render() {
return (
<div>child1</div>
);
}
}
class Child2 extends React.Component {
componentDidMount() {
// 訂閱者,監(jiān)聽(tīng)并接收消息
eventProxy.on('msg', (msg) => {console.log('msg: ' + msg)});
}
render() {
return (
<div>child2</div>
);
}
}
嵌套層級(jí)深組件 — context
祖父組件和孫子組件通信時(shí),我們有時(shí)候還是覺(jué)得通過(guò)props有點(diǎn)繁瑣了。此時(shí)可以考慮使用context全局變量。使用方法:
祖父組件中定義getChildContext()方法,將要傳遞給孫子的數(shù)據(jù)放在其中
祖父組件中childContextTypes申明要傳遞的數(shù)據(jù)類型
孫子組件中contextTypes申明可以接收的數(shù)據(jù)類型
孫子組件通過(guò)this.context訪問(wèn)祖父?jìng)鬟f進(jìn)來(lái)的數(shù)據(jù)。
采用全局變量的方式,容易導(dǎo)致數(shù)據(jù)混亂,分不清數(shù)據(jù)是從哪兒來(lái)的,不容易控制。建議少用這種方式。
Redux
2 refs
attachRef 將子組件引用保存到父組件refs對(duì)象中
refs的用法很簡(jiǎn)單,只需要JSX中定義好ref屬性即可。那么首先一個(gè)問(wèn)題來(lái)了,refs這個(gè)對(duì)象在哪兒定義的呢?還記得createClass方法的constructor吧,它里面會(huì)定義并初始化refs對(duì)象。源碼如下
createClass: function (spec) {
// 自定義React類的構(gòu)造方法,通過(guò)它創(chuàng)建一個(gè)React.Component對(duì)象
var Constructor = identity(function (props, context, updater) {
// Wire up auto-binding
if (this.__reactAutoBindPairs.length) {
bindAutoBindMethods(this);
}
this.props = props;
this.context = context;
// refs初始化為一個(gè)空對(duì)象
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
// 調(diào)用getInitialState初始化state
this.state = null;
var initialState = this.getInitialState ? this.getInitialState() : null;
this.state = initialState;
});
...
}
從上面代碼可見(jiàn),每次創(chuàng)建自定義組件的時(shí)候,都會(huì)初始化一個(gè)為空的refs對(duì)象。那么第二個(gè)問(wèn)題來(lái)了,ref字符串所指向的對(duì)象的引用,是什么時(shí)候加入到refs對(duì)象中的呢?答案就在ReactCompositeComponent的attachRef方法中,源碼如下
attachRef: function(ref, component) {
// getPublicInstance返回我們的父組件
var inst = this.getPublicInstance();
var publicComponentInstance = component.getPublicInstance();
var refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
// 將子元素的引用,以ref屬性為key,保存到父元素的refs對(duì)象中
refs[ref] = publicComponentInstance;
},
attachRef方法又是什么時(shí)候被調(diào)用的呢?我們這兒就不源碼分析了。大概說(shuō)下,mountComponent中,如果element的ref屬性不為空,則會(huì)以transaction事務(wù)的方式調(diào)用attachRefs方法,而attachRefs方法中則會(huì)調(diào)用attachRef方法,將子組件的引用保存到父組件的refs對(duì)象中。
detachRef 從父組件refs對(duì)象中刪除子組件引用
對(duì)內(nèi)存管理有些了解的同學(xué)肯定會(huì)有疑惑,既然父組件的refs中保存了子組件引用,那么當(dāng)子組件被unmountComponent而銷毀時(shí),子組件的引用仍然保存在refs對(duì)象中,豈不是會(huì)導(dǎo)致內(nèi)存泄漏?React當(dāng)然不會(huì)有這個(gè)bug了,秘密就在detachRef方法中,源碼如下
detachRef: function(ref) {
var refs = this.getPublicInstance().refs;
// 從refs對(duì)象中刪除key為ref子元素,防止內(nèi)存泄漏
delete refs[ref];
},
代碼很簡(jiǎn)單,delete掉ref字符串指向的成員即可。至于detachRef的調(diào)用鏈,我們還得從unmountComponent方法說(shuō)起。unmountComponent會(huì)調(diào)用detachRefs方法,而detachRefs中則會(huì)調(diào)用detachRef,從而將子元素引用從refs中釋放掉,防止內(nèi)存泄漏。也就是說(shuō)在unmountComponent時(shí),React自動(dòng)幫我們完成了子元素ref刪除,防止內(nèi)存泄漏。
3 key
當(dāng)我們的子組件是一個(gè)數(shù)組時(shí),比如類似于Android中的ListView,一個(gè)列表中有很多樣式一致的項(xiàng),此時(shí)給每個(gè)項(xiàng)加上key這個(gè)屬性就很有作用了。key可以標(biāo)示當(dāng)前項(xiàng)的唯一性。
對(duì)于數(shù)組,其內(nèi)部包含長(zhǎng)度不確定的子項(xiàng)。當(dāng)組件state變化時(shí),需要重新渲染組件。那么有個(gè)問(wèn)題來(lái)了,React是更新組件,還是先銷毀再新建組件呢。key就是用來(lái)解決這個(gè)問(wèn)題的。如果前后兩次key不變,則只需要更新,否則先銷毀再更新。
對(duì)于子項(xiàng)的key,必須是唯一不重復(fù)的。并且盡量傳不變的屬性,千萬(wàn)不要傳無(wú)意義的index或者隨機(jī)值。這樣才能盡量以更新的方式來(lái)重新渲染。React源碼中判斷更新方式的源碼如下
function shouldUpdateReactComponent(prevElement, nextElement) {
// 前后兩次ReactElement中任何一個(gè)為null,則必須另一個(gè)為null才返回true。這種情況一般不會(huì)碰到
var prevEmpty = prevElement === null || prevElement === false;
var nextEmpty = nextElement === null || nextElement === false;
if (prevEmpty || nextEmpty) {
return prevEmpty === nextEmpty;
}
var prevType = typeof prevElement;
var nextType = typeof nextElement;
// React DOM diff算法
if (prevType === 'string' || prevType === 'number') {
// 如果前后兩次為數(shù)字或者字符,則認(rèn)為只需要update(處理文本元素),返回true
return (nextType === 'string' || nextType === 'number');
} else {
// 如果前后兩次為DOM元素或React元素,則必須type和key不變(key用于listView等組件,很多時(shí)候我們沒(méi)有設(shè)置key,故只需type相同)才update,否則先unmount再重新mount。返回false
return (
nextType === 'object' &&
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
);
}
}
看到key這個(gè)屬性的重要性了吧。對(duì)于數(shù)組組件,我們一定要在每個(gè)子項(xiàng)上設(shè)置一個(gè)key,這樣可以大大提高DOM diff的性能。
那為什么數(shù)組組件之外的其他組件,不用設(shè)置key呢?因?yàn)樗麄兊膖ype或者在父組件中的位置不同,完全可以區(qū)分開(kāi),所以不需要key就可以完全確定是哪個(gè)組件了。
4 React DOM
React通過(guò)findDOMNode()可以找到組件實(shí)例對(duì)應(yīng)的DOM節(jié)點(diǎn),但需要注意的是,我們只能在render()之后,也就是componentDidMount()和componentDidUpdate()中調(diào)用。因?yàn)橹挥衦ender后,DOM對(duì)象才生成了。
class example extends React.Component {
componentDidMount() {
// 只有render后才生成了DOM node,才能調(diào)用findDOMNode
let dom = ReactDOM.findDOMNode(this);
}
}
那為什么render后DOM才生成呢,我們可以從源碼角度來(lái)分析。React源碼分析3 — React組件插入DOM流程一文中,我們知道m(xù)ountComponent解析得到了markup,也就是React組件對(duì)應(yīng)的HTML,會(huì)由_mountImageIntoNode方法插入到真實(shí)DOM中,故這個(gè)事務(wù)結(jié)束后,才生成了真正的DOM。故肯定只有render之后,才有真實(shí)的DOM可以被訪問(wèn)。
那為什么componentDidMount()能訪問(wèn)DOM呢?它不是也在mountComponent()方法流程中嗎?這是因?yàn)镽eact采用異步事務(wù)的方式來(lái)調(diào)用componentDidMount的,它把componentDidMount放到一個(gè)事務(wù)隊(duì)列中,只有當(dāng)前mountComponent這個(gè)事務(wù)處理完了,才會(huì)回過(guò)頭去處理componentDidMount,故在componentDidMount中可以拿到真實(shí)的DOM。這個(gè)設(shè)計(jì)得給React點(diǎn)贊。這一點(diǎn)可以從源碼來(lái)分析。
mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
// 省略一段代碼
...
if (inst.componentDidMount) {
// 調(diào)用componentDidMount,以事務(wù)的形式。放到queue中,異步的方式,有那么點(diǎn)Android MessageQueue的感覺(jué)
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}
return markup;
},
另外值得注意的是,React不建議我們碰底層的DOM,因?yàn)镽eact有一套性能比較高的DOM diff方式來(lái)更新真實(shí)DOM。并且容易導(dǎo)致DOM引用忘記釋放等內(nèi)存泄漏問(wèn)題。一句話,除非不得已,不要碰DOM。