likes
comments
collection
share

Vue2的this陷阱

作者站长头像
站长
· 阅读数 25

前言

温馨提示:在阅读本文之前需要有一定的ts基础,可以学习一下ts官方文档,也可以参考文章末尾的附录

大约3年前,也就是2020年9月18日晚11点半,万众瞩目的Vue3发布了Release版本,时至今日Vue3生态圈已经非常丰富,将Vue3用于生产环境也不在话下;但是请注意,在2020年9月18日晚上11点半之前开发的项目大多数还是用的Vue2Vue2不会死,他还会在国内存在很多年!

但是Vue2非常不好用,在开发体验方面options API最大的问题之一就是this的指向问题;

抛出问题

Vue2中的options API一般会这样写js逻辑:

export default {
    data(){
        return {}
    },
    created(){
        console.log("this指向谁",this);
    },
    methods:{
        callback(){
            console.log("callback方法this指向谁",this);
        }
    }
}

上面的代码导出了一个对象,对象在调用方法时this都是指向对象本身,例如在created中this的指向应该是整个对象,callback中的this指向的是methods对象,显然编辑器的直觉和我们一样,如下图:

Vue2的this陷阱

但是事实并非如此,如下图,this指向Vue组件实例,可以看到methods中的方法和data中的数据都直接暴露在最外层可以直接获取,这是因为Vue并不是直接使用options对象调用函数,而是将options传入一个构造函数,所以违反了直觉!

Vue2的this陷阱

编辑器为什么也错了?

编辑器内部借助的是ts,它是一种静态类型检查,也就是说它只能检测到代码定义的那一刻的类型,如果通过各种骚操作改变了函数的this指向,ts是检测不出来的,需要开发者自行修正this,这里也是最容易出现问题的地方。因此Vue中this的指向问题其实需要通过类型定义来修正,这样编辑器的提示才能正常,但是应该怎么做呢?

最简单的方式就是:放弃Vue2,拥抱Vue3吧!

Vue3修正this指向

在Vue3中写options API开发体验比Vue2要好得多,但是谁会在Vue3中写options API呢?写composition API它不香吗?

废话不多说,Vue3中提供了一个方法:defineComponent,用这个方法去包裹我们的对象,然后所有的this都指向正常了:

Vue2的this陷阱

问题来了,defineComponent怎么改变的ts中的this指向?如果这个函数让我来设计,我该怎么去设计?我就是把脑子想破了我也想不出来,我只知道js中可以通过bind绑定this,ts中可以通过第一个参数去设置this绑定。所以我想抄一抄答案(看看源码怎么做的):

Vue2的this陷阱

这就是defineComponent函数的实现,什么,就这?其实就是把对象类型的options直接原封不动返回了啊!别急,背后还有ts撑腰,看看ts对于defineComponent的类型定义:

Vue2的this陷阱

这是啥,完全不是人能够看懂的!没必要完全看懂,只需要找到解开谜团的钥匙就行了;最终目标是改变this,看有没有和this相关的定义,发现有如下定义:

Vue2的this陷阱

看来关键就在于ThisType,当定义了ThisType之后,对象中的所有方法的this都指向它;啊,恍然大悟,看来关键点就在于ThisType!那么下面也不用再去看源码怎么写了,接下来直接从0到1撸一个defineComponent!

实现defineComponent!

首先需要借助泛型,解析出options中的数据结构,由于options中结构相对固定,所以可以直接通过key来获取参数的类型,比如定义一个泛型参数T,那么T['data']就代表了data的类型,而T['methods']就代表了methods的类型,所以得到如下定义:

export function defineComponent<T>(options: T & ThisType<ReturnType<T["data"]> & T["methods"]>) {
  return options;
}

js文件中不能直接引入ts,那么就新增一个index.js,它的内容就是接收options然后又原封不动返回出去:

export default function defineComponent(options) {
    return options;
}

