1. 程式人生 > 其它 >React元件之間的通訊方式總結(上)

React元件之間的通訊方式總結(上)

先來幾個術語:

官方 我的說法 對應程式碼
React element React元素 let element=<span>A爆了</span>
Component 元件 class App extends React.Component {}
App為父元素,App1為子元素 <App><App1></App1></App>

本文重點:

  • 元件有兩個特性
    • 1、傳入了一個“props”
    • 2、返回了一個React元素
  • 元件的建構函式
    • 如果需要重新定義constructor,必須super一下,才能啟用this
      ,也就是可以用來自React.component方法
  • 元件的props
    • 是可讀的,也就是不能在元件中修改prop的屬性
    • JSX中傳入物件的props,可以通過{...object}的方式
  • 父子元素之間的通訊(初級版本)
    • 父=>子,通過父元素的render既可改變子元素的內容。
    • 子=>夫,通過父元素傳入子元素中的props上掛載的方法,讓子元素觸發父元素中的方法,從而進行通訊。

Component

上回說到JSX的用法,這回要開講react元件之間的一個溝通。那麼什麼是元件?我知道英文是Component,但這對我而言就是一個單詞,毫無意義。要了解Component之間是如何進行友好交流的,那就要先了解Component是個什麼鬼。

上回說到的JSX,我們可以這麼建立物件:

let element=<h1 className="aaa">A爆了</h1>
//等同於
let element=React.createElement(
  "h1",
  {className:"aaa"},
  "A爆了"
)

還是老老實實地用h1div這種標準的HTML標籤元素去生成React元素。但是這樣的話,我們的JS就會變得巨大無比,全部都是新建的React元素,有可能到時候我們連物件名都不曉得怎麼起了,也許就變成let div1;let div2這樣的。哈哈哈開個玩笑。但是分離是肯定要分離的。這個時候就有了名為Component的概念。他可以做些什麼呢?簡單的說就是建立一個個獨立的

可複用的小元件。話不多說,我們來瞅瞅來自官方的寫法:

寫法一:函式型建立元件,大家可以看到我就直接定義一個名為App的方法,每次執行App()的時候就會返回一個新的React元素。而這個方法我們可以稱之為元件Component。有些已經上手React的朋友,可能傻了了,這是什麼操作,我的高大上class呢?extend呢?很遺憾地告訴你,這也是元件,因為他符合官方定義:1、傳入了一個“props” ,2、返回了一個React元素。滿足上述兩個條件就是Component!

function App(props) {
  return <span>{props.name}!A爆了</span>     
}

這個是最簡易的Component了,在我看來Component本身是對React.createElement的一種封裝,他的render方法就相當於React.createElement的功能。高大上的元件功能來啦:

import React, { Component } from 'react';
class App extends Component {
  render() {
    return <span>{this.props.name}!A爆了</span>     
  }
}
export default App;

這個class版本的元件和上方純方法的元件,從React的角度上來說,並無不同,但是!畢竟我class的方式還繼承了React.Component,不多點小功能都說不過去對吧?所以說我們這麼想繼承了React.Component的元件的初始功能要比純方法return的要多。所以每個React的Component我們都可以當作React元素直接使用。

好了,我們來研究研究Component這個類的方法吧。

首先是一個神奇的constructor函式,這個函式在類中,可以說是用於初始化的函式。如果省去不寫,也不會出錯,因為我們的元件都是React.Component的子類,所以都繼承了React.Componentconstructor方法。如果我們在子類Component中定義了constructor相當於是覆蓋了父類的方法,這樣React.Component的建構函式就失效了。簡單地來說就是很多預設的賦值都失效了。你是獲取不到props的。因此官方為了提醒大家不要忘記super一下,也就是繼承父類的constructor,因此會報"this hasn't been initialised - super() hasn't been called"這個錯誤。意思就是你先繼承一下。也就是說super是執行了父類的constructor的方法。所以!!!重點來了——我們寫super的時候不能忘記傳入props。不傳入props,程式就無法獲取定義的元件屬性了。

constructor(props) {
    super(props);//相當於React.Component.call(this,props)
}

官方也給大家劃重點了:

Class components should always call the base constructor with props.(類組建在執行基本constructor的時候,必須和props一起。)

對於我們沒有寫constructor,但在其他自帶方法中,比如render,也可以直接獲取到props,這個詭異的操作就可以解釋了。因為我們省略了重定義,但是constructor本身不僅是存在的而且也執行了,只不過沒有在我們寫的子類中體現出來而已。

props的坑

分析了Component之後,大家有沒有發現Component的一個侷限?沒錯!就是傳參!關於Component的一個定義就是,只能傳入props的引數。也就是說所有的溝通都要在這個props中進行。有種探監的既視感,只能在規定的視窗,拿著對講機聊天,其他的方式無法溝通。React對於props有著苛刻的規定。參考 前端進階面試題詳細解答

