React系列八:非父子組件通訊

快來加入咱們吧!

"小和山的菜鳥們",爲前端開發者提供技術相關資訊以及系列基礎文章。爲更好的用戶體驗,請您移至咱們官網小和山的菜鳥們 ( xhs-rookies.com/ ) 進行學習,及時獲取最新文章。前端

"Code tailor" ,若是您對咱們文章感興趣、或是想提一些建議,微信關注 「小和山的菜鳥們」 公衆號,與咱們取的聯繫,您也能夠在微信上觀看咱們的文章。每個建議或是贊同都是對咱們極大的鼓勵!web

前言

這節咱們將介紹 React 中非父子組件的通訊,上節咱們說到父子組件間的通訊可經過 props 和回調函數完成,但隨着應用程序愈來愈大,使用 props 和回調函數的方式就變得很是繁瑣了,那麼非父子組件間的組件通訊,有沒有一種簡單的方法呢?微信

本文會向你介紹如下內容:markdown

  • 跨級組件間的通訊
  • Context
  • 兄弟組件通訊

跨級組件間的通訊

Context

Context 提供了一個無需爲每層組件手動添加 props,就能在組件樹間進行數據傳遞的方法ide

Context 的使用場景

  • 對於有一些場景:好比一些數據須要在多個組件中進行共享(地區偏好、UI 主題、用戶登陸狀態、用戶信息等)。
  • 若是咱們在頂層的 App 中定義這些信息,層層傳遞下去,對於一些中間層不須要數據的組件來講,這是一種冗餘的操做。

image.png

若是層級更多的話,一層層傳遞是很是麻煩,而且代碼是很是冗餘的:函數

  • React 提供了一個 API:Context
  • Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props
  • Context 設計目的是爲了共享那些對於一個組件樹而言是「全局」的數據,例如當前認證的用戶、主題或首選語言;

Context 相關的 API

React.createContext
const MyContext = React.createContext(defaultValue)
複製代碼

建立一個須要共享的 Context 對象:oop

  • 若是一個組件訂閱了 Context,那麼這個組件會從離自身最近的那個匹配的 Provider 中讀取到當前的context 值;
  • 只有當組件所處的樹中沒有匹配到 Provider 時,其 defaultValue 參數纔會生效。defaultValue 是組件在頂層查找過程當中沒有找到對應的Provider,那麼就使用默認值

注意:undefined 傳遞給 Provider 的 value 時,消費組件的 defaultValue 不會生效。組件化

Context.Provider
<MyContext.Provider value={/* 某個值 */}>
複製代碼

每一個 Context 對象都會返回一個 Provider React 組件,它容許消費組件訂閱 context 的變化:學習

  • Provider 接收一個 value 屬性,傳遞給消費組件;
  • 一個 Provider 能夠和多個消費組件有對應關係;
  • 多個 Provider 也能夠嵌套使用,裏層的會覆蓋外層的數據;

當 Provider 的 value 值發生變化時,它內部的全部消費組件都會從新渲染;優化

Class.contextType
class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context
    /* 在組件掛載完成後,使用 MyContext 組件的值來執行一些有反作用的操做 */
  }
  componentDidUpdate() {
    let value = this.context
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context
    /* ... */
  }
  render() {
    let value = this.context
    /* 基於 MyContext 組件的值進行渲染 */
  }
}
MyClass.contextType = MyContext
複製代碼

掛載在 class 上的 contextType 屬性會被重賦值爲一個由 React.createContext() 建立的 Context 對象:

  • 這能讓你使用 this.context 來消費最近 Context 上的那個值;
  • 你能夠在任何生命週期中訪問到它,包括 render 函數中;
Context.Consumer
<MyContext.Consumer>
  {value => /* 基於 context 值進行渲染*/}
</MyContext.Consumer>
複製代碼

這裏,React 組件也能夠訂閱到 context 變動。這能讓你在 函數式組件 中完成訂閱 context

  • 這裏須要 函數做爲子元素(function as child)這種作法;
  • 這個函數接收當前的 context 值,返回一個 React 節點;

Context 使用

舉個例子,在下面的代碼中,咱們經過一個 「theme」 屬性手動調整一個按鈕組件的樣式:

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />
  }
}

function Toolbar(props) {
  // Toolbar 組件接受一個額外的「theme」屬性,而後傳遞給 ThemedButton 組件。
  // 若是應用中每個單獨的按鈕都須要知道 theme 的值,這會是件很麻煩的事,
  // 由於必須將這個值層層傳遞全部組件。
  return (
    <div> <ThemedButton theme={props.theme} /> </div>
  )
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />
  }
}
複製代碼

使用 context, 咱們能夠避免經過中間元素傳遞 props

// Context 可讓咱們無須明確地傳遍每個組件,就能將值深刻傳遞進組件樹。
// 爲當前的 theme 建立一個 context(「light」爲默認值)。
const ThemeContext = React.createContext('light')
class App extends React.Component {
  render() {
    // 使用一個 Provider 來將當前的 theme 傳遞給如下的組件樹。
    // 不管多深,任何組件都能讀取這個值。
    // 在這個例子中,咱們將 「dark」 做爲當前的值傳遞下去。
    return (
      <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider>
    )
  }
}

