iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 6
8
Modern Web

一步一腳印的React旅程系列 第 6

[筆記][React]進入Component的事件處理篇!

Hello!大家好啊!明明才第六篇而已,感覺卻快被榨乾了XD。

前兩天我們的例子都圍繞在計時器身上,但是真正的網頁可沒有那麼簡單,因為還有使用者這種生物在,身為一個正常的網頁,頁面上會有一兩個事件,也是再正常不過的事情了對吧?所以今天就來學習怎麼在組件中設定事件吧!


設定事件

能夠跟我一起走到第六篇,應該都能稍微了解我的tempo了,那...就直接進入實做吧XD

class CheckButton extends React.Component{
    //在`class`中宣告一個事件
    writeConsole() {
        console.log('點了點了點了')
    }

    render(){
        //使用onClick指定觸發的事件
        return <input type="button" onClick={this.writeConsole} 
                value="點我看console" />
    }
}

ReactDOM.render(<CheckButton /> ,document.getElementById('root'))

結果如下:
https://ithelp.ithome.com.tw/upload/images/20180923/201069355pE8NKid32.png

GitHub程式連結
GitPage頁面連結

這應該是觸發事件最基本的方式了,只是有些事情是需要注意的,包括讓我花了很多時間的小細節XD

  1. 要監聽的事件必須要使用駝峰命名。
    一般情況我們會這麼用,但這是錯誤的,他不會有任何效果:
        <input type="button" onclick={this.writeConsole} value="點我看console" />
    
    Reactonclick必須變為onClick,如下:
        <input type="button" onClick={this.writeConsole} value="點我看console" />
    
  2. 指定函式的方式不是字串。
    通常我們會這樣使用,但這是錯的:
    <input type="button" onClick="writeConsole()">
    
    要使用花括號指定function,並且指定this
    <input type="button" onClick={this.writeConsole}>
    

在事件中更改state的值

上一篇有提到如果要修改state必須使用setState(),所以我們要寫一個計數器的話,可以很輕易地寫出下方的程式碼:

class AddButton extends React.Component{
    constructor(props){
        super(props)
        this.state = ({clickCount : 0})
    }
    
    addCount(){
        this.setState({clickCount:this.state.clickCount + 1})
        console.log(`點了${this.state.clickCount}下`)
    }
    
    render(){
        return <input type="button" 
                      onClick={this.addCount} value="點我" />
    }
}

ReactDOM.render(<AddButton />,document.getElementById('root'))

看起來一切沒問題對吧!我們打包執行並點擊按鍵觸發事件:
https://ithelp.ithome.com.tw/upload/images/20180923/20106935zIlHVOQhhy.png

我滴天哪!想都沒想到居然噴出了一個華麗的錯誤,錯誤內容是Cannot read property 'setState' of undefined,而我們呼叫setState的地方只有在addCount函式內的這一行this.setState({...}),有句話說「羊毛出在羊身上」所以先讓我們確認一下this在這時候讀到的值是什麼?因為setState這個函式絕對不會有錯的,除非錯的是this,那讓我們在出錯前把this打印出來:

class AddButton extends React.Component{
    constructor(props){
        super(props)
        this.state = ({clickCount : 0})
    }
    
    addCount(){
        console.log(this)
        this.setState({clickCount:this.state.clickCount + 1})
        console.log(`點了${this.state.clickCount}下`)
    }
    
    render(){
        return <input type="button" 
                      onClick={this.addCount} value="點我" />
    }
}

ReactDOM.render(<AddButton />,document.getElementById('root'))

結果this果然沒有指向class本身,而是undefined
https://ithelp.ithome.com.tw/upload/images/20180923/20106935vpAo5zZ1lP.png

所以問題來了,我們該怎麼去設定方法執行時的this呢?欸嘿嘿!這個小問題就交給bind()來處理吧!如果對bind不太熟可以參考[筆記][JavaScript]使用call()、apply()、bind()設定函式中的「this」

知道設定的方法後,還要在對的地方去傳入this那才會是正確的,這裡簡單說明一下,一個使用class建構器產生的物件,該物件的屬性絕對來自於constructor建構子,所以當物件被建立出來後,我們可以把那個物件稱做constructor建構子的實體,如果有興趣可以參考這裡[筆記][JavaScript]建構器及實體物件的constructor特性,雖然裡面不是使用ES6的class語法說明,但是原理還是一樣的!

