TS装饰器bindThis优雅实现React类组件中this绑定
温馨提示:这篇文章已超过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 descriptor
和 accessor 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