React事件绑定终极优化方案
React
作为目前炙手可热的前端框架,里面有很多吸引人的地方,但是也有一些开发体验不太好的地方,比如我们平时做事件绑定的时候要显示的绑定this
,否则就可能导致各种bug
,关于事件this
绑定也有很多种形式,各种方法都有优劣,下面我们将对照几种绑定方式来进行对比,最终实现一个适合自己的方案
在构造函数中进行绑定
class App extends Component {
constructor (props) {
super(props)
this.state = {
t: 't'
}
// this.bind1 = this.bind1.bind(this) 无参写法
this.bind1 = this.bind1.bind(this, this.state.t)
}
// 无参写法
// bind1 () {
// console.log('bind1', this)
// }
bind1 (t, event) {
console.log('bind1', this, t, event)
}
render () {
return (
<div>
<button onClick={this.bind1}>打印1</button>
</div>
)
}
}
这种方式的优点就是只会生成一个方法实例,并且绑定一次之后如果多次用到这个方法也不需要再绑定。缺点是如果我们直接在constructor
中进行绑定的话,参数就无法动态化,只能固定死用state
里的值,比如我们一个列表组件, 点击某一条某一个具体操作的时候,需要传入一个id
之类的字段作为唯一标识,这种绑定形式就无法处理了,缺点二就是即使不用到state
,也需要在构造器里做绑定
行内匿名函数
bind3 (t, event) {
console.log('bind3', this, t, event)
}
render () {
return (
<div>
// <button onClick={() => this.bind3()}>打印3</button> 无参写法
<button onClick={(event) => this.bind3(this.state.t, event)}>打印3</button>
</div>
)
}
这种方式的优点就是比较简单,灵活,但是最大的缺点就是在每次render
的时候都会执行这个匿名函数,当这个函数作为props
传入低阶组件的时候,这些组件可能会进行额外的重新渲染,因为每一次都是新的方法实例作为的新的属性传递,带来了额外的性能开销
在render
中显示bind
bind3 (t, event) {
console.log('bind3', this, t, event)
}
render () {
return (
<div>
// <button onClick={this.bind3.bind(this)}>打印3</button> 无参写法
<button onClick={this.bind3.bind(this, xxx)}>打印3</button>
</div>
)
}
这种写法虽然和匿名函数写法上完全不一样,但是缺点基本上可以认为是一样的 😂,也会带来额外的性能开销
使用属性初始化器语法绑定this
bind3 = () => {
this.setState({
xxx: 'xxx'
})
}
render () {
return (
<div>
<button onClick={this.bind3}>打印3</button>
</div>
)
}
这种方法利用了箭头函数中箭头函数内部的this是词法作用域,由上下文确定
的特点,写法上比较简单,且不会带来额外性能开销,并且不会像第一种那样带来多余代码,看似很完美,但是和在constructor
一样,无法将参数动态化
::this.xxx()
bind5(){
console.log('bind5', this)
}
render() {
return (
<div>
<button onClick={::this.bind5}></button>
</div>
)
}
这种方法我自己没用过,所以不知道具体有什么优缺点
上面几种是目前我们写React
组件时,基本上都会用到的几种this
绑定方式,通过分析来看,多多少少都有一点缺点,目前也有很多开源比如[autobind](https://www.npmjs.com/package/auto-bind或者[memo-bind](https://www.npmjs.com/package/memo-bind,但是也会有部分不足,所以决定自己实现一个:
bind-with-arguments
先来看看基础版本的:
function bindWithArguments(target, name, descriptor) {
// 缓存最终要执行的方法
const excuter = target[name];
// 对该方法进行重新包装
return Object.assign(target, descriptor, {
value: (...args) => {
return () => {
// 用apply来绑定作用域以及传入外部参数, 并且把执行结果作为返回值
return excuterType.apply(target, args);
};
}
});
}
在测试的过程中发现,上述版本只能满足同步的回调函数,但是很多时候,我们会根据用户操作来发起某个具体的异步请求,所以仅仅上面那部分是不行的,根据[MDN](https://developer.mozilla.org/上对[async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction的介绍,我们来完善代码:
function bindWithArguments(target, name, descriptor) {
// 缓存最终要执行的方法
const excutor = target[name];
// 普通方法: [object Function] async: [object AsyncFunction]
const excutorType = {}.toString.call(excutor).slice(8, -1);
const isAsync = excutorType === 'AsyncFunction';
return Object.assign(target, descriptor, {
value: (...args) => {
return isAsync ? async () => {
// 如果是异步方法,就用async/await包一层再返回出去
return (await excutor.apply(target, args));
} : () => {
return excutor.apply(target, args);
}
}
});
}
现在异步方法已经支持了,看下具体用法
const deffer = () => new Promise((resolve) => {
setTimeout(resolve, 2000);
});
async asyncFn(a, b) {
await deffer();
return 'deffer';
}
// 装饰同步方法
@bindWithArg
async callAsync() {
console.time();
// 内部再调用异步方法
const res = await this.asyncFn(1, 2);
console.timeEnd();
// 后续工作
}
// 装饰同步方法
@bindWithArg
syncFn(a, b) {
console.log(a, b);
}
render() {
return (
<div>
<h3>bindWithArguments</h3>
// 调用同步方法
<p onClick={this.syncFn('1', {
a: 'xxx',
b: 'yyy'
})}>sync fn</p>
// 调用异步方法
<p onClick={this.callAsync()}> async click fn</p>
</div>
)
}
看上面的例子已经很清楚,但是其实还有一点不太完美的地方,就是我们不需要传入任何参数的时候也是用onClick={ this.xxx() }
来调用,对于用惯了autobind
的我们可能不太习惯,所以我们继续完善:
const functionMap = {
// 异步并且有参数
AsyncWith: (excutor, target, ...argus) => {
return async () => (await excutor.apply(target, argus))
},
// 异步无参数
AsyncEmpty: async (excutor, target) => {
return (await excutor.call(target));
},
// 同步并且有参数
SyncWith: (excutor, target, ...argus) => {
return () => excutor.apply(target, argus);;
},
// 同步无参数
SyncEmpty: (excutor, target) => {
return excutor.call(target);
}
};
function bindWithArg(target, name, descriptor) {
const excutor = target[name];
const excutorType = {}.toString.call(excutor).slice(8, -1);
// 判断是否异步
const asyncCall = excutorType === 'AsyncFunction' ? 'Async' : 'Sync';
// 判断是否有参数
const emptyCall = excutor.length === 0 ? 'Empty' : 'With';
const fnType = `${asyncCall}${emptyCall}`;
return Object.assign(target, descriptor, {
value: functionMap[fnType].bind(null, excutor, target)
});
}
至此,就完成了所有功能,主要是用到闭包和函数形参的一些知识点,本项目已经开源到github,需要了解详细的请移步
需要注意的是,此装饰器不支持使用属性初始化器语法绑定this
的写法,因为这种写法在定义该函数时,已经做了绑定,所以无需做重复工作