rwson

rwson

一个前端开发

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的写法,因为这种写法在定义该函数时,已经做了绑定,所以无需做重复工作