1. 程式人生 > >規避 React 元件中的 bind(this)

規避 React 元件中的 bind(this)

React 元件中處理 onClick 類似事件繫結的時候,是需要顯式給處理器繫結上下文(context)的,這一度使程式碼變得冗餘和難看。

請看如下的示例:

class App extends Component {
  constructor() {
    super();
    this.state = {
      isChecked: false
    };
  }
  render() {
    return (


      <div className="App">
        <label >
          check me:
          <
input type="checkbox" checked={this.state.isChecked} onChange={this.toggleCheck} /> </label> </div> ); } toggleCheck() { this.setState(currentState => { return { isChecked: !currentState.isChecked }; }); } }

頁面上放了一個 checkbox 元素,點選之後切換其選中狀態。這是很直觀的一段程式碼,但並不會像你想的那樣正常工作。

事件處理器上下文丟失的報錯
事件處理器上下文丟失的報錯

因為 checkboxonChange 事件處理器中,找不到 React 元件的 setState 方法,這說明其執行時的上下文不是該元件,而是別的什麼東西,具體我們來除錯下。

除錯檢視丟失上下文後 this 的值
除錯檢視丟失上下文後 this 的值

出乎意料,是 undefined,這個方法在一個完全野生的環境下執行,沒有任何上下文。

WHY

當然這並不是 React 的鍋,這是 JavaScript 中 this 的工作原理。具體可參見 Chapter 2: this All Makes Sense Now!

來追溯其底層原因,簡單來講 this 的值取決於函式呼叫的方式。

預設的繫結

function display(){
  console.log(this)
}

display() // 嚴格模式下為全域性 `window`,非嚴格模式下為 `undefined`

隱式繫結

通過物件來呼叫,該函式的上下文被隱式地指定為該物件。

var obj = {
  name: 'Nobody',
  display: function(){
    console.log(this.name);
   }
 };
 obj.display(); // Nobody. 裡面取的是 obj 身上的 `name` 屬性 

但,如果把該物件上的方法賦值給其他變數,或通過引數傳遞的形式,再執行,那光景就又不一樣了。

var obj = {
  name: "Nobody",
  display: function() {
    console.log(this.name);
  }
};

var name = "global!";
var outerDisplay = obj.display;
outerDisplay(); // global! 這裡取到的 `name` 是全域性中的內個

這裡賦值給 outerDisplay 後再呼叫,等同於呼叫一個普通函式,而不是物件中的那個,所以此時 this 為全域性物件,剛好全局裡面有定義一個 name 變數。同樣地,如果是嚴格模式下,因為此時 thisundefined,所以訪問不到所謂的 undefiend.name,於是會拋錯。

function invoker(fn) {
  fn();
}

setTimeout( obj.display, 1000 ); // global!
invoker(obj.display); // global!

這裡 setTimeout 呼叫的時候,因為它的簽名實際上是 setTimeout(fn,delay),所以,可以理解為將 obj.display 賦值給了它的入參 fn,實際上執行的是 fn 而不再是物件上的方法了。對於 invoker 函式也是一樣的道理。

強制繫結

這個時候,bind 就成了那個拯救世界的英雄,任何時間我們都可以通過它來顯式地指定函式的執行上下文。

var name =global!”;
obj.display = obj.display.bind(obj); 
var outerDisplay = obj.display;
outerDisplay(); // Nobody

bind 將指定的上下文與函式繫結後返回一個新的函式,這個新函式再拿去賦值或傳參什麼的都不會對其上下文產生影響了,執行時始終是我們指定的那個。

現場還原

有了上面的背景,就可以還原文章開頭的問題了,即事件處理器的上下文 丟失的問題。

JSX 中的 HTML 標籤本質上對應 React 中建立該標籤的一個函式。比如你寫的 div 編譯會其實是 React.createElement(‘div’)。所以當你書寫 <Input> 時其實是呼叫了 React.createElement 來建立一個 <Input> 標籤。

React.createElement(
  type,
  [props],
  [...children]
)

標籤上的屬性會作為 props 引數傳遞給 createElement 函式。

<Input onChange={this.toggleCheck}> 表示將元件中的 toggleCheck 方法賦值給 createElement 的入參 propsprops 是個物件,接收所有書寫在標籤上的屬性,),實際呼叫的時候一如上面所說的,呼叫的已經不是元件中的 toggleCheck 方法了。

React.createElement(type, props){
  // 讓我們建立一個 <type> 並在 <type> 的值發生變化的時候呼叫一下 `props.onChange`
  ...
  props.onChange() // 它已經不是原來的方法了,丟失了上下文
  ...
}

因為 ES6 的 Class 是在嚴格模式下執行的,所以事件處理器中如果使用了 this 那它就是 undefined

所以你看到 React 官方的示例中,constructor 裡有 bind(this) 的語句就不奇怪了,就是為了糾正這個事件處理器歪了的執行上下文。

  constructor() {
    super();
    this.state = {
      isChecked: false
    };
+  this.toggleCheck = this.toggleCheck.bind(this);
  }

這樣是能正常工作了,但是,這句程式碼的存在真的很彆扭,因為,

  • 對於業務來說,毫無意義,徒增程式碼量
  • 很醜陋,每加一個處理器就要加一條這樣的繫結
  • 冗餘,這樣重複的程式碼大量冗餘在專案中,在搜尋中混淆了原本的方法

避免的方式有很多,就看哪種最對味。下面來看看如何避免寫這些繫結方法。

#0行內的繫結

最簡單的可以在行內進行繫結操作,這樣不用單獨寫一句出來。

   <input
            type="checkbox"
            checked={this.state.isChecked}
-             onChange={this.toggleCheck}
+            onChange={this.toggleCheck.bind(this)}
      />

#1箭頭函式

因為箭頭函式不會建立新的作用域,其上下文是語義上的(lexically)上下文。所以在繫結事件處理器時,直接使用剪頭函式是很方便的一種規避方法。

          <input
            type="checkbox"
            checked={this.state.isChecked}
-             onChange={this.toggleCheck}
+            onChange={() => this.toggleCheck()}
          />

#2將類的方法改成屬性

如果將這個處理器作為該元件的一個屬性,這個屬性作為事件的處理器以箭頭函式的形式存在,執行的時候也是能正常獲取到上下文的。

-  toggleCheck() {
+  toggleCheck = () => {
    this.setState(currentState => {
      return {
        isChecked: !currentState.isChecked
      };
    });
  }

總結

React 元件中,其實跟 React 沒多大關係,傳遞事件處理器,或方法作為回撥時,其上下文會丟失。為了修復,我們需要顯式地給這個方法繫結一下上下文。除了常用的在構造器中進行外,還可通過箭頭函式,公有屬性等方式來避免冗餘的繫結語句。

相關資源