少踩點坑,你值得知道的React事件繫結

嘉寶Appian發表於2016-12-22

寫在前面

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

github地址:Close2React

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

目前該專案有兩個分支, half-es6 + master

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

half-es6的寫法並沒有完全使用es6的class的概念, master是完善了它

現在讓我們快速瞭解React的事件繫結都有什麼坑~
⬇️ ⬇️ ⬇️

案例1:

tab示例效果圖

少踩點坑,你值得知道的React事件繫結

錯誤示範

// 父元件主要是為了實現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呼叫父元件的方法

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

少踩點坑,你值得知道的React事件繫結

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

順便解釋一下pure function
1、給出同樣的引數值,該函式總是求出同樣的結果。該函式結果值不依賴任何隱藏資訊或程式執行處理可能改變的狀態或在程式的兩個不同的執行,也不能依賴來自I/O裝置的任何外部的輸入
2、結果的求值不會促使任何可語義上可觀察的副作用或輸出,例如易變物件的變化或輸出到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 的 編輯 & 儲存 示例效果圖

少踩點坑,你值得知道的React事件繫結

錯誤示範

// 父元件
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。

少踩點坑,你值得知道的React事件繫結

錯誤姿勢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中輸入內容,結果如下。雖然正確,但這樣寫感覺實在是不夠智慧。

少踩點坑,你值得知道的React事件繫結

總之,這樣寫雖然解決了問題,但我還是覺得姿勢還是不對。

正確姿勢

// 正確的父元件
...
    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的內容時踩的坑

少踩點坑,你值得知道的React事件繫結

// 父元件
    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,一個賣萌出家的演算法妹紙。

相關文章