少踩點坑,你值得知道的React事件綁定

寫在前面

之前寫Vue寫慣了,心血來潮,寫起了react。html

github地址:Close2Reactvue

項目使用框架版本主要有 react(15.4.1) + react-dom(15.4.1) + webpack(1.13.3) + axios(0.15.3) + node(6.2.2), 點擊查看項目簡介:一段人人都應該知道的從 vue 到 react 的過渡史node

目前該項目有兩個分支, half-es6 + masterreact

half-es6和master實現的功能同樣, 實現了CURD + Axios + Otherswebpack

half-es6的寫法並無徹底使用es6的class的概念, master是完善了它ios

如今讓咱們快速瞭解React的事件綁定都有什麼坑~
⬇️ ⬇️ ⬇️git

案例1:

tab示例效果圖

錯誤示範

// 父組件主要是爲了實現tab的切換
const Content = React.createClass({
    getInitialState() {
        return {
            tabTxt: ['CURD', 'Axios', 'Others'],
            choice: 0, //目前激活的tab的下標
        }
    },

    switchChoice(idx){ // 設置choice
        this.setState({
            choice: idx
        })
    },

    renderTabInit(text, idx) {
        return (<Tab key={idx} idx={idx} // 利用props的綁定,把switchChoice傳到子組件中去,this.props.choose能夠調用到這個方法 choose={this.switchChoice} choice={this.state.choice} >{text}</Tab>)
    },

    render() { ...... }
});複製代碼

自覺得把方法傳入了子組件,就在Tab子組件中直接this.props.choose調用父組件的方法es6

const Tab = React.createClass({
    render(){
        return (
            <span className={this.props.idx == this.props.choice? "tab on" : "tab"} data-idx={this.props.idx} // 本意是在點擊的時候,可以直接調用父組件的方法 onClick={this.props.choose(this.props.idx)} >{this.props.children}</span>
        )
    }
});複製代碼

結果瀏覽器打開就爆炸了。boom
github

大概意思就是說:
我在父組件中的setState在渲染的時候致使了一個錯誤。React不能更新一個正在變化的state。
組件中的render應該是一個帶有state和props的pure function(純函數)。若是不是純函數,構造器會產生一些反作用。
好比在render的時候(組件掛載的時候)會根據props指定的參數繼續向下執行,則會在掛載的時候(還沒發生點擊事件)就直接執行了父組件的函數。web

順便解釋一下pure function
一、給出一樣的參數值,該函數老是求出一樣的結果。該函數結果值不依賴任何隱藏信息或程序執行處理可能改變的狀態或在程序的兩個不一樣的執行,也不能依賴來自I/O裝置的任何外部的輸入
二、結果的求值不會促使任何可語義上可觀察的反作用或輸出,例如易變對象的變化或輸出到I/O裝置

正確姿式

const Tab = React.createClass({
    chooseTab() { // 子組件的中轉函數
        this.props.choose(this.props.idx); //在這裏調用父組件的函數
    },

    render(){
        return (
            <span className={this.props.idx == this.props.choice? "tab on" : "tab"} data-idx={this.props.idx} onClick={this.chooseTab} // 調用中轉函數 >{this.props.children}</span>
        )
    }
});複製代碼

這個中轉函數的名詞是我本身取的,只是這樣就能讓點擊事件的函數變成pure function,就不會在組件掛載的時候就沿着props繼續向下執行,就能避免在掛載組件的時候就直接調用父組件的setState了。


案例2

todolist 的 編輯 & 保存 示例效果圖

錯誤示範

// 父組件
const PageA = React.createClass({
    getInitialState() { ... }, // 初始化todolist的數據
    componentDidMount(){ ... }, // 掛載組件時的函數
    initDidCount() { ... }, // 更新完成的進度

    handleTxtChange(event){ // 重點: 當input的輸入值變化時調用這個函數
        let index = event.target.getAttribute('data-index'); // 強行獲得todolist的index
        // 這裏必定須要index這個參數做爲修改this.state.list時候的下標
        this.state.list[index].text = event.target.value; // 把input的值更新到state上去
        this.setState({
            list: this.state.list
        });
        this.initDidCount(); // 更新完成進度
    },

    handleCheckChange(event,idx) { ... }, // checkbox的onChange,和input的onChange同樣
    deleteItem(idx) { ... },  // 刪除

    initListLi(val,idx) {
        return (
            <List {...val} key={idx} index={idx} // 綁定一些須要用到的props // 利用props的綁定,把handleTxtChange傳到子組件中去, //子組件中用this.props.handleTxtChange能夠調用到這個方法 //(handleCheckChange也是同理) handleTxtChange={this.handleTxtChange} handleCheckChange={this.handleCheckChange} deleteItem={this.deleteItem} />
        )
    },

    render() { ...... }
});複製代碼

這裏也會和案例1有一樣的狀況,父組件用props傳入的方法裏面有setState,若是在子組件的reader中直接用this.props.handleTxtChange 調用的話,會致使函數不純。

錯誤姿式1

// 錯誤的父組件1
...
 handleTxtChange(event,idx){ // 重點:【錯誤寫法1】 強行傳了兩個參數
        console.log(event, idx); // 在控制檯上輸出結果
            this.state.list[idx].text = event.target.value; // 把input的值更新到state上去
            this.setState({
                list: this.state.list
            });
            this.initDidCount(); // 更新完成進度
        },
...


// 錯誤的子組件1
...
render (){
        return (
            <li className="li">
                ...
                    {
                    this.state.status?
                        // 重點代碼開始
                        <input type="text" className="ipt" defaultValue={this.props.text} //【錯誤寫法1】 直接調用了父組件的函數,而且直接傳了兩個參數(框架的默認參數event和自定義參數indexonChange={this.props.handleTxtChange(event,this.props.index)}/>:    
                       // 重點代碼結束
                        <p className="p">{this.props.text}</p>
                }
                ...
            </li>
        )
    }
...複製代碼

你會發現,你想要給props的方法裏傳的自定義參數index能正常獲取,
而框架自帶參數event怎麼都拿不到,
結果只能以下,event會變成undefined。

錯誤姿式2

// 錯誤的父組件2
...
 handleTxtChange(event){ // 重點:【錯誤寫法2】 只有框架自帶參數event
        console.log(event.target); // 在控制檯上輸出結果
         let index = event.target.getAttribute('data-index'); // 強行拿到標籤上的自定義屬性
            this.state.list[index].text = event.target.value; // 把input的值更新到state上去
            this.setState({
                list: this.state.list
            });
            this.initDidCount(); // 更新完成進度
        },
...


// 錯誤的子組件2
...
render (){
        return (
            <li className="li">
                ...
                {
                    this.state.status?
                        // 重點代碼開始
                        <input type="text" className="ipt" defaultValue={this.props.text} //【錯誤寫法2】 直接調用父組件的函數,可是不傳參數 // 自定義參數利用自定義屬性的方式傳入, // 此次嘗試,也只是爲了可以拿到正確的event data-index={this.props.index} // 強行使用了自定義屬性 onChange={this.props.handleTxtChange}/>:    // 不帶參數
                       // 重點代碼結束
                        <p className="p">{this.props.text}</p>
                }
                ...
            </li>
        )
    }
...複製代碼

當發現多傳了參數,致使了框架自帶的默認參數event怎麼都取不到的時候,
決定不傳參數,用其餘歪門邪道(好比自定義屬性)拿到想要的參數。
在input中輸入內容,結果以下。雖然正確,但這樣寫感受實在是不夠智能。


總之,這樣寫雖然解決了問題,但我仍是以爲姿式仍是不對。

正確姿式

// 正確的父組件
...
    handleTxtChange(event,idx){// 重點:【正確姿式】 不只帶了框架默認參數event,還帶了自定義參數
        this.state.list[idx].text = event.target.value;
        this.setState({ // 最正常的賦值寫法
            list: this.state.list
        });
        this.initDidCount();
    },
...

// 正確的子組件
...
    handleTxt(event) {
        // 用一箇中轉函數來存onChange時會調用的父組件的函數
        // 並加上任意的參數
        this.props.handleTxtChange(event, this.props.index);
    },

    render (){
        return (
            <li className="li">
                ...
                {
                    this.state.status?
                        // 重點代碼開始
                        <input type="text" className="ipt" defaultValue={this.props.text} // 【正確姿式】調用子組件的中轉函數 onChange={this.handleTxt}/>:
                         // 重點代碼結束
                        <p className="p">{this.props.text}</p>
                }
                    ...
            </li>
        )
    }
...複製代碼

若是這樣寫的話,是達到了和案例1同樣的效果。
中轉函數的效果,保證了render時的函數都是pure function
而且也防止了子組件在掛載時,render順着this.props.function調用父組件的函數
從而避免了一系列錯誤。

案例3

案例3純粹是爲了演示一個增長操做,在增長一條記錄後,須要清空input的內容時踩的坑

// 父組件
    addLiItem(obj) {
        this.state.list.push(obj); // 沒啥好說,就是添加一個元素到list中去
        this.setState({
            list: this.state.list
        });
        this.initDidCount();
    },複製代碼
// 子組件
const Add = React.createClass({
    getInitialState() {
        return {
            addValue: '',
            addStatus: false
        }
    },

    handleAddChange(event) {
        this.setState({
            addValue: event.target.value
        })
    },

    add(){
        this.props.addLiItem({
            text: this.state.addValue,
            status: false
        });
        this.setState({ //【重點部分】
            addValue: ''
        }, ()=>{
            this.refs.addIpt.value = ''; // 利用ref操做dom
        });
    },
    // 若是隻是setState的時候發現完成沒辦法達到清空的效果
    // 這時候的【正確姿式】是去操做dom,必定要操做dom
    render() {
        return (
            <div>
               // 定義了一個ref是addIpt的input標籤
                <input className="ipt" onChange={this.handleAddChange} value={this.addStatus} ref="addIpt"/>
                <button className="btn btn-save" style={{float: 'left'}} onClick={this.add}>添加</button>
            </div>
        )
    }
});複製代碼

究極正確形態

好比案例3

// add子組件部分
render() {
    return (
        <div> 
            // 利用箭頭函數的形式的寫法,可是調用的是子組件裏的方法
            <input className="ipt" onChange={(e)=>this.handleAddChange(e)} value={this.addStatus} ref="addIpt"/>
            <button className="btn btn-save" style={{float: 'left'}} onClick={()=>this.add()}>添加</button>
        </div>
    )
}

// 父組件部分
// 須要一個參數obj,配合父組件的addLiItem方法的參數
// 第一個obj是指,子組件傳遞過來的參數,而後把子組件傳遞過來的參數傳給父組件的addLiItem方法
<Add addLiItem={(obj)=>this.addLiItem(obj)}/>複製代碼

案例2的編輯保存

// 父組件中
// 修改input的值,則須要event和idx兩個參數
handleTxtChange(event, idx){ 
    this.state.list[idx].text = event.target.value;
    this.setState({
        list: this.state.list
    });
    this.initDidCount();
}

// 修改checkbox的值,只須要idx
handleCheckChange(idx) { 
    this.state.list[idx].status = !this.state.list[idx].status;
    this.setState({
        list: this.state.list
    });
    this.initDidCount();
}

// 刪除一條記錄,只須要idx
deleteItem(idx) {
    var temp = this.state.list.splice(idx, 1);
    this.setState({
        list: this.state.list
    });
    this.initDidCount();
}

// 循環輸出todolist
initListLi(val, idx) { 
    return (
        <List {...val} key={idx} index={idx} // 把父組件的方法做爲prop handleTxtChange={(e)=>this.handleTxtChange(e,idx)}
              handleCheckChange={()=>this.handleCheckChange(idx)}
              // 調用父組件的刪除方法須要傳一個idx
              deleteItem={()=>this.deleteItem(idx)}
        />
    )
}

render() {
    return (
        <article className="page">
            ...
            <ul className="ul">
                // 在map中調用父組件自己的方法,並把map的參數傳給initListLi
                // 第一個(val,idx)是指,map方法自帶的參數,而後把子組件傳遞過來的參數傳給父組件的initListLi方法
                {  this.state.list.map((val,idx)=>this.initListLi(val,idx))  }
            </ul>
            ...
        </article>
    )
}

// todolist的一條記錄的子組件
render (){
    return (
        <li className="li">
            <input type="checkbox" checked={this.props.status} data-index={this.props.index} // 不須要`中轉函數` 直接調用propshandleCheckChange方法, onChange={()=>this.props.handleCheckChange()}/>
            {
                this.state.status ?
                    <input type="text" className="ipt" defaultValue={this.props.text} data-index={this.props.index} // 不須要`中轉函數` 直接調用propshandleTxtChange方法,帶一個參數e onChange={(e)=>this.props.handleTxtChange(e)}/> :
                    <p className="p">{this.props.text}</p>
            }
             // 不須要`中轉函數` 直接調用props的deleteItem方法
            <button className="btn btn-danger" onClick={()=>this.props.deleteItem()}>刪除</button>
            {
                this.state.status ?
                    <button className="btn btn-save" onClick={()=>this.saveLiValue()}>保存</button> :
                    <button className="btn btn-save" onClick={()=>this.editLiValue()}>編輯</button>
            }
        </li>
    )
}複製代碼

總結

爲了儘量使用pure function,也爲了保證掛載的時候不要出問題
在子組件須要調用父組件的this.props.function的時候
儘量使用中轉函數,就像page_a_1.js同樣
可是若是你可以正確使用箭頭函數,仍是使用箭頭函數,就像page_a.js同樣
你懂得~~

寫在後面

github地址:Close2React

我是嘉寶Appian,一個賣萌出家的算法妹紙。

相關文章
相關標籤/搜索