TS裝飾器bindThis優雅實現React類組件中this綁定

為什麼this會是undefined

初學React類組件時,最不爽的一點應該就是 this 指向問題瞭吧!初識React的時候,肯定寫過這樣錯誤的demo。

import React from 'react';
export class ReactTestClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: 1 };
  }
  handleClick() {
    this.setState({a: 2})
  }
  render() {
    return <div onClick={this.handleClick}>{this.state.a}</div>;
  }
}

上面的代碼在執行 onClick 時,就會如期遇到如下的錯誤…

🤔 this 丟失瞭。編譯React類組件時,會將 jsx 轉成 React.createElement,並onClick 事件用對象包裹一層傳參給該函數。

// 編譯後的結果
class ReactTestClass extends _react.default.Component {
  constructor(props) {
    super(props);
    this.state = {
      a: 1
    };
  }
  handleClick() {
    this.setState({
      a: 2
    });
  }
  render() {
    return /*#__PURE__*/ _react.default.createElement(
      "div",
      {
        onClick: this.handleClick // ❌ 鬼在這裡
      },
      this.state.a
    );
  }
}
exports.ReactTestClass = ReactTestClass;

寫到這裡肯定會讓大傢覺得是 React 在埋坑,其實不然,官方文檔有澄清:

這並不是 React 自身的行為: 這是因為 函數在 JS 中就是這麼工作的。通常情況下,比如 onClick={this.handleClick} ,你應該 bind 這個方法。

經受過面向對象編程的洗禮,為什麼還要在類中手動綁定 this? 我們參考如下代碼

class TestComponent {
    logThis () {
        console.log(this); // 這裡的 `this` 指向誰?
    }
    privateExecute (cb) {
         cb();
    }
    execute () {
        this.privateExecute(this.logThis); // 正確的情況應該傳入 this.logThis.bind(this)
    }
}
const instance = new TestComponent();
instance.execute();

上述代碼如期打印瞭 undefined。就是在 privateRender 中執行回調函數(執行的是 logThis 方法)時,this 變成瞭 undefined。寫到這裡可能有人會提出疑問,就算不是類的實例調用的 logThis 方法,那 this 也應該是 window 對象。

沒錯!在非嚴格模式下,就是 window 對象,但是(知識點) 使用瞭 ES6 的 class 語法,所有在 class 中聲明的方法都會自動地使用嚴格模式,故 this 就是 undefined

所以,在非React類組件內,有時候也得手動綁定 this

優雅的@bindThis

使用 .bind(this)

render() {
    return <div onClick={this.handleClick.bind(this)}>{this.state.a}</div>;
}

或箭頭函數

handleClick = () => {
    this.setState({a: 2})
}

都可以完美解決,但是早已習慣面向對象和喜歡搞事情的我總覺得處理的不夠優雅而大方。最終期望綁定this的方式如下,

import React from 'react';
import { bindThis } from './bind-this';
export class ReactTestClass extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: 1 };
  }
  @bindThis // 通過 `方法裝飾器` 自動綁定this
  handleClick() {
    this.setState({ a: 2 });
  }
  render() {
    return <div onClick={this.handleClick}>{this.state.a}</div>;
  }
}

對於 方法裝飾器,該函數對應三個入參,依次是

export function bindThis(
    target: Object, 
    propertyKey: string, 
    descriptor: PropertyDescriptor,
) {
    // 如果要返回值,應返回一個新的屬性描述器
}

target 對應的是類的 prototype

propertyKey 對應的是方法名稱,字符串類型,例如 "handleClick"

descriptor 屬性描述器

對於 descriptor 能會比較陌生,當前該屬性打印出來的結果是,

{
  value: [Function: handleClick],
  writable: true,
  enumerable: false,
  configurable: true
}

參看 MDN 上的 Object.defineProperty,我們發現對於屬性描述器一共分成兩種,data descriptoraccessor descriptor,兩者的區別主要在內在屬性字段上:

configurable enumerable value writable get set
data descriptor
accessor descriptor

✅ 可以存在的屬性,❌ 不能包含的屬性

其中,

configurable,表示兩種屬性描述器能否轉換、屬性能否被刪除等,默認 false

enumerable,表示是否是可枚舉屬性,默認 false

value,表示當前屬性值,對於類中 handleClick 函數,value就是該函數本身

writable,表示當前屬性值能否被修改

get,屬性的 getter 函數。當訪問該屬性時,會調用此函數。執行時不傳入任何參數,但是會傳入 this 對象(由於繼承關系,這裡的this並不一定是定義該屬性的對象)

set,屬性的 setter 函數。當屬性值被修改時,會調用此函數。該方法接受一個參數(也就是被賦予的新值),會傳入賦值時的 this 對象。

既然 get 函數有機會傳入 this 對象,我們就從這裡入手,通過 @bindThis 裝飾器給 handleClick 函數綁定真正的 this

export function bindThis(
    target: Object, 
    propertyKey: string, 
    descriptor: PropertyDescriptor,
) {
    const fn = descriptor.value; // 先拿到函數本身
    return {
        configurable: true,
        get() {
            const bound = fn.bind(this); // 這裡的 this 是當前類的示例
            return bound;
        }
    }
}

bingo~~~

一個優雅又不失功能的 @bindThis 裝飾器就這麼愉快地搞定瞭。

參考

考慮邊界條件更為詳細的 @bindThis 版本可參考:autobind-decorator

以上就是TS裝飾器bindThis優雅實現React類組件中this綁定的詳細內容,更多關於React類組件this綁定的資料請關註WalkonNet其它相關文章!

推薦閱讀: