TS装饰器bindThis优雅实现React类组件中this绑定

2022-11-28 585阅读

温馨提示:这篇文章已超过514天没有更新,请注意相关的内容是否还可用!

初学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,两者的区别主要在内在属性字段上:


configurableenumerablevaluewritablegetset
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