帶你找出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>;      }  }

可以看出,32從最大的區別在於,3fn直接綁定在實例的屬性上(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綁定事件的幾種區別時,相信大家都能答出來了。。。。