// 中間的組件不再必指明往下傳遞 theme 了。
function Toolbar() {
  return (
    <div> <ThemedButton /> </div>
  )
}

class ThemedButton extends React.Component {
  // 指定 contextType 讀取當前的 theme context。
  // React 會往上找到最近的 theme Provider,而後使用它的值。
  // 在這個例子中,當前的 theme 值爲 「dark」。
  static contextType = ThemeContext
  render() {
    return <Button theme={this.context} />
  }
}
複製代碼

兄弟組件通訊

兄弟組件即他們擁有共同的父組件!

而在講兄弟組件以前咱們先要講到一個概念:狀態提高

狀態提高 :在 React 中,將多個組件中須要共享的 state 向上移動到它們的最近共同父組件中,即可實現共享 state。這就是所謂的 狀態提高

簡單例子

接下來經過一個例子幫助你們深入理解:

咱們將從一個名爲 BoilingVerdict 的組件開始,它接受 celsius 溫度做爲一個 prop,並據此打印出該溫度是否足以將水煮沸的結果。

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>
  }
  return <p>The water would not boil.</p>
}
複製代碼

接下來, 咱們建立一個名爲 Calculator 的組件。它渲染一個用於輸入溫度的 <input>,並將其值保存在 this.state.temperature 中。

另外, 它根據當前輸入值渲染 BoilingVerdict 組件。

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.state = {temperature: ''};
  }

  handleChange(e) {
    this.setState({temperature: e.target.value});
  }

  render() {
    const temperature = this.state.temperature;
    return (
        <p>Enter temperature in Celsius:</p>
        <input value={temperature} onChange={e => this.handleChange(e)} />
        <BoilingVerdict celsius={parseFloat(temperature)} />
    );
  }
}
複製代碼

image.png

image.png

添加第二個輸入框

如今的新需求是,在已有攝氏溫度輸入框的基礎上,咱們提供華氏度的輸入框,並保持兩個輸入框的數據同步。

咱們先從 Calculator 組件中抽離出 TemperatureInput 組件,而後爲其添加一個新的 scale prop,它能夠是 "c" 或是 "f":(表明攝氏溫度和華氏溫度)

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit',
}

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
    this.state = { temperature: '' }
  }

  handleChange(e) {
    this.setState({ temperature: e.target.value })
  }

  render() {
    const temperature = this.state.temperature
    const scale = this.props.scale
    return (
      <fieldset> <legend>Enter temperature in {scaleNames[scale]}:</legend> <input value={temperature} onChange={this.handleChange} /> </fieldset>
    )
  }
}
複製代碼

咱們如今能夠修改 Calculator 組件讓它渲染兩個獨立的溫度輸入框組件:

class Calculator extends React.Component {
  render() {
    return (
      <div> <TemperatureInput scale="c" /> <TemperatureInput scale="f" /> </div>
    )
  }
}
複製代碼

image.png

咱們如今有了兩個輸入框,但當你在其中一個輸入溫度時,另外一個並不會更新。這與咱們的要求相矛盾:咱們但願讓它們保持同步。

另外,咱們也不能經過 Calculator 組件展現 BoilingVerdict 組件的渲染結果。由於 Calculator 組件並不知道隱藏在 TemperatureInput 組件中的當前溫度是多少。

狀態提高

到目前爲止, 兩個 TemperatureInput 組件均在各自內部的 state 中相互獨立地保存着各自的數據。

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {temperature: ''};
  }

  handleChange(e) {
    this.setState({temperature: e.target.value});
  }

  render() {
    const temperature = this.state.temperature;
    // ...
複製代碼

然而,咱們但願兩個輸入框內的數值彼此可以同步。當咱們更新攝氏度輸入框內的數值時,華氏度輸入框內應當顯示轉換後的華氏溫度,反之亦然。

React 中,將多個組件中須要共享的 state 向上移動到它們的最近共同父組件中,即可實現共享 state。這就是所謂的「狀態提高」。接下來,咱們將 TemperatureInput 組件中的 state 移動至 Calculator 組件中去。

若是 Calculator 組件擁有了共享的 state,它將成爲兩個溫度輸入框中當前溫度的「數據源」。它可以使得兩個溫度輸入框的數值彼此保持一致。因爲兩個 TemperatureInput 組件的 props 均來自共同的父組件 Calculator,所以兩個輸入框中的內容將始終保持一致。

讓咱們看看這是如何實現的。

**核心點在於:**父組件將狀態改變函數做爲 props 傳遞給子組件。

咱們會把當前輸入的 temperaturescale 保存在組件內部的 state 中。這個 state 就是從兩個輸入框組件中「提高」而來的,而且它將用做兩個輸入框組件的共同「數據源」。這是咱們爲了渲染兩個輸入框所須要的全部數據的最小表示。

因爲兩個輸入框中的數值由同一個 state 計算而來,所以它們始終保持同步:

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }

  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', temperature});
  }

  tryConvert(temperature, convert){
  	... //用來轉化溫度
  }

  render() {
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;    				const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
    return (
      <div> <TemperatureInput scale="c" temperature={celsius} onTemperatureChange={this.handleCelsiusChange} /> <TemperatureInput scale="f" temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange} /> <BoilingVerdict celsius={parseFloat(celsius)} /> </div>
    );
  }
}
複製代碼

