先來幾個術語:
官方 |
我的說法 |
對應代碼 |
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爆了")
還是老老實實地用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的問題了。
//Apprender() { return( [ <App1 num={this.state.num}/>, <button onClick={this.addNum}>點我+1</button> ] ) }//App1render() { return( [ <p>{this.props.num}</p>, ] ) }
react的生命週期
看到生命週期life cycle,我就感覺到了生生不息的迴圈cycle啊!我是要交代在這個圈圈裡了嗎?react中的生命週期是幹嘛的呢?如果只是單純的渲染就沒有生命週期一說了吧,畢竟只要把內容渲染出來,任務就完成了。所以這裡的生命週期一定和變化有關,有變化才需要重新渲染,然後再變化,再渲染,這才是一個圈嘛,這才是life cycle。那麼React中的元素變化是怎麼變的呢?
先來一個官方的生命週期(我看著就頭暈):
點我看live版本
官方的全周期:
官方的簡約版周期:
有沒有看著頭疼,反正我是跪了,真令人頭大的生命週期啊。我還是通過實戰來確認這個更新是怎麼產生的吧。實戰出真理!(一些不安全的方法,或者一些我們不太用得到的,這裡就不討論了。)
Mounting裝備階段:
- constructor()
- render()
- componentDidMount()
Updating更新階段:
- render()
- componentDidUpdate()
- 具有爭議的componentWillReceiveProps()
Unmounting卸載階段:
Error Handling錯誤捕獲極端
這裡我們通過運行代碼來確認生命週期,這裡是一個父元素嵌套子項目的部分代碼,就是告訴大家,我在每個階段列印了啥。這部分的例子我用的還是上方的App和App1的例子。
//fatherconstructor(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");}
//childconstructor(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
,也就是說他是Crowdsourced Security Testing道了父元素傳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> ] ) }
父元素成功擷取來自子項目的慰問!
這次就科普到這裡吧。