1. 程式人生 > >React原始碼分析6 -- 元件通訊,refs,key,ReactDOM

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全域性變數。使用方法:

  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。