ts的文件名改为index.d.ts,这个时候在App.vue中引入defineCompoent时,会自动去index.d.ts中寻找类型定义,最后看一下引入defineComponent之后this的类型是否正确:

Vue2的this陷阱

截止到这里确实将data、methods中的属性都解析出来了,但是这里还不支持data、computed、created这些选项的默认提示。要实现的话非常简单,只需要定义一个组件options类型别名VueComponentOptions,然后让泛型参数扩展自VueComponentOptions就可以了:

type VueComponentOptions = {
    data?:Function
    props?:Object
    methods?:Object
    computed?:Object
}

export function defineComponent<T extends VueComponentOptions>(options: T & ThisType<ReturnType<T["data"]> & T["methods"]>) {
  return options;
}

接下来如果要定义props就比较困难了,因为props分为两种,第一种数组形式,第二种是对象形式,数组形式的话数组中每一个值都应该添加到this的key中,如果是对象那么就是把对象的key添加到this对象的key上。

// 定义一个ResolveProps的类型别名,专门处理props
type ResolveProps<T> = T extends any[]
  ? {
      [K in Pick<T, keyof T>[number]]: any;
    }
  : Pick<T, keyof T>;
  
// 测试一下
type R = ResolveProps<["a", "b"]>;
type P = ResolveProps<{
    isBest:boolean,
    isGood:boolean
}>;

Vue2的this陷阱

Vue2的this陷阱

上面的实现还有一个问题:对于对象类型的props,能够在this上获取到,但是它的值的类型是有问题的,应该取自type,所以只需要遍历T上的每一个key,然后值的类型就是取type属性;但是还需要注意的是,这里的type都是构造函数,需要取实例类型,可以直接使用InstanceType:

type ResolveProps<T> = T extends any[]
  ? {
      [K in Pick<T, keyof T>[number]]: any;
    }
  : {
      [K in keyof T]: InstanceType<T[K]["type"]>;
    };

至此,Vue2中的data、methods、computed、props、生命周期函数中的this都能够正常提示,完整代码如下:

// index.js
export default function defineComponent(options) {
    return options;
}

// index.d.ts
type VueComponentOptions = {
  data?: Function;
  props?: Object;
  methods?: Object;
  computed?: Object;
};

type ResolveProps<T> = T extends any[]
  ? {
      [K in Pick<T, keyof T>[number]]: any;
    }
  : {
      [K in keyof T]: InstanceType<T[K]["type"]>;
    };

export function defineComponent<T extends VueComponentOptions>(
  options: T & ThisType<ReturnType<T["data"]> & T["methods"] & ResolveProps<T["props"]>>
) {
  return options;
}

后记

总结一下本文的总体脉络:

  1. Vue2中的this违反直觉,编辑器不能正确解析
  2. Vue3中的defineComponent可以帮助编辑器解析this
  3. 实现一个defineComponent让编辑器在Vue2中也能正常解析this

在书写本文的过程中阅读了Vue3ts部分的源码,发现很多ts代码其实根本无法阅读,非常难以理解,所以在实际项目中如果在ts功底不够的情况下贸然使用ts编码,有可能会产出比js更难以维护的代码。

另外需要注意的是在Vue2中不要使用ts,在Vue3中也需要慎重考虑。

附录

针对本文涉及到的一些ts的常识进行解释:

  • T['data']:从T类型中取data属性的类型,ts中不支持点语法,只能用中括号取
  • ThisType:指定对象中的this的类型,与之相对应的是函数中第一个参数也可以指定this类型
  • ReturnType:获取函数的返回值类型
  • type与interface区别:type是类型别名,而interface是接口,interface其实已经不是类型的范畴了,在Java中class必须实现interface
  • extends:判断是否是子集关系,一般结合三目运算符,例如判断是否为数组type IsArray = T extends any[] ? true : false
  • Pick:lib提供的工具方法用来获取对应key的值类型,有两个参数,第一个参数为原类型,第二个参数为key的集合
  • InstanceType:获取实例类型

Vue相关文章推荐