不管是什么样的历史原因,或者是基于什么样的考虑。反正现在我们已经接受了
JavaScript
中的this
的多面性,以及乐此不疲的使用this
这种多面性,来编写灵活的代码,比如借用其他对象的方法,改变回调函数的调用者等,但有时候我们还是希望this
能够老实一点,别让我们花费很大精力去找寻他。
快速找到
- 函数包装模式 function wrapper pattern
- 渲染绑定模式 render bind pattern
- 覆写绑定模式 rewrite bind pattern
- 属性赋值模式 attribute assignment pattern
- 高阶函数渲染绑定模式 higher-order function render bind pattern
- 高阶函数覆写绑定模式 higher-order function rewrite bind pattern
- 属性getter渲染绑定模式 attribute getter render bind pattern
- 属性getter赋值绑定模式 attribute getter assignment bind pattern
说明
由于本文是主要介绍React中锁定this
的N种方法,不会过多的介绍this
多面性的原因,相信大家应该都知道词法作用域
和动态作用域
。并且也知道在es6
之前我们依然有很多种方式,去锁定this
的指向(call, apply, bind)。接下来我们也会结合这些方式,在React
中来锁定this
;不过在这之前,我们先看下之前我们采取的方式。对于本文标题facade pattern
(外观模式)指的是,这些锁定this
的方式,只是看起来不一样,有些本质上是一样的,有些是es5通过一些技巧实现的
,有些是es6原生支持的(箭头函数)
,这些看起来很不一样的方式,有些在babel
编译以后本质是一样的(使用闭包锁住上下文, 通过高阶函数返回新的函数)。
千好万好ES6
好(箭头函数 () => {}好)
看下在es6之前我们是如何解决this
// 回调里面使用this
var Demo = {
init() {
this.initEvents();
}
validateData() {
return true;
}
initEvents() {
var self = this;
$('a.submit').click(function () {
self.validateData();
});
}
}
Demo.init();
// 借用其他对象的方法
var name = '影帝';
var Person = {
name: '渣渣辉',
sayName() {
console.log(this.name);
}
};
var Other = {
name: '张家辉'
};
var sayName = Person.sayName;
sayName(); // '影帝'
sayName.call(Other); // 张家辉'
Other.sayName = sayName;
var otherSayName = Other.sayName;
otherSayName(); // '影帝'
Other.sayName.call(Person); '渣渣辉',
复制代码
这样看起来好奇怪呀,但是没办法,我们早已经习惯,自从ES2015
称为标准以来,我们已经很少看到这种代码了。好了言归正题,我们开始看看React
中this
的问题。
React中this的问题
既然js
中有这些问题,当然react
也不能列外,使用react
的我们都知道,为了保持组件的高复用,组件可以分为容器组件
和UI组件
,容器组件
组件负责业务处理,UI组件
负责页面渲染,一般情况下UI组件
都尽量要是纯的
,没有自己的状态,也不处理业务,但是有时候需要触发一些事件,这样就需要执行从父组件等传过来的函数,同时这些函数里面一般还会出现this
,我们希望this
的指向是上层组件的引用,而这个时候函数的执行者却不是上层组件,于是this
开始变脸,变得我们不认识。但是我们要避免这种情况,就需要锁定this
,锁定this
的方式有很多,我们一一分析,这其中各有优劣,也有react
推荐的最佳实践
。至于如何选择,看业务场景,以及团队编码风格,建议最好还是遵守最佳实践
;
React中‘锁定’this的N种方法
1.函数包装模式
/**
* 函数包装模式 function wrapper pattern
*/
class Component extends React.Component {
doSomething() {
childDoSomething() {}
render() {
return (
<div onClick={() => this.doSomething();}>
<ChildComponent doSomething={() => this.childDoSomething();} />
</div>
);
}
}
复制代码
建议: 不推荐 也不禁止。
优缺点:
- 缺点:没有明显的缺点,只是需要多包裹一层
- 优点:简单,易于理解,对新手比较友好
实现原理:
这里是通过es6
箭头函数来实现this
的锁定;
当然对应的有es5
版本,其实就是我们之前熟悉的那种方式,且看代码
/**
* function wrapper pattern es5
*/
class Component extends React.Component {
doSomething() {
childDoSomething() {}
render() {
const self = this;
return (
<div onClick={function () { self.doSomething();}>
<ChildComponent doSomething={function () { self.childDoSomething();} />
</div>
);
}
}
复制代码
建议: 不推荐, 最好不要这样写。
优缺点:
- 缺点:需要对this的指向进行保存,导致代码没有箭头函数来的简洁, 优雅(其实也就是箭头函数的优点)
- 优点:对熟悉es5老式写法的比较友好
实现原理:
使用变量先保持对this
的引用,使用的时候是这个变量,也就是此函数外部的this
;
2.渲染绑定模式
/**
* 渲染绑定模式 render bind pattern
* 或者叫
* 懒绑定模式 lazy bind pattern
*/
class Component extends React.Component {
doSomething() {}
childDoSomething() {}
render() {
return (
<div onClick={this.doSomething.bind(this)}>
<ChildComponent doSomething={this.childDoSomething.bind(this);} />
</div>
);
}
}
复制代码
建议: 禁止采取这种模式
优缺点:
- 缺点:有性能隐患,每次
render
都会重新绑定 - 优点:好像也只有看起来稍微好看,不用像在
constructor
里面一样重新辅助
实现原理:
就是使用bing
来锁定
,关于bind
的使用以及原理可以参考mdn
或者网上其他文章或者教程,当然你也可以实现自己的bind
;
3.覆写绑定模式
/**
* 覆写绑定模式rewrite bind pattern
* 或者叫
* 预绑定模式 prepare bind pattern
*/
class Component extends React.Component {
constructor() {
this.doSomething = this.doSomething.bind(this);
this.childDoSomething = this.childDoSomething.bind(this);
}
doSomething() {}
childDoSomething() {}
render() {
return (
<div onClick={this.doSomething}>
<ChildComponent doSomething={this.childDoSomething} />
</div>
);
}
}
复制代码
建议: 建议采用这种方式,也是react最佳实践
推荐的写法。
优缺点:
- 缺点:需要在构造函数里面重写需要绑定
this
的方法,如果这类方法比较多了,就不是那么的优雅了,不过尚可以接受。 - 优点:
react最佳实践
推荐,也是最常见的方式,性能较好,只会绑定一次
实现原理:
和渲染时绑定模式实现原理一样,只是在这种方式下是提前绑定好。
对比:
这种模式和上一种在render
时绑定实现原理是一样的,这种方式只会绑定一次,性能是好于在render
里面的绑定;对比下来在写法上面也有些区别,一个是在constructor
提前绑定,一个是在准备要用的时候懒绑定。
4.属性赋值模式
/**
* 属性赋值模式 attribute assignment pattern
*/
class Component extends React.Component {
doSomething = () => {}
childDoSomething = () => {}
render() {
return (
<div onClick={this.doSomething}>
<ChildComponent doSomething={this.childDoSomething} />
</div>
);
}
}
复制代码
建议: 可以采用,react最佳实践
也有推荐的这种写法。
优缺点:
- 缺点:不被标准所支持(babel以后是没有问题的),写法怪怪的, 要是有很多这种写法, 不够优雅。
- 优点:写法很简单(虽然很怪),不用显式的
bind
。
实现原理:
借用箭头函数在定义的时候就绑定好了this
。
5.高阶函数渲染绑定模式
/**
* 高阶函数渲染绑定模式 higher-order function render bind pattern
* 或者叫 高阶函数懒绑定模式 higher-order function lazy bind pattern
*/
class Component extends React.Component {
doSomething(data) {
return () => {
// 使用this, data
}
}
childDoSomething(data) {
return () => {
// 使用this, data
}
}
render() {
return (
<div onClick={this.doSomething()}>
<ChildComponent doSomething={this.childDoSomething()} />
</div>
);
}
}
复制代码
建议: 可以采用,尝试函数式写法。
优缺点:
- 缺点:不熟悉高阶函数(或者函数式),接受起来有难度,需要调用一次, 每次都产生新的函数。
- 优点:优雅,高阶函数, 可以提前保存一些变量。
实现原理:
利用高阶函数返回箭头函数, 实现this
的锁定。
当然这种方法有对应的es5
版本
/**
* higher-order function es5 pattern
*/
class Component extends React.Component {
doSomething(data) {
cosnt self = this;
return function() {
// self, data
}
}
childDoSomething(data) {
cosnt self = this;
return function() {
// self, data
}
}
render() {
return (
<div onClick={this.doSomething()}>
<ChildComponent doSomething={this.childDoSomething()} />
</div>
);
}
}
复制代码
注意:
这种模式被我称为懒模式,和在render
里面使用bind
的方式很像,在准备要使用的时候才绑定,每次都产生一个新的函数,可能会带来性能问题。当然又这种懒模式,我们也有提前绑定模式。
6.高阶函数覆写绑定模式
/**
* 高阶函数覆写绑定模式 higher-order function rewrite bind pattern
* 或者叫
* 高阶函数预绑定模式 higher-order function prepare bind pattern
*/
class Component extends React.Component {
constructor() {
this.doSomething = this.doSomething();
this.childDoSomething = this.childDoSomething();
}
doSomething(data) {
return () => {
// 使用this, data
}
}
childDoSomething(data) {
return () => {
// 使用this, data
}
}
render() {
return (
<div onClick={this.doSomething}>
<ChildComponent doSomething={this.childDoSomething} />
</div>
);
}
}
复制代码
建议: 可以采用,尝试函数式写法。
优缺点:
- 缺点:不熟悉高阶函数(或者函数式),接受起来有难度,需要调用一次。
- 优点:没有显式绑定,在某些场景下可以提前保存一些变量, 对比上一种模式性能较好。
实现原理:
和上一个高阶函数渲染绑定模式
一样利用高阶函数返回箭头函数, 实现this
的锁定。不同的是这个模式是在构造函数里面提前调用。绑定后函数只会产生一次。
当然这种方法有对应的es5
版本, 和上个模式的es5
版本很像,也是通过变量缓存this
, 不同就是在constructor
里面调用一次函数,而不是在render
里面。
我是分割线,到了最后一种方式了
7.属性getter渲染绑定模式
/**
* 属性getter渲染绑定模式 attribute getter render bind pattern
* 或者叫
* 属性getter懒绑定模式 attribute getter lazy bind pattern
*/
class Component extends React.Component {
get doSomething() {
// 这里也可以使用this, 做一些属性的计算, 比如 this.xxx + this.yyyy
return () => {
// 使用this
}
}
get childDoSomething() {
// 这里也可以使用this, 做一些属性的计算, 比如 this.xxx + this.yyyy
return () => {
// 使用this
}
}
render() {
return (
<div onClick={this.doSomething}>
<ChildComponent doSomething={this.childDoSomething} />
</div>
);
}
}
复制代码
建议: 可以采用,尝试新的写法。
优缺点:
- 缺点:接受起来需要成本, 每次产生新的函数。
- 优点:被标准所支持,没有显式绑定,没有显式调用,比较简洁优雅,可以提前做一些属性的聚合或者计算。
实现原理:
借用属性的getter
, 返回一个箭头函数绑定this
;
说明:
这种模式和高阶函数很像,都是返回一个新的函数,这种模式在特定情况下很强大,简洁的同时,可以对当前对象的一些属性做一些计算(是不是很像Vue
的计算属性), 这种模式下每次getter
后返回的都是一个新的函数,可能会有性能问题,但是如果对其他属性进行了聚合计算,或者说是依赖其他属性的最新值
,就需要在render
里面getter
,以保证依赖的属性都是干净的值
(最新的值);
当然大家知道里面返回的是箭头函数,肯定也有es5
版本,其实和其他模式的es5
版本都很像,在这里就不写了。既然这种模式下有可能产生性能问题,对比其他模式,我们可定也有预绑定模式
。请往下看
8.属性getter赋值绑定模式
/**
* 属性getter赋值绑定模式attribute getter assignment bind pattern
* 或者叫
* 属性getter预绑定模式 attribute getter prepare bind pattern
*/
class Component extends React.Component {
constructor() {
this.doSomethingBind = this.doSomething;
this.childDoSomethingBind = this.childDoSomething
}
get doSomething() {
// 这里也可以使用this, 做一些属性的计算, 比如 this.xxx + this.yyyy
return () => {
// 使用this
}
}
get childDoSomething() {
// 这里也可以使用this, 做一些属性的计算, 比如 this.xxx + this.yyyy
return () => {
// 使用this
}
}
render() {
return (
<div onClick={this.doSomethingBind}>
<ChildComponent doSomething={this.childDoSomethingBind} />
</div>
);
}
}
复制代码
建议: 可以采用,尝试新的写法。
优缺点:
- 缺点:接受起来需要成本,赋值的函数需要另外一个名字 。
- 优点:被标准所支持,没有显式绑定,只产生一次函数,比较简洁优雅,可以提前做一些属性的聚合或者计算。
实现原理:
借用属性的getter
, 返回一个箭头函数绑定this
;赋值给对象的另外一个属性,调用的是另外一个方法。
说明:
这种模式和上一种模式区别在于,提前绑定,只会产生一次函数。但是要注意不是重写函数,而是赋值给另外一个不同的方法名,可能大家觉得这种换名字不够好,但是换个角度考虑一下,系对象多了一个方法,同时又持有之前的getter
,这样可以更加的灵活,可以选择性的使用这两种函数。
同其他一些返回箭头的模式一样,这种模式依然有es5
版本,写法同上,不在赘述。
总结
上面列举的这些模式,不一定是全部写法,不过足以应对工作中的大多数场景,同时有些模式还可以让我们去接触另外的实现方式。列举了这么多种,每一种都有优劣,工作中可以选择性的去使用,看场景和团队风格。
展望
既然JavaScript
中this
的问题一直困扰着我们,那么有没有一种方式可以不使用this
,就可以实现我们想要的所有功能,答案是肯定的。React 16.7.0-alpha
版本加入了特别神奇的hooks
(好像Vue 3.0里面也已经加入了相似的特性),可以让我们彻底摆脱this
的困扰(当然this
依然是js
里面一个神奇的存在),同时让我们的代码更加函数式
,更大程度的复用处理逻辑
,当然这个特性还在等待成为事实标准,我们希望这一天很快到来,不过我们仍然可以现在就是使用它。
相关链接
1.可以让我们不用关心this绑定问题(react hooks 官方文档)
2.关于hooks 的一些问题(react hooks 官方文档)