玩轉 React(七)- 元件之間的資料共享

sarike發表於2017-12-11

上一篇文章 玩轉 React(六)- 處理事件 介紹了在 React 中如何處理使用者事件,以及 React 事件機制與原生 DOM 事件的差異和注意的問題,同時也介紹了事件處理函式中 this 的指向問題以及處理的幾種方式及其優缺點。

大家在閱讀的過程中有任何為題可以給我留言,同時歡迎大家加入玩轉 React 微信群,我的微訊號是 leobaba88,先加我好友,驗證資訊:玩轉 React,然後我會拉你進群。


今天這篇文章要講的內容是關於多個元件之間如何共享資料,或者說是如何通訊的。只有掌握了正確的元件之間通訊的方式,才能在開發互動複雜的前端應用時做到遊刃有餘,所謂正確的方式也就是符合 React 設計理念的方式。使用一個框架時,一定要遵從框架的最佳實踐,人家框架是這樣設計的,你偏要那樣來用,用得不爽還要噴其不好用,那就不應該了。

內容摘要

  • React 中的資料是單向自頂向下傳遞的。
  • 單向資料流與雙向繫結的差異。
  • 最符合 React 理念的元件之間共享資料的方式。
  • 資料唯一來源原則。
  • 一些不好的方式。
  • 先跟 Redux 打個招呼。
  • 其他一些關於元件間通訊的內容(context、ref)。

元件之間通訊的最佳方式

現在我們就來探討下,什麼樣的方式才是 React 中元件之間通訊的正確方式。

在前面的文章中,我們有說過,React 之所以能勝任大型複雜前端專案的開發,是因為其 單向資料流 這一重要特性,單向資料流能讓檢視更新邏輯變得簡單,從原始的對 DOM 操作變為對資料操作,簡單了就容易維護。

React 元件中資料的流動方向是自頂向下的,也就是說在元件樹中,資料只能從父元件以屬性的方式傳遞到子元件,父元件的資料可能是其接收到的屬性,也可能是自身的內部狀態。

有些同學這裡可能會比較困惑,說子元件明明可以通過一個函式屬性將資料傳遞給父元件呀。好多同學甚至因此搞不明白單向資料流和雙向繫結的差異。其實換個角度考慮一下就清楚很多了,既然“資料傳遞”這個詞區分度不夠大,那就換個區分度比較大的說法。我們可以這樣理解,函式屬性是子元件用來通知父元件發生了什麼,它更像是子元件觸發的一個事件,父元件可以依據業務邏輯來選擇如何處理這個事件,它可以更新資料後重新傳遞給子元件,也可以置之不理。

函式屬性(或者說事件)在元件之間通訊過程中是必不可少的,但是切莫讓它影響了大家對單向資料流這一概念的理解。

資料雙向繫結不一樣,在雙向繫結中父元件將資料傳遞給子元件,子元件修改資料後會將資料回傳同步給父元件,父元件是無條件接受的。這裡就不過多去說哪個好哪個差了,有興趣的同學可以自己去體會,懶一點的就堅持學習 React 吧。

狀態提升(Lifting State Up)

既然 React 中的資料是單向自頂向下傳遞的,那麼符合 React 這一特性的元件通訊方式就顯而易見了。

狀態提升的意思是,當元件 A 需要依賴另外一個元件 B 的內部狀態,而他們又不是父子關係時,需要將元件 B 的內部狀態提升到他們公共的祖先元件中管理。這樣他們就都可以通過屬性接收到這份資料了。

當元件 B 需要對資料進行變更時,可以通過函式屬性來通知祖先元件對資料更新,然後重新傳遞給子元件。

唯一資料來源(Single source of truth)

有些同學可能又會迷惑,為什麼多個元件之間必須要共用同一份資料,我可不可以引入一個事件庫,一個元件分發事件,另一個元件註冊相應的事件來接受資料自己維護。

類似的方案五花八門,會有很多,我認為這樣做當然是不好的,會有如下問題:

  1. 破壞了元件的封裝性,易於複用的元件都是相對獨立的,它只需要定義自己需要的資料和行為(函式屬性)即可,我不需要誰幫我分發事件。

  2. 資料傳遞是不連續的,這樣做會增加專案的複雜性,當專案到一定階段後,對這份資料的依賴就變得千絲萬縷、難以維護了。

  3. 相同的資料會有多個副本,需要保證資料同步,在增加專案複雜性的同時也提高了出現BUG的機率。

這是我個人的看法,我也確實有遇到過這種用法,有不同意見大家可以進群交流。

資料唯一來源是官方推薦的資料共享的原則,也是最符合 React 設計理念,與單向資料流特性相輔相成的,希望大家務必遵守。

Redux

Redux 是一個狀態管理庫,它不是專屬於 React 技術棧的,但是跟 React 配合起來相當不錯。

當我們的前端應用規模較小的時候,我們可以不引入任何的狀態管理工具,只需要依據上面說的狀態提升的方式來管理應用狀態即可。為了讓應用的狀態更直觀,你可以將跟元件作為狀態匯流排,來管理整個應用所有的狀態。而且對於小規模的專案是推薦這樣來做的,沒有必要高射炮打蚊子,過渡設計。

但是當前端應用規模變得比較複雜時,我們就需要有類似 Redux 這樣一個來專門進行狀態管理的東西了。它的職責如下:

  1. 維護一個資料倉儲(store)管理整個應用的狀態(state),確保資料的唯一來源。
  2. 可以通過 dispatch 方法分發一個 action,來通知 Redux 需要對資料進行變更。
  3. Redux 接收到 action 後可以依據 action 的型別對 state 進行相應的修改。
  4. 資料跟新後 Redux 會觸發註冊的監聽器(如:更新元件屬性),完成檢視更新。

