Vue2的this陷阱
前言
温馨提示:在阅读本文之前需要有一定的ts基础,可以学习一下ts官方文档,也可以参考文章末尾的附录
大约3年前,也就是2020年9月18日晚11点半,万众瞩目的
Vue3
发布了Release版本,时至今日Vue3
生态圈已经非常丰富,将Vue3
用于生产环境也不在话下;但是请注意,在2020年9月18日晚上11点半之前开发的项目大多数还是用的Vue2
,Vue2
不会死,他还会在国内存在很多年!但是
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对象,显然编辑器的直觉和我们一样,如下图:
但是事实并非如此,如下图,this
指向Vue组件实例,可以看到methods中的方法和data中的数据都直接暴露在最外层可以直接获取,这是因为Vue并不是直接使用options对象调用函数,而是将options传入一个构造函数,所以违反了直觉!
编辑器为什么也错了?
编辑器内部借助的是ts,它是一种静态类型检查
,也就是说它只能检测到代码定义的那一刻的类型,如果通过各种骚操作改变了函数的this指向,ts是检测不出来的,需要开发者自行修正this,这里也是最容易出现问题的地方。因此Vue中this的指向问题其实需要通过类型定义来修正,这样编辑器的提示才能正常,但是应该怎么做呢?
最简单的方式就是:放弃Vue2,拥抱Vue3吧!
Vue3修正this指向
在Vue3中写options API开发体验比Vue2要好得多,但是谁会在Vue3中写options API呢?写composition API它不香吗?
废话不多说,Vue3中提供了一个方法:defineComponent
,用这个方法去包裹我们的对象,然后所有的this都指向正常了:
问题来了,defineComponent怎么改变的ts中的this指向?如果这个函数让我来设计,我该怎么去设计?我就是把脑子想破了我也想不出来,我只知道js中可以通过bind绑定this,ts中可以通过第一个参数去设置this绑定。所以我想抄一抄答案(看看源码怎么做的):
这就是defineComponent函数的实现,什么,就这?其实就是把对象类型的options直接原封不动返回了啊!别急,背后还有ts撑腰,看看ts对于defineComponent的类型定义:
这是啥,完全不是人能够看懂的!没必要完全看懂,只需要找到解开谜团的钥匙就行了;最终目标是改变this,看有没有和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的类型是否正确:
截止到这里确实将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
}>;
上面的实现还有一个问题:对于对象类型的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;
}
后记
总结一下本文的总体脉络:
- Vue2中的this违反直觉,编辑器不能正确解析
- Vue3中的defineComponent可以帮助编辑器解析this
- 实现一个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相关文章推荐
转载自:https://juejin.cn/post/7232864896743735352