React組件通信,refs,key,ReactDOM

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。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容