不管是什么样的历史原因, 或者是基于什么样的考虑. 反正现在我们已经接受了 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 官方文档) https://reactjs.org/docs/hooks-intro.html
2. 关于 hooks 的一些问题(react hooks 官方文档) https://reactjs.org/docs/hooks-faq.html
来源: https://juejin.im/post/5beeb0d96fb9a049f91226e1