Redux 跟 React 一起來用,更詳細的介紹可以參考:官方文件,這裡大家可以先簡單瞭解下,在後面關於 React 實戰的文章中也會詳細介紹 Redux 的使用。

類似的狀態管理工具還有:MobxJS,感興趣的同學也可以瞭解下。

關於元件通訊的其他內容

在 React 中還有一些其他的與元件間通訊相關的知識,這裡也順便跟大家介紹下。

context

首先說一下,這是一個不推薦使用的特性,React 官方有明確說明,這是一個實驗性的API,可能會在後面的版本中去掉這個東西。所以我是從來不用的,呵呵!

context 的作用是啥呢,當大家有過 React 實戰經驗時,很容易遇到這種場景,如果元件的層級組織得不合適,可能會巢狀的非常深,當底層的一個元件需要使用頂層一個元件的資料時,需要通過屬性一層層傳遞下去,非常繁瑣。

context 就是解決這個問題的,只需要在頂層元件中宣告 context,那它的所有子元件可以通過 this.context 直接獲取得到。如下例項所示:

import React from 'react';
import PropTypes from 'prop-types';

class Button extends React.Component {
  render() {
    return (
      <button style={{background: this.context.color}}>
        {this.props.children}
      </button>
    );
  }
}

Button.contextTypes = {
  color: PropTypes.string
};

class Message extends React.Component {
  render() {
    return (
      <div>
        {this.props.text} <Button>Delete</Button>
      </div>
    );
  }
}

class MessageList extends React.Component {
  getChildContext() {
    return {color: "purple"};
  }

  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

MessageList.childContextTypes = {
  color: PropTypes.string
};
複製程式碼

例項中,元件層級關係是:MessageList -> Message -> Button。

MessageList 元件中維護一個 color 值用於 Button 元件的背景色,一般情況下我們需要將 color 以屬性的方式傳給 Message 元件,再通過屬性傳給 Button 元件。然後在例項中,通過 React 的 context 功能,MessageList 可以將 color 的值越過 Message 直接傳給 Button。

是不是很方便?確實很方便,但是這會導致資料傳遞不連續,過度使用會使得專案邏輯變得不直觀,增加專案維護的複雜性。

ref

每一個 React 元件有一個特殊的屬性 ref,該屬性的值可以是一個字串,也可以是一個函式。由於字串形式的 ref 在內部實現和實際使用中存在諸多問題,官方不推薦使用,而且可能在未來的版本中會移除,所以我們也沒必要聊它了,只要大家在看到字串形式的 ref 屬性時知道也有這種用法就可以了。

ref 屬性值是一個函式時,如果元件是一個 HTML 元素相容的 React 內部元件時(如:div、img 等),函式接收其對應的原生 DOM 節點作為引數。**如果元件是一個我們以類的方式定義的元件時,函式接收該元件類的例項作為引數。**需要注意的是,如果元件是一個以函式的方式定義的元件,那麼設定為 ref 值得函式永遠都會接收到一個 null

那麼 ref 與元件之間的通訊有什麼關係呢?請看上段文字加粗內容和下面這個例項:

class UserForm extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      name: null,
      age: null
    }
  }
  formData() {
    return this.state
  }
  handleFieldChange(e) {
    const { name, value } = e.target
    this.setState({
      [name]: value
    })
  }
  render() {
    return (
      <div>
        <input
          type="text"
          name="name"
          placeholder="Name"
          onChange={e => this.handleFieldChange(e)}
        />
        <input
          type="text"
          name="age"
          placeholder="Age"
          onChange={e => this.handleFieldChange(e)}
        />
      </div>
    )
  }
}
class App extends React.Component {
  handleSubmit() {
    const formData = this.form.formData()
    alert(`formData: ${JSON.stringify(formData)}`)
  }
  render() {
    return (
      <div>
        <UserForm ref={form => {this.form = form}} />
        <button onClick={() => this.handleSubmit()}>Submit</button>
      </div>
    )
  }
}

ReactDOM.render(<App />, document.querySelector('#root'))
複製程式碼

演示地址:https://codepen.io/Sarike/pen/OOKYXJ

既然通過 ref 能夠獲取子元件的例項,那麼我們自然可以呼叫其成員方法,從而獲取資料。

當然,目前這確實能工作,但絕對不是一種好的方式。因為作為一個元件,是需要有一定的封裝性的,它應該對外只會承諾我接受什麼樣的屬性,而不會承諾有什麼樣的成員方法。換句話說,如果 JavaScript 的類支援私有成員方法,那麼 React 元件類中的成員方法都應該定義成私有的。

這應該屬於一種 Hack 的使用方式,而且這樣做有悖單向資料流原則。

ref 有它自己的使用場景,這裡只是說明這種方式不適用於元件之間通訊。

總結

雖然囉嗦了這麼多,實際上只希望大家知道一件事情,請使用狀態提升的方式在多個元件之間共享資料,切記維持應用單向資料流和資料唯一來源原則。

文章中有些觀點仁者見仁,有什麼疑惑歡迎留言討論。

好久沒更新了,但是沒有放棄,感謝大家支援。歡迎加我微信好友:leobaba88,進群交流。驗證資訊:玩轉 React


PS:本系列的所有文章將在 segmentfault 和 掘金 同步釋出。

本作品保留所有權利。未獲得許可人許可前,不允許他人複製、發行、展覽和表演作品。不允許他人基於該作品創作演繹作品 。

相關文章