既然知道constructor會指向class本身,那我們可以把addCount這個方法,宣告在constructor中,讓組件被建構出來的時候一樣可以有該方法能用:

class AddButton extends React.Component{
    constructor(props){
        super(props)
        this.state = ({clickCount : 0})
        //在constructor指定呼叫addCount,並在呼叫時指定this為class本身
        this.addCount = this.addCount.bind(this)
    }
    
    addCount(){
        console.log(this)
        this.setState({clickCount:this.state.clickCount + 1})
        console.log(`點了${this.state.clickCount}下`)
    }
    
    render(){
        return <input type="button" 
                      onClick={this.addCount} value="點我" />
    }
}

ReactDOM.render(<AddButton />,document.getElementById('root'))

結果就會如我們所願!成功修改了state的值了!
https://ithelp.ithome.com.tw/upload/images/20180924/20106935a8kB6elmE0.png
但是我們發現明明在console.log之前已經使用了setState()去修改state的值,但第一次點擊的時候計數器顯示的卻是0而不是1,原因是因為setState()他是異步執行的關係,所以並不會等到修改後才執行console.log,為了這個情況官方提供了兩種方式處理,相信其中一種方式已經浮現在大家的腦中了,沒錯!就是componentDidUpdate()

class AddButton extends React.Component{
    constructor(props){
        super(props)
        this.state = ({clickCount : 0})
        //在constructor指定呼叫addCount,並在呼叫時指定this為class本身
        this.addCount = this.addCount.bind(this)
    }
    
    addCount(){
        this.setState({clickCount:this.state.clickCount + 1})
    }
    
    componentDidUpdate(){
        //把原本在addCount中的console.log移到componentDidUpdate()中
        console.log(`點了${this.state.clickCount}下`)
    }
    
    render(){
        return <input type="button" 
                      onClick={this.addCount} value="點我" />
    }
}

ReactDOM.render(<AddButton />,document.getElementById('root'))

結果就會和剛剛不一樣了!每一次取都是修改後的值。
https://ithelp.ithome.com.tw/upload/images/20180924/20106935oKTpWyQDvB.png

第二個方法是直接在setState()中傳入第二個參數,讓第二個參數做為修改完後執行的function,如下:

class AddButton extends React.Component{
    constructor(props){
        super(props)
        this.state = ({clickCount : 0})
        //在constructor指定呼叫addCount,並在呼叫時指定this為class本身
        this.addCount = this.addCount.bind(this)
    }
    
    addCount(){
        this.setState({clickCount:this.state.clickCount + 1},()=>{console.log(`點了${this.state.clickCount}下`)})
    }
    
    render(){
        return <input type="button" 
                      onClick={this.addCount} value="點我" />
    }
}

ReactDOM.render(<AddButton />,document.getElementById('root'))

結果也會和使用componentDidUpdate()一樣,以下只附上第二種方式:
GitHub程式連結
GitPage頁面連結

在事件中傳入參數

傳入參數很簡單,只需要在設定事件時在指定function名稱後添加bind(this),就可以在第一個this後添加要傳入的參數了,如下:

class AddButton extends React.Component{
    constructor(props){
        super(props)
        this.state = ({clickCount : 0})
        this.addCount = this.addCount.bind(this)
    }
    //增加一個count的參數,用來表示每次增加多少
    addCount(count){
        console.log(`每次添加值:${count}`)
        this.setState({clickCount:this.state.clickCount + count})
    }

    componentDidUpdate(){
        console.log(`點了${this.state.clickCount}下`)
    }
    
    render(){
        //在onClick中指定觸發的函式後面添加.bind(this)並填上傳入的第一個參數1
        return <input type="button" 
                      onClick={this.addCount.bind(this,1)} value="點我" />
    }
}

ReactDOM.render(<AddButton />,document.getElementById('root'))

結果會如下:
https://ithelp.ithome.com.tw/upload/images/20180924/20106935H9jiPWzkh7.png

GitHub程式連結
GitPage頁面連結

取得觸發事件的DOM

在實務上常常會需要再觸發事件的時候,去取得當前觸發事件的DOM,例如當我選擇某個下拉選單,我必須根據該下拉選單的值做處理,這時候該怎麼辦呢?來做個小例子吧!

