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
- 如果需要重新定義
- 元件的
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爆了"
)
還是老老實實地用h1
、div
這種標準的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.Component
的constructor
方法。如果我們在子類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>
]
)
}
父元素成功獲取來自子元素的慰問!
這次就科普到這裡吧。