帶你找出react中,回調函數綁定this最完美的寫法!
- 2020 年 3 月 10 日
- 筆記
相信每一個人寫過
react
的人都對react
組件的的this
綁定有或多或少的了解
在我看來,有若干種this
寫法,我們通過本文,一步步找優缺點,篩選出最完美的react
this
寫法!(有點小激動)
1、遠古時代 React.createClass
說實話,在我接觸react
的時候,這種寫法就只在相關文章見到了。React.createClass
會自動綁定所有函數的this
到組件上
React.createClass({ fn() { // this 指向組件本身 console.log(this); }, render() { return <div onClick={this.fn}></div>; } });
react 0.13
開始就已經支援class
聲明組件了。react 16
已經廢棄了這種寫法,這裡就不討論了。直接淘汰
2、錯誤示範
class App extends React.Component { fn() { console.log(this); } render() { return <div onClick={this.fn}></div>; } }
這種寫法,最終列印this
是指向undefined
。原因在於上面的事件綁定函數調用可以看作如下。
// 偽程式碼 onClick = app.fn; onClick();
在onClick
進行調用時,this
的上下文是全局,由於是在es module
中,全局this
指向undefined
,所以這個錯誤示範的事件處理函數中的this
不是指向組件本身的
3、利用proposal-class-public-fields
直接綁定箭頭函數
class App extends React.Component { fn = () => { console.log(this); }; render() { return <div onClick={this.fn}></div>; } }
目前
proposal-class-public-fields
仍處於提案階段,需要藉助@babel/plugin-proposal-class-properties
這個 babel 插件在瀏覽器中才能正常工作
經過babel
轉換,等價於以下的程式碼
class App extends React.Component { constructor(props) { super(props); this.fn = () => { console.log(this); }; } render() { return <div onClick={this.fn}></div>; } }
可以看出,3
和2
從最大的區別在於,3
將fn
直接綁定在實例的屬性上(2
是綁定在原型的方法上),並利用箭頭函數繼承父級this
作用域達到了this
綁定的效果。
優點:程式碼十分簡潔,不需要手動寫bind
、也不需要在constructor
中進行額外的操作
缺點:很多文章都提到這是一種完美寫法,但其實每一個實例在初始化的時候都會新建一個新事件回調函數(因為綁定在實例的屬性上,每個實例都有一個fn
的方法。本質上,這是一種重複浪費),所以其實並不是很完美
4、Constructor
中使用 bind
class App extends React.Component { constructor(props) { super(props); this.fn = this.fn.bind(this); } fn() { console.log(this); } render() { return <div onClick={this.fn}></div>; } }
優點:fn
函數在組件多次實例化過程中只生成一次(因為是用實例的fn
屬性直接指向了組件的原型,並綁定了this
屬性)
缺點:程式碼寫起來比較繁瑣,需要在constructor
中,手動綁定每一個回調函數
5、在render
中進行bind
綁定
class App extends React.Component { fn() { console.log(this); } render() { return <div onClick={this.fn.bind(this)}></div>; } }
優點:fn
函數多次實例化只生成一次,存在類的屬性上,類似於4
,寫法上比4
稍微好一點。
缺點:this.fn.bind(this)
會導致每次渲染都是一個全新的函數,在使用了組件依賴屬性進行比較、pureComponent
、函數組件React.memo
的時候會失效。
最關鍵的是5
的寫法會被6
全方面吊打完爆
6、箭頭函數內聯寫法
class App extends React.Component { fn() { console.log(this); } render() { return <div onClick={() => fn()}></div>; } }
優點:
1、寫法簡潔
2、與2-5
的寫法相比,6
寫法最大的最大好處就是傳參靈活
3、全面吊打寫法4
,相同的缺點,但是多了傳參數靈活。如果需要渲染一個數組,並且數組根據不同項,事件處理不一樣時,2-5
就很尷尬了
const arr = [1, 2, 3, 4, 5]; class App extends React.Component { fn(val) { console.log(val); } render() { return ( <div> {arr.map(item => ( // 採用 6的寫法,要列印數組這一項就很方便 <button onClick={() => this.fn(item)}>{item}</button> ))} </div> ); } }
網上看多文章都在使用3
的方案的時候推薦使用閉包傳參實現該效果
const arr = ["1", "2", "3", "4", "5"]; class App extends React.Component { fn = val => () => { console.log(val); }; render() { return ( <div> {arr.map(item => ( // 每次也生成了全新的函數了 <button onClick={this.fn(item)}>{item}</button> ))} </div> ); } }
經過前面的分析。使用這種寫法,還不如直接使用6
的內聯寫法,兩種每次都是返回全新的函數,而且,少了一次返回閉包函數的開銷。
缺點: 每次渲染都是一個全新的函數,類似於5
的缺點,在使用了組件依賴屬性進行比較、pureComponent
、函數組件React.memo
的時候會失效
7、函數組件的useCallback
雖然函數組件無this
一說法,但既然講到react
回調函數,還是提一下
在hook
出現之前,函數組件是不能保證每次的回調函數都是同一個的,(雖然可以把回調提到函數作用域外固定,但都是一些 hack 的方法了)
const App = () => { // 每次都是全新的 return <div onClick={() => console.log(2333)}></div>; };
有了hook
。我們便可以使用useCallback
固定住回調
const App = () => { const fn = useCallback(() => console.log(2333), []); // 每次都是固定 return <div onClick={fn}></div>; };
有沒有發現。其實很類似class
組件的將回調掛在class
上,嗯,這就hook
強大的地方,利用了react fiber
,掛在了它的memorizeState
上,實現了能在多次渲染中保持(這就不展開講了)。缺點還是和上面提過的,參數傳遞不方便,如渲染數組
8、(最完美)
的寫法?
當然,如果不使用內聯寫法又獲取到參數行不行呢。當然也是可以的,利用元素的自定義屬性data-
屬性傳遞參數
const arr = ["1", "2", "3", "4", "5"]; class App extends React.Component { constructor(props) { super(props); this.fn = this.fn.bind(this); } fn(e) { // 1 2 3 4 5 console.log(e.target.dataset.val); } render() { return ( <div> {arr.map(item => ( // 每次也生成了全新的函數了 <button data-value={item} onClick={this.fn}> {item} </button> ))} </div> ); } }
orz! 這是最完美寫法了吧!不考慮程式碼繁瑣的情況下,既正確綁定了this
,又不會多次實例化函數,又能渲染數組。。
其實還是錯誤的…data-xxx
屬性只能傳遞string
類型的數據,因為是附加給html
的,react
會進行一步JSON.stringify
的操作,如果你傳遞一個對象,列印出來是value: "[object Object]"
。果然,就算是為了獲取字元串參數,也不推薦這種寫法。可以,但沒必要!
9、最省事的寫法?
有一位大佬寫了一個 babel 插件babel-plugin-react-scope-binding的插件,能夠實現 將2
的錯誤示範自動轉化內聯函數,更牛逼的是還能傳參。介紹。確實是最省事的寫法,不過很容易引起歧義,也有上面提到的問題
好吧,感謝你看到這裡,廢話連篇一篇文章,其實似乎並沒有找回完美的寫法。。。
下面說說本人的一些愚見吧
在平時寫程式碼中,在render
沒有非常大的開銷情況下(也沒有依賴組件的某些屬性進行性能優化、沒使用 pureComponent), 會優先使用純內聯的寫法(無論是函數組件還是 class 組件)。因為重新創建函數開銷我覺得不是特別大的,並且內聯我覺得還有最大的好處就是,看到一個事件調用,不需要再點到事件函數調用的地方…減少了飛來飛去的情況,而且上面也提到,內聯傳遞參數是非常方便的。在實在遇到性能問題,再考慮優化。無需為了優化而優化
最近春招季,看完這篇文章,雖然還是找不出最完美的react
綁定事件寫法,但是面試官提起react
綁定事件的幾種區別時,相信大家都能答出來了。。。。