class InputGender extends React.Component{
    constructor(props){
        super(props)
        this.state = ({gender : ''})
        this.changeGender = this.changeGender.bind(this)
    }
    //宣告事件時傳入參數event取得觸發事件變數
    changeGender(event){
        //將觸發事件的DOM從event內的target屬性取出
        console.log(event.target)
        //指定選擇的性別給state.gender
        this.setState({gender:event.target.value})
    }
    componentDidUpdate(){
        console.log(`已將state.gender變動為:${this.state.gender}`)
    }
    render(){
        return (<select onChange={this.changeGender.bind(this)}>
                    <option value="M">男</option>
                    <option value="W">女</option>
                </select>)
    }
}
ReactDOM.render(<InputGender />,document.getElementById('root'))

如此一來在選擇的時候就能夠取到觸發事件的DOM元件了:
https://ithelp.ithome.com.tw/upload/images/20180924/20106935t8UcxNNG9V.png

但是如果我們有自定義參數要傳入function中,就沒辦法直接設定event了,但是還是可以在事件內用window.event.target取得觸發事件的DOM:

class InputGender extends React.Component{
    constructor(props){
        super(props)
        this.state = ({gender : ''})
        this.changeGender = this.changeGender.bind(this)
    }
    //strA是傳入的參數
    changeGender(strA){
        console.log(`傳入參數${strA}`)
        console.log(window.event.target)
        this.setState({gender:event.target.value})
    }
    componentDidUpdate(){
        console.log(`已將state.gender變動為:${this.state.gender}`)
    }
    render(){
        //在onchange中增加一個自訂參數
        return (<select onChange={this.changeGender.bind(this,'aaaa')}>
                    <option value="M">男</option>
                    <option value="W">女</option>
                </select>)
    }
}
ReactDOM.render(<InputGender />,document.getElementById('root'))

結果仍然沒有問題:
https://ithelp.ithome.com.tw/upload/images/20180924/201069358eO3jdk2O8.png

GitHub程式連結
GitPage頁面連結


雖然一開始有點難懂,不過在好不容易搞懂class的繼承和各種this的傳來傳去後,就感覺清楚多了XD,希望小弟我的文章能夠減少各位卡關的時間!

最後如果文章中有任何問題或解釋不清楚的地方,還麻煩留言告訴我,小弟會盡快修改或補充文章內容的!謝謝大家/images/emoticon/emoticon41.gif

參考文章:

  1. https://reactjs.org/docs/handling-events.html
  2. http://eddychang.me/blog/javascript/98-why-setstate-is-async.html

上一篇
[筆記][React]Component的狀態State及生命週期Lifecycle
下一篇
[筆記][React]用 if 條件式來控制Component
系列文
一步一腳印的React旅程30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
lolis
iT邦新手 5 級 ‧ 2020-01-09 13:57:25

windoe.event.target打錯囖 XD

神Q超人 iT邦研究生 5 級 ‧ 2020-01-09 14:17:52 檢舉

非常感謝你 /images/emoticon/emoticon06.gif

0
no027843
iT邦新手 5 級 ‧ 2020-04-08 16:03:56

您好,請問在取得觸發事件的DOM的部分
如果我console.log(event)得到的東西SyntheticEvent是什麼呢?謝謝!

0
Sherry
iT邦新手 5 級 ‧ 2020-05-19 18:21:03

嗨,
我在事件中傳入參數這個練習區塊照著您的程式碼練習,但是console.log(每次添加值:${count});並沒有被印出來,想問問是為什麼呢?

神Q超人 iT邦研究生 5 級 ‧ 2020-05-26 14:05:13 檢舉

Hi!不好意思那麼晚才注意到這則訊息!

你的程式碼和文章內的一模一樣嗎?
還是方不方便貼上你的 Code 讓我看一下:)

0
ShawnGood
iT邦新手 5 級 ‧ 2022-03-16 23:07:27

感謝分享
我找到event跑去那了

changeGender(strA){
        console.log(`傳入參數${strA}`)
        console.log(window.event.target)
        this.setState({gender:event.target.value})
    }

它跑到後面去排隊了 /images/emoticon/emoticon25.gif

changeGender(strA,event){
        console.log(`傳入參數${strA}`)
        console.log(event)
        this.setState({gender:target.value})
    }

參考這篇說的
https://ithelp.ithome.com.tw/articles/10195896

我要留言

立即登入留言