再讓咱們看下 TemperatureInput 組件如何變化。咱們移除組件自身的 state,經過使用 this.props.temperature 替代 this.state.temperature 來讀取溫度數據。當咱們想要響應數據改變時,咱們須要調用 Calculator 組件提供的 this.props.onTemperatureChange(),而再也不使用 this.setState()

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value)
  }

  render() {
    const temperature = this.props.temperature
    const scale = this.props.scale
    return (
      <fieldset> <legend>Enter temperature in {scaleNames[scale]}:</legend> <input value={temperature} onChange={this.handleChange} /> </fieldset>
    )
  }
}
複製代碼

如今不管你編輯哪一個輸入框中的內容,Calculator 組件中的 this.state.temperaturethis.state.scale 均會被更新。其中一個輸入框保留用戶的輸入並取值,另外一個輸入框始終基於這個值顯示轉換後的結果。

讓咱們來從新梳理一下當你對輸入框內容進行編輯時會發生些什麼:

  • React 會調用 DOM 中 <input>onChange 方法。在本實例中,它是 TemperatureInput 組件的 handleChange 方法。
  • TemperatureInput 組件中的 handleChange 方法會調用 this.props.onTemperatureChange(),並傳入新輸入的值做爲參數。其 props 諸如 onTemperatureChange 之類,均由父組件 Calculator 提供。
  • 起初渲染時,用於攝氏度輸入的子組件 TemperatureInput 中的 onTemperatureChange 方法與 Calculator 組件中的 handleCelsiusChange 方法相同,而,用於華氏度輸入的子組件 TemperatureInput 中的 onTemperatureChange 方法與 Calculator 組件中的 handleFahrenheitChange 方法相同。所以,不管哪一個輸入框被編輯都會調用 Calculator 組件中對應的方法。
  • 在這些方法內部,Calculator 組件經過使用新的輸入值與當前輸入框對應的溫度計量單位來調用 this.setState() 進而請求 React 從新渲染本身自己。
  • React 調用 Calculator 組件的 render 方法獲得組件的 UI 呈現。溫度轉換在這時進行,兩個輸入框中的數值經過當前輸入溫度和其計量單位來從新計算得到。
  • React 使用 Calculator 組件提供的新 props 分別調用兩個 TemperatureInput 子組件的 render 方法來獲取子組件的 UI 呈現。
  • React 調用 BoilingVerdict 組件的 render 方法,並將攝氏溫度值以組件 props 方式傳入。
  • React DOM 根據輸入值匹配水是否沸騰,並將結果更新至 DOM。咱們剛剛編輯的輸入框接收其當前值,另外一個輸入框內容更新爲轉換後的溫度值。

得益於每次的更新都經歷相同的步驟,兩個輸入框的內容才能始終保持同步。

Monitoring State in React DevTools

講完了狀態提高,讓咱們如今來看看它怎麼運用到兄弟組件通訊中來!

如今有這樣一個場景

  • 繪製登陸頁面:輸入用戶名和密碼
  • 點擊登陸

image.png

class Login extends React.Component {
		constructor(props) {
        super (props);
        this.state = {
            userName:"",
            password:""
        }
    }

  	handlerLogin(e){
      this.setState(e)
    }

    render(){
        return(
        	<div>
          	<UserNameInput onChange = {value => this.handlerLogin({username:value})}>
          	<PasswordInput onChange = {value => this.handlerLogin({password:value})}>
          </div>
        )
    }
}

class UserNameInput extends React.Component {
     handlerUserName(e){
       this.props.handlerLogin(e.target.value);
     }

  	render(){
      return (
      	<div>
        	<input onChange={e => this.handlerUserName(e)} placeholder="請輸入用戶名"/>
        </div>
      )
    }
}

class PasswordInput extends React.Component {
     handlerPassword(e){
       this.props.handlerLogin(e.target.value);
     }

    render(){
        return (
          <div>
            <input onChange={e => this.handlerUserName(e)} placeholder="請輸入密碼"/>
          </div>
        )
      }
}
複製代碼

其實這裏的代碼並無寫完,但咱們能夠看到的是咱們已經能夠在 App 組件中拿到用戶名和密碼了,接下來咱們就能夠在此去調用登陸接口了。

下節預告

下節中咱們將講述使用 React 組件間通訊的相關知識,組件化的內容將以前的實戰案例進行改版,優化以前的實戰方案。敬請期待!

相關文章
相關標籤/搜索