React源碼分析5 -- 組件通信,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 ../eventProxyclass 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全局變數。使用方法:

  1. 祖父組件中定義getChildContext()方法,將要傳遞給孫子的數據放在其中
  2. 祖父組件中childContextTypes申明要傳遞的數據類型
  3. 孫子組件中contextTypes申明可以接收的數據類型
  4. 孫子組件通過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。


推薦閱讀:

node源碼筆記——模塊載入
React 源碼解析
深度學習一行一行敲cyclegan-tensorflow版(ops.py文件Batch_Norm與instance_norm討論)
深度學習一行一行敲cyclegan-tensorflow版(網路訓練)
Android Framework源碼當中哪些類有必要進行深入學習?

TAG:React | 前端开发 | 源码阅读 |