All React components must act like pure functions with respect to their props.

簡單地來說就是props是不能被改變的,是隻讀的。(大家如果不信邪,要試試,可以直接改props的值,最終等待你的一定是報錯頁面。)

這裡需要科普下純函式pure function的概念,之後Redux也會遇到的。意思就是純函式只是一個過程,期間不改變任何物件的值。因為JS的物件有個很奇怪的現象。如果你傳入一個物件到這個方法中,並且改變了他某屬性的值,那麼傳入的這個物件在函式外也會改變。pure function就是你的改動不能對函式作用域外的物件產生影響。所以每次我們在Component裡面會遇到一個新的物件state,一般這個元件的資料我們會通過state在當前元件中進行變化處理。

劃重點:因為JS的特性,所以props設定為只讀,是為了不汙染全域性的作用域。這樣很大程度上保證了Component的獨立性。相當於一個Component就是一個小世界。

我發現定義props的值也是一門學問,也挺容易踩坑的。

比如下方程式碼,我認為打印出來應該是props:{firstName:"Nana",lastName:"Sun"...},結果是props:{globalData:true}.

let globalData={
    firstName:"Nana",
    lastName:"Sun",
    greeting:["Good moring","Good afternoon","Good night"]
}
ReactDOM.render(<App globalData/>, document.getElementById('root'));

所以對於props是如何傳入元件的,我覺得有必要研究一下下。

props其實就是一個引數直接傳入元件之中的,並未做什麼特殊處理。所以對props進行處理的是在React.createElement這一個步驟之中。我們來回顧下React.createElement是怎麼操作的。

React.createElement(
  "yourTagName",
  {className:"aaa",color:"red:},  "文字/子節點")//對應的JSX寫法是:<yourTagName className="aaa" color="red>文字/子節點</yourTagName>

也就是他的語法是一個屬性名=屬性值,如果我們直接放一個<App globalData/>,那麼就會被解析成<App globalData=true/>},所以props當然得不到我們想要的結果。這個是他的一個語法,我們無法扭轉,但是我們可以換一種寫法,讓他無法解析成屬性名=屬性值,這個寫法就是{...globalData},解構然後重構,這樣就可以啦。

Components之間的訊息傳遞

單個元件的更新->setState

Components之間的訊息傳遞是一個互動的過程,也就是說Component是“動態”的而不是“靜態”的。所以首先我們得讓靜態的Component“動起來”,也就是更新元件的的值,前面不是提過props不能改嘛,那怎麼改?前文提過Component就是一個小世界,所以這個世界有一個狀態叫做state

先考慮如何外力改變Component的狀態,就比如點選啦,劃過啦。

class App extends Component {
  state={
      num:0
  }
  addNum=()=>{
    this.setState({
      num:this.state.num+1
    })
  }
  render() {
    return( [
        <p>{this.state.num}</p>,
        <button onClick={this.addNum}>點我+1</button>
      ]
    )     
  }
}

這裡我用了onClick的使用者主動操作的方式,迫使元件更新了。其實component這個小世界主要就是靠state來更新,但是不會直接this.state.XXX=xxx直接改變值,而是通過this.setState({...})來改變。

這裡有一個小tips,我感覺大家很容易犯錯的地方,有關箭頭函式的this指向問題,大家看下圖。箭頭函式轉化成ES5的話,我們就可以很清晰得看到,箭頭函式指向他上一層的函式物件。這裡也就指向App這個物件。

如果不想用箭頭函式,那麼就要注意了,我們可以在onClick中加一個bind(this)來繫結this的指向,就像這樣onClick={this.addNum.bind(this)}

  render() {
    return( [
        <p>{this.state.num}</p>,
        <button onClick={this.addNum.bind(this)}>點我+1</button>
      ]
    )     
  }

元件之間的通訊

那麼Component通過this.setState可以自high了,那麼元件之間的呢?Component不可能封閉自己,不和其他的Component合作啊?那我們可以嘗試一種方式。

在App中我把<p>{this.state.num}</p>提取出來,放到App1中,然後App1直接用props來顯示,因為props是來自父元素的。相當於我直接在App(父元素)中傳遞num給了App1(子元素)。每次App中state發生變化,那麼App1就接收到召喚從而一起更新。那麼這個召喚是基於一個什麼樣的理論呢?這個時候我就要引入React的生命週期life cycle的問題了。

//App
render() {
  return( [
      <App1 num={this.state.num}/>,
      <button onClick={this.addNum}>點我+1</button>
    ]
  )     
}
//App1
render() {
  return( [
      <p>{this.props.num}</p>,
    ]
  )     
}

react的生命週期

看到生命週期life cycle,我就感覺到了生生不息的迴圈cycle啊!我是要交代在這個圈圈裡了嗎?react中的生命週期是幹嘛的呢?如果只是單純的渲染就沒有生命週期一說了吧,畢竟只要把內容渲染出來,任務就完成了。所以這裡的生命週期一定和變化有關,有變化才需要重新渲染,然後再變化,再渲染,這才是一個圈嘛,這才是life cycle。那麼React中的元素變化是怎麼變的呢?

先來一個官方的生命週期(我看著就頭暈):

點我看live版本

官方的全週期:

官方的簡約版週期:

有沒有看著頭疼,反正我是跪了,真令人頭大的生命週期啊。我還是通過實戰來確認這個更新是怎麼產生的吧。實戰出真理!(一些不安全的方法,或者一些我們不太用得到的,這裡就不討論了。)

Mounting裝備階段:

  • constructor()
  • render()
  • componentDidMount()

Updating更新階段:

  • render()
  • componentDidUpdate()
  • 具有爭議的componentWillReceiveProps()

Unmounting解除安裝階段:

  • componentWillUnmount()

Error Handling錯誤捕獲極端

  • componentDidCatch()

這裡我們通過執行程式碼來確認生命週期,這裡是一個父元素巢狀子元素的部分程式碼,就是告訴大家,我在每個階段列印了啥。這部分的例子我用的還是上方的App和App1的例子。

//father
constructor(props){
  console.log("father-constructor");
}
componentDidMount() {
  console.log("father-componentDidMount");
}
componentWillUnmount() {
  console.log("father-componentWillUnmount");
}
componentDidUpdate() {
  console.log("father-componentDidUpdate");
}
render() {
  console.log("father-render");
}
//child
constructor(props){
  console.log("child-constructor");
  super(props)
}
componentDidMount() {
  console.log("child-componentDidMount");
}
componentWillUnmount() {
  console.log("child-componentWillUnmount");
}
componentDidUpdate() {
  console.log("child-componentDidUpdate");
}
componentWillReceiveProps(){
  console.log("child-componentWillReceiveProps");
}
render() {
  console.log("child-render");
}

好了開始看圖推理

初始化執行狀態:

父元素先執行建立這沒有什麼問題,但是問題是父元素還沒有執行結束,殺出了一個子元素。也就是說父元素在render的時候裡面碰到了子元素,就先裝載子元素,等子元素裝載完成後,再告訴父元素我裝載完畢,父元素再繼續裝載直至結束。

我點選了一下,父元素setState,然後更新了子元素的props

同樣的先父元素render,遇到子元素就先暫時掛起。子元素這個時候出現了componentWillReceiveProps,也就是說他是先知道了父元素傳props過來了,然後再render。因為有時候我們需要在獲取到父元素改變的props之後再執行某種操作,所以componentWillReceiveProps很有用,不然子元素就直接render了。突想皮一下,那麼我子元素裡面沒有props那是不是就不會執行componentWillReceiveProps了??就是<App1 num={this.state.num}/>變成<App1/>。我還是太天真了。這個componentWillReceiveProps依然會執行也就是說:

componentWillReceiveProps並不是父元素傳入的props發生了改變,而是父元素render了,就會出發子元素的這個方法。

關於解除安裝,我們來玩一下,把App的方法改成如下方所示,當num等於2的時候,不顯示App1。

render() {
  return( 
    <div>
      {this.state.num===2?"":<App1 num={this.state.num}/>}      <button onClick={this.addNum}>點我+1</button>
    </div>
  )     
}

App先render,然後解除安裝了App1之後,完成了更新componentDidUpdate

那麼大家看懂了生命週期了嗎??我總結了下:

  • 父元素裝載時render了子元素,就先裝載子元素,再繼續裝載父元素。
  • 父元素render的時候,子元素就會觸發componentWillReceiveProps,並且跟著render
  • 父元素解除安裝子元素時,先render,然後解除安裝了子元素,最後componentDidUpdate

如何子傳父親呢??

通過生命週期,子元素可以很容易的獲取到父元素的內容,但是父元素如何獲得來自子元素的內容呢?我們不要忘記了他們為一個溝通橋樑props!我們可以在父元素中建立一個方法用於獲取子元素的資訊,然後繫結到子元素上,然後不就可以獲取到了!操作如下所示:

receiveFormChild=(value)=>{
  console.log(value)
}
render() {
  return( 
    <div>
      {this.state.num===2?"":<App1 num={this.state.num} popToFather={this.receiveFormChild}/>}      <button onClick={this.addNum}>點我+1</button>
    </div>
  )     
}

當子元素執行popToFather的時候,訊息就可以傳給父親啦!

子元素:

render() {
  return( [
      <p>{this.props.num}</p>,
      <button onClick={()=>this.props.receiveState("來自子元素的慰問")}>子傳父</button>
    ]
  )     
}

父元素成功獲取來自子元素的慰問!

這次就科普到這裡吧。