終於學到了組件實例的三大核心屬性之一 State。
本篇內容為看影片學習做的筆記,影片為 尚硅谷2021版React技術全家桶
既然提到的是組件”實例“的三大核心屬性,那就代表是類式組件了,畢竟函數式組件連類都沒有,談何實例呢。(其實 hooks 也可以讓函數式組件用 state、props 和 refs,這邊暫且不提)
這邊整理幾個關於state的小重點,建議可以學完後面再來回顧:
- state 是組件物件最重要的屬性,值是物件(可以包含多個 key-value 的組合)。
- 組件被稱為”狀態機”,通過更新組件的 state 來更新對應的頁面提示(重新渲染組件)。
- 組件中 render 方法中的 this 為組件實例物件。
- 組件自定義的方法中 this 為undefined,如何解決?
- 強制綁定 this:通過函數物件的 bind()
- 箭頭函數
- State 的值,不能直接修改或更新,必須使用
setState({})
改變組件 state 值
現在我們要來完成一個簡單的小題目,從中學習 State :
要讓畫面上呈現 今天天氣很炎熱,並且單擊鼠標左鍵時,炎熱兩個字會替換成涼爽。
不是 炎熱 就是 涼爽,這種非黑即白的概念可以透過 boolean 去控制:
isHot:true => 炎熱
isHot:false => 涼爽
我們得想辦法把isHot放進 state裡面,然後將它讀出來進行判斷,因此在最開始,我們要先思考如何將寫到 state 去。
想對類的實例化進行操作,比如增加一個屬性或修改一個屬性的值,代表我們需要去寫建構子。
一樣是通過官方的範例來寫:
constructor(props){ super(props) this.state = {isHot:true} }
將 state 設置為 isHot 值後,再去寫回傳的內容
render() { return ( <h1>今天天氣很{this.state.isHot ? '炎熱' : '涼爽'}</h1> ); }
這時候我們可以看到 state 已經有 isHot: true
現在我們只差點擊一下就切換狀態,而在說之前,我們先複習一下原生事件綁定方法。
原生事件綁定
假設我們今天有三個按鈕,當我點擊這三個按鈕都能跳出訊息視窗:
<button id="btn1">按鈕1</button> <button id="btn2">按鈕2</button> <button>按鈕3</button>
我可以用 addEventListener 去監聽事件
const btn1 = document.getElementById('btn1') btn1.addEventListener('click',()=>{ alert('按鈕1被點擊') }) const btn2 = document.getElementById('btn2') btn2.addEventListener('click',()=>{ alert('按鈕2被點擊') })
或者我也可以直接在 button 中加上 onclick屬性,去調用我自己寫的一個方法
<button onclick="demo()">按鈕3</button>
function demo() { alert('按鈕3被點擊') }
事實上,在React中這三種方式也都能用,只是React更推薦直接在標籤中寫上onclick,也就是按鈕 3 的寫法。
那麼我們就把原生的方法丟進來試試:
<script type="text/babel"> class Weather extends React.Component { constructor(props){ super(props) // 初始化狀態 this.state = {isHot:true,wind:'微風'} } render() { // 讀取狀態 const {isHot} = this.state return ( <h1 id="title">今天天氣很{isHot ? '炎熱' : '涼爽'}</h1> ); } } // 渲染組件到頁面 ReactDOM.render( <Weather/>, document.getElementById('test') ) const title = document.getElementById('title') title.addEventListener('click',()=>{ console.log('標題被點擊') }) </script>
實際上,是可以實現的
又或者直接寫onclick也是可以的:
title.onclick = () =>{ console.log('標題被點擊') }
但是,都用上React了卻還是一直去使用 document ,這不合適,所以我們應該使用下面這種方法。
加上一個方法
function demo() { alert('標題被點擊') }
將 h1 標籤裡面加上 onClick
render() { // 讀取狀態 const {isHot} = this.state return ( <h1 onClick="demo()">今天天氣很{isHot ? '炎熱' : '涼爽'}</h1> ); }
然而,卻報錯了:
因為 onClick 要接收的並不是 string 類型,我們需要改成:
<h1 onClick={demo()}>今天天氣很{isHot ? '炎熱' : '涼爽'}</h1>
這下可以了,但我們又會發現,我們都還沒去點標題就已經出現了訊息視窗
因為寫成 {demo()}
代表要把 demo() 的返回值給onClick作為回調,然而我們的demo方法並沒有返回值(其實有,是undefined),因此點的時候回調的是 undefined,而底層發現是undefined就沒有別的動作。
所以我們必須改成 {demo}
,代表我把右邊的函數交給onClick作為回調,等我點擊的時候幫我調用 demo 方法。
class Weather extends React.Component { constructor(props){ super(props) // 初始化狀態 this.state = {isHot:true} } render() { // 讀取狀態 const {isHot} = this.state return ( <h1 onClick={demo}>今天天氣很{isHot ? '炎熱' : '涼爽'}</h1> ); } } // 渲染組件到頁面 ReactDOM.render( <Weather/>, document.getElementById('test') ) function demo() { console.log('標題被點擊') }
但是把按鍵 function 寫在外面很雜很難看,所以我們把它丟進類中,並且記得把 function 拔掉:
class Weather extends React.Component { constructor(props){ super(props) this.state = {isHot:true} } render() { // 讀取狀態 const {isHot} = this.state return ( <h1 onClick={this.changeWeather}>今天天氣很{isHot ? '炎熱' : '涼爽'}</h1> ); } changeWeather() { // changeWeather放在哪裡? Weather的原型物件上 供實例使用 // 由於 changeWeather 是作為onClick的回調 , 所以不是通過實例調用的, 是直接調用 // 類中的方法默認開啟了局部的嚴格模式, 所以 changeWeather 中的 this 為 undefined console.log(this) } } // 渲染組件到頁面 ReactDOM.render( <Weather/>, document.getElementById('test') )
但是現在這樣還是錯誤的。
因為 changeWeather 放在 Weather 的原型物件上,供實例使用,而目前 this.changeWeather 自身還沒有實例,但它會順著原型找到原型身上的。
這時候我們需要用到 bind,bind的用處為:
- 生成新的函數
- 修改函數裡的this
因此需要將建構子改為:
constructor(props){ super(props) this.state = {isHot:true} this.changeWeather = this.changeWeather.bind(this) }
bind(this)
:因為是放在建構子中,所以裡面的 this 指的是 Weather 的實例物件。如此一來,你的實例物件自身就多了一個 changeWeather 方法
又或者,將名字改一改會比較好去理解:
constructor(props){ super(props) this.state = {isHot:true} this.nice = this.changeWeather.bind(this) } render() { // 讀取狀態 const {isHot} = this.state return ( <h1 onClick={this.nice}>今天天氣很{isHot ? '炎熱' : '涼爽'}</h1> ); } changeWeather() { console.log(this) }
接著就是要來改 state 的值了。
如果直接這樣去寫:
const isHot = this.state.isHot this.state.isHot = !isHot
理論上沒問題,實際也並沒有問題,但是React不認可這種直接更改。
因為這屬於直接更改,React並不認可,雖然它是可行的不會報錯,但是不會真正去修改state的值。
在官方文檔中的”正確的使用State”有提到:
因此我們需要透過 setState({})
去更新 state的值:
const isHot = this.state.isHot // state 不可直接更改 , state 必須透過 setState進行更新 this.setState({ isHot:!isHot })
最後,完整的程式碼為:
class Weather extends React.Component { constructor(props){ super(props) this.state = {isHot:true} this.changeWeather = this.changeWeather.bind(this) } render() { const {isHot} = this.state return ( <h1 onClick={this.changeWeather}>今天天氣很{isHot ? '炎熱' : '涼爽'}</h1> ); } changeWeather() { const isHot = this.state.isHot // state 不可直接更改 , state 必須透過 setState進行更新 this.setState({ isHot:!isHot }) } } // 渲染組件到頁面 ReactDOM.render( <Weather/>, document.getElementById('test') )
學得頭疼…哈…
簡化程式碼
接著讓我們來簡化一下建構子
constructor(props){ super(props) this.state = {isHot:true,wind:'微風'} this.changeWeather = this.changeWeather.bind(this) }
我們可以把固定的屬性直接寫在類中,像是這樣
constructor(props){ super(props) } state = {isHot:true,wind:'微風'} changeWeather = this.changeWeather.bind(this)
確實可以這麼去寫,並且建構子中沒東西了還可以直接刪掉。
但是有個更精簡的寫法,甚至不需要這行 changeWeather = this.changeWeather.bind(this)
。
那就是將 changeWeather 方法改成箭頭函數:
changeWeather = () => { const isHot = this.state.isHot this.setState({ isHot:!isHot }) }
為什麼?
箭頭函數的特性是它沒有自己的 this,但如果在箭頭函數中使用 this 並不會報錯,反而它會找其外頭函數的 this 作為箭頭函數的 this 去使用 (這邊的外側 -> 類中)。
因此可以完美的代替原本的 changeWeather = this.changeWeather.bind(this)
這句。
所以最後我們可以精簡成:
class Weather extends React.Component { state = {isHot:true,wind:'微風'} render() { const {isHot,wind} = this.state return ( <h1 onClick={this.changeWeather}>今天天氣很{isHot ? '炎熱' : '涼爽'} , {wind}</h1> ); } changeWeather = () => { const isHot = this.state.isHot this.setState({ isHot:!isHot }) } } ReactDOM.render( <Weather/>, document.getElementById('test') )
所以以後,當我們要在類中寫自定義方法,要用賦值語句的形式然後加上箭頭函數。
完事。
參考資料
- 解決 preact-router 資源請求路徑錯誤的問題 - 2022 年 6 月 24 日
- [楓之谷私服] 潮流轉蛋機 NPC 腳本優化 - 2022 年 6 月 19 日
- [楓之谷私服] 簡單的飛天椅子(坐騎)改法 v120 - 2022 年 6 月 19 日