实习入职第四天了,通过阅读项目代码发现业务开发中前辈们使用vue2都是基于class-styled-component 去开发的,不去采用options 风格去开发组件自然有它的出发点与考量,我认为核心在与class-styled-componet对于ts有更好的支持。并且基于这个问题的思考我翻看了一下vue-class-component和vue-property-decorator的源码,理清了他们的底层运行逻辑,并做了下文的记录。
vue与ts
说到vue3相较于vue2的优势,大概率会提到vue3有更好的typescript支持,社区里有不少人说因为vue3是用ts写的,确实用ts写的话自然vue项目打包出来就顺带得到了.d.ts
类型声明文件,我觉着这个勉强算个原因,但不主要。
我认为vue2之所以对ts支持差,是因为它options api(配置式)的风格,它的组件就是一个对象,方法、计算属性、watch回调...啥都属于这个组件对象,但ts的类型检查能力不是针对对象的!而是针对函数和类class的,这也就是为啥vue2中如果想用ts,一般都会选择Vue2+TypeScript+vue-class-component+vue-property-decorator的技术栈体系,以OO(面向对象)的方式开发Vue程序。
本质上vue-class-component
就是把class风格的组件最终转化成options配置组件,至于vue-property-decorator
就是基于vue-class-component
提供的自定义装饰器的能力,把原先的option配置通过装饰器的形式提供给我们,方便开发。
至于vue3,它组合式api的风格,把vue2中的各种配置都转化为了用函数来定义,这样自然而然就与ts的类型检查能力相契合了。
vue-class-component原理
我们写一个类组件时会用Component
装饰器去修饰class
,下面是Component
源码:
vue-class-component/src/index.ts:
typescript
function Component <V extends Vue>(options: ComponentOptions<V> & ThisType<V>): <VC extends VueClass<V>>(target: VC) => VC
function Component <VC extends VueClass<Vue>>(target: VC): VC
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
if (typeof options === 'function') {
return componentFactory(options)
}
return function (Component: VueClass<Vue>) {
return componentFactory(Component, options)
}
}
说白了Component
就是一个重载函数,目的就是支持多种传参形式,本质是一个装饰器工厂(即return function xxx
创建装饰器函数),而装饰器函数的逻辑就是对componentFactory
函数的调用并返回。
vue-class-component/src/component.ts:
经过上面的源码分析,给人的最直观的感觉就是:@Component装饰器在装饰class类组件时做的事情类似于一种对所有class属性的"拦截",拦截过程中同步构造options对象组件。 所以我们也可以明确,vue-class-component
将class组件处理成options组件也不是在编译时,而是运行时。
vue-property-decorator原理
我们实际开发用的不是vue-class-component
,而是基于它二次开发的库vue-property-decorator
,它提供了若干class属性的装饰器,如@Watch
、@Prop
等,其实本质就是让我们借助这些装饰器将class属性转化为vue组件的options对象的属性。
vue-class-component拓展机制
要想理解vue-property-decorator
的原理,首先我们要明白vue-class-component
是如何对外提供扩展能力的:
回顾@Component
装饰器的实现,见上面github源码截图的第70-75行:
上面的代码我们尝试从Component
,也就是class组件类身上拿到 __decorator__
数组并遍历,从代码可知, __decorator__
中存储的是函数回调,我们依次触发所有回调时会把 @Component
装饰器正在构造的组件options
对象 作为参数 传递给回调函数。
也就是说如果我们想基于vue-class-component拓展能力的话,我们可以通过给class添加__decorator__
数组来完成(这样我们就有能力介入options组件对象的构造)。
这里有个需要注意的知识点------装饰器的执行顺序:首先执行属性装饰器,然后是方法装饰器,最后是类装饰器。
所以说
@Component
装饰器执行时可以保证class内部的所有逻辑已经执行完毕,即__decorator__
已经收集完毕。
同时vue-class-component
对外暴露了createDecorator
这个api,让我们方便的基于vue-class-component
进行拓展,vue-class-component/src/util.ts:
以@Ref为例分析
如果我们在class组件中写如下代码,通过上文的分析我们知道对于普通的class方法都是直接把函数体赋值到options对象身上的,所以经过@Component
装饰器的处理,MyMethod
方法的函数体仍然会是this.myDom
,但是MyMethod
方法中通过this.myDom
我们预期的是访问this.$refs.myDom
才对!
typescript
@Component
export default class YourComponent extends Vue {
// vue的ref的装饰器写法
@Ref() myDom: HTMLDivElement
myMethod() {
this.myDom.style.color = '#f00';
}
}
所以@Ref
要做的事情就是让class组件中的this.myDom
"映射"成options组件对象中的this.$refs.myDom
,我们可以借助计算属性来完成这个转化,所以@Ref
要做的就是给options对象添加一个名为myDom
的计算属性,让它指向this.$refs.myDom
即可!
下面我们看源码就不难理解了:
vue-property-decorator/src/decorators/Ref.ts:
typescript
import Vue from 'vue'
import { createDecorator } from 'vue-class-component'
/**
* decorator of a ref prop
* @param refKey the ref key defined in template
*/
export function Ref(refKey?: string) { // refKey可以省略,我们忽略它往下看
// createDecorator做的事情就是把函数参数封装一层(目的是把options传给函数参数)然后push的__decorator__数组中,等待@Component装饰器执行的最后调用
return createDecorator((options, key) => {
// 给options添加计算属性
options.computed = options.computed || {}
options.computed[key] = {
cache: false,
get(this: Vue) {
return this.$refs[refKey || key]
},
}
})
}
上面的@Ref
源码中,主体逻辑就是return createDecorator
,createDecorator
做的事情就是把函数参数封装一层(目的是把options传给函数参数)然后push的__decorator__
数组中,等待@Component
装饰器执行时调用。自然传递给createDecorator
的函数逻辑就是给options对象添加computed
计算属性了。
总结
- Vue2+TypeScript+vue-class-component+vue-property-decorator的技术栈体系目的是让vue2有更好的ts支持
- vue-class-component提供的核心装饰器
@Component
,是在class实例化的运行时,而非编译时,通过装饰器提供的类似于"拦截"的能力同步构建options对象组件,从而实现class组件到普通配置型对象组件的转化。 - vue-class-component提供了一定的拓展能力,vue-property-decorator就是基于vue-class-component自定义了一些常用的属性/方法装饰器,从而实现class组件中一些常用配置的便捷定义。