React原始碼分析6 -- 元件通訊,refs,key,ReactDOM
1 元件間通訊
父元件向子元件通訊
React規定了明確的單向資料流,利用props將資料從父元件傳遞給子元件。故我們可以利用props,讓父元件給子元件通訊。故父元件向子元件通訊還是很容易實現的。引申一點,父元件怎麼向孫子元件通訊呢?可以利用props進行層層傳遞,使用ES6的…運算子可以用很簡潔的方式把props傳遞給孫子元件。這裡我們就不舉例了。
要注意的一點是,setProps,replaceProps兩個API已經被廢棄了,React建議我們在頂層使用ReactDOM.reader()進行props更新。
子元件向父元件通訊
React資料流是單向的,只能從父元件傳遞到子元件。那麼子元件怎麼向父元件通訊呢?其實仍然可以利用props。父元件利用props傳遞方法給子元件,子元件回撥這個方法的同時,將資料傳遞進去,使得父元件的相關方法得到回撥,這個時候就可以把資料從子元件傳遞給父元件了。看一個例子。
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() {
// 子元件中呼叫父元件的方法,將資料以引數的方式傳遞給父元件,這樣父元件方法就得到回調了,也收到資料了
this.props.transferMsg("child has mounted");
}
render() {
return (
<div >child</div>
)
}
}
這個例子應該很清楚了,通過回撥的方式,可以將資料從子元件傳遞給父元件。引申一下,孫子元件怎麼把資料傳遞給父元件呢?同樣可以利用props層層回撥。利用ES6的…運算子也可以用比較簡潔的方式完成props層層回撥。
兄弟元件通訊 — 釋出/訂閱
兄弟元件可以利用父元件進行中轉,將資料先由child1傳給parent,然後parent傳給child2. 這個方法顯然耦合比較嚴重,傳遞次數過多,容易引發父元件不必要的生命週期回撥,甚至影響其他子元件,故強烈建議不要使用這個方式。
我們可以利用觀察者模式來解決這個問題。觀察者模式採用釋出/訂閱的方法,可以將訊息傳送者和接收者完美解耦。React中可以引入eventProxy模組,利用eventProxy.trigger()方法釋出訊息,eventProxy.on()方法監聽並接收訊息。eventProxy我們就不展開講了。下面看一個例子
import eventProxy from '../eventProxy'
class Child1 extends React.Component {
componentDidMount() {
// 釋出者,發出訊息
eventProxy.trigger('msg', 'child1 has been mounted');
}
render() {
return (
<div>child1</div>
);
}
}
class Child2 extends React.Component {
componentDidMount() {
// 訂閱者,監聽並接收訊息
eventProxy.on('msg', (msg) => {console.log('msg: ' + msg)});
}
render() {
return (
<div>child2</div>
);
}
}
巢狀層級深元件 — context
祖父元件和孫子元件通訊時,我們有時候還是覺得通過props有點繁瑣了。此時可以考慮使用context全域性變數。使用方法:
- 祖父元件中定義getChildContext()方法,將要傳遞給孫子的資料放在其中
- 祖父元件中childContextTypes申明要傳遞的資料型別
- 孫子元件中contextTypes申明可以接收的資料型別
- 孫子元件通過this.context訪問祖父傳遞進來的資料。
採用全域性變數的方式,容易導致資料混亂,分不清資料是從哪兒來的,不容易控制。建議少用這種方式。
Redux
還可以利用Flux和Redux架構來進行元件通訊,這個我們以後再專門詳細分析。
2 refs
用法
我們在getRender()返回的JSX中,可以在標籤中加入ref屬性,然後通過refs.ref就可以訪問到我們的Component了,例如。
class Parent extends React.Component {
getRender() {
<div>
<Child ref = 'child' />
</div>
}
componentDidMount() {
// 通過refs可以拿到子元素,然後就可以訪問到子元素的方法了
let child = this.refs.child;
child.test();
}
}
class Child extends React.Component {
test() {
console.log("child method called by ref");
}
}
attachRef 將子元件引用儲存到父元件refs物件中
refs的用法很簡單,只需要JSX中定義好ref屬性即可。那麼首先一個問題來了,refs這個物件在哪兒定義的呢?還記得createClass方法的constructor吧,它裡面會定義並初始化refs物件。原始碼如下
createClass: function (spec) {
// 自定義React類的構造方法,通過它建立一個React.Component物件
var Constructor = identity(function (props, context, updater) {
// Wire up auto-binding
if (this.__reactAutoBindPairs.length) {
bindAutoBindMethods(this);
}
this.props = props;
this.context = context;
// refs初始化為一個空物件
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
// 呼叫getInitialState初始化state
this.state = null;
var initialState = this.getInitialState ? this.getInitialState() : null;
this.state = initialState;
});
...
}
從上面程式碼可見,每次建立自定義元件的時候,都會初始化一個為空的refs物件。那麼第二個問題來了,ref字串所指向的物件的引用,是什麼時候加入到refs物件中的呢?答案就在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物件中
refs[ref] = publicComponentInstance;
},
attachRef方法又是什麼時候被呼叫的呢?我們這兒就不原始碼分析了。大概說下,mountComponent中,如果element的ref屬性不為空,則會以transaction事務的方式呼叫attachRefs方法,而attachRefs方法中則會呼叫attachRef方法,將子元件的引用儲存到父元件的refs物件中。
detachRef 從父元件refs物件中刪除子元件引用
對記憶體管理有些瞭解的同學肯定會有疑惑,既然父元件的refs中儲存了子元件引用,那麼當子元件被unmountComponent而銷燬時,子元件的引用仍然儲存在refs物件中,豈不是會導致記憶體洩漏?React當然不會有這個bug了,祕密就在detachRef方法中,原始碼如下
detachRef: function(ref) {
var refs = this.getPublicInstance().refs;
// 從refs物件中刪除key為ref子元素,防止記憶體洩漏
delete refs[ref];
},
程式碼很簡單,delete掉ref字串指向的成員即可。至於detachRef的呼叫鏈,我們還得從unmountComponent方法說起。unmountComponent會呼叫detachRefs方法,而detachRefs中則會呼叫detachRef,從而將子元素引用從refs中釋放掉,防止記憶體洩漏。也就是說在unmountComponent時,React自動幫我們完成了子元素ref刪除,防止記憶體洩漏。
3 key
當我們的子元件是一個數組時,比如類似於Android中的ListView,一個列表中有很多樣式一致的項,此時給每個項加上key這個屬性就很有作用了。key可以標示當前項的唯一性。
對於陣列,其內部包含長度不確定的子項。當元件state變化時,需要重新渲染元件。那麼有個問題來了,React是更新元件,還是先銷燬再新建元件呢。key就是用來解決這個問題的。如果前後兩次key不變,則只需要更新,否則先銷燬再更新。
對於子項的key,必須是唯一不重複的。並且儘量傳不變的屬性,千萬不要傳無意義的index或者隨機值。這樣才能儘量以更新的方式來重新渲染。React原始碼中判斷更新方式的原始碼如下
function shouldUpdateReactComponent(prevElement, nextElement) {
// 前後兩次ReactElement中任何一個為null,則必須另一個為null才返回true。這種情況一般不會碰到
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') {
// 如果前後兩次為數字或者字元,則認為只需要update(處理文字元素),返回true
return (nextType === 'string' || nextType === 'number');
} else {
// 如果前後兩次為DOM元素或React元素,則必須type和key不變(key用於listView等元件,很多時候我們沒有設定key,故只需type相同)才update,否則先unmount再重新mount。返回false
return (
nextType === 'object' &&
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
);
}
}
看到key這個屬性的重要性了吧。對於陣列元件,我們一定要在每個子項上設定一個key,這樣可以大大提高DOM diff的效能。
那為什麼陣列元件之外的其他元件,不用設定key呢?因為他們的type或者在父元件中的位置不同,完全可以區分開,所以不需要key就可以完全確定是哪個元件了。
4 無狀態元件
無狀態元件其實本質上就是一個函式,傳入props即可,沒有state,也沒有生命週期方法。元件本身對應的就是render方法。例子如下
function Title({color = 'red', text = '標題'}) {
let style = {
'color': color
}
return (
<div style = {style}>{text}</div>
)
}
無狀態元件不會建立物件,故比較省記憶體。沒有複雜的生命週期方法呼叫,故流程比較簡單。沒有state,也不會重複渲染。它本質上就是一個函式而已。
對於沒有狀態變化的元件,React建議我們使用無狀態元件。總之,能用無狀態元件的地方,就用無狀態元件。
5 React DOM
React通過findDOMNode()可以找到元件例項對應的DOM節點,但需要注意的是,我們只能在render()之後,也就是componentDidMount()和componentDidUpdate()中呼叫。因為只有render後,DOM物件才生成了。
class example extends React.Component {
componentDidMount() {
// 只有render後才生成了DOM node,才能呼叫findDOMNode
let dom = ReactDOM.findDOMNode(this);
}
}
那為什麼render後DOM才生成呢,我們可以從原始碼角度來分析。React原始碼分析3 — React元件插入DOM流程一文中,我們知道mountComponent解析得到了markup,也就是React元件對應的HTML,會由_mountImageIntoNode方法插入到真實DOM中,故這個事務結束後,才生成了真正的DOM。故肯定只有render之後,才有真實的DOM可以被訪問。
那為什麼componentDidMount()能訪問DOM呢?它不是也在mountComponent()方法流程中嗎?這是因為React採用非同步事務的方式來呼叫componentDidMount的,它把componentDidMount放到一個事務佇列中,只有當前mountComponent這個事務處理完了,才會回過頭去處理componentDidMount,故在componentDidMount中可以拿到真實的DOM。這個設計得給React點贊。這一點可以從原始碼來分析。
mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
// 省略一段程式碼
...
if (inst.componentDidMount) {
// 呼叫componentDidMount,以事務的形式。放到queue中,非同步的方式,有那麼點Android MessageQueue的感覺
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}
return markup;
},
另外值得注意的是,React不建議我們碰底層的DOM,因為React有一套效能比較高的DOM diff方式來更新真實DOM。並且容易導致DOM引用忘記釋放等記憶體洩漏問題。一句話,除非不得已,不要碰DOM。