本文只浅析@Prop属性装饰器和@Watch方法装饰器的核心源码,其他原理相似,暂不赘述。
关于JS装饰器可查看本人另一篇:JS Decorator —— 装饰器(装饰模式)
关于@Component类装饰器及vue-class-component源码可查看本人另一篇:源码探秘之 vue-class-component
先回顾一下使用 vue+ts 开发的一个组件例子:
<template>
</template>
<script lang="ts">
// 此处是从vue-property-decorator 统一引入的,实际 Component 定义在 vue-class-component 包内
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
@Component({
name: 'test' // 组件name
})
export default class Test extends Vue {
// 父组件传递的参数
@Prop({type: String, default: ''}) msg!:string
// 定义的变量
title = '标题'
// 计算属性
get computedMsg () {
return 'computed ' + this.msg
}
// 侦听器
@Watch('title', { immediate: true })
watchTitleChange (val: string) {
console.log('title change:', val);
}
// 方法
initList () {
// do something
}
// 生命周期
mounted () {
this.initList()
}
}
</script>
经过@Component、@Prop、@Watch装饰器转换后,会变成标准的 Vue(2.x) 实例(当然很多属性是通过mixin加入进来的):
<script>
new Vue({
name: 'test',
props: {
msg: { type: String, default: '' }
},
data() {
return {
title: '标题',
};
},
computed: {
computedMsg() {
return 'computed ' + this.msg
},
},
watch: {
title: {
handle(val) {
console.log('title change:', val);
},
immediate: true
},
},
methods: {
initList() {
// do something
},
},
mounted() {
this.initList();
},
});
</script>
源码开始
@Prop属性装饰器
文件:src/decorators/Prop.ts
/**
* decorator of a prop
* @param options the options for the prop
* @return PropertyDecorator | void
*/
// 高阶函数接受参数,兼容三种写法
// @Prop(String) readonly name!: string | undefined;
// @Prop({ default: 30, type: Number }) private age!: number;
// @Prop([String, Boolean]) private sex!: string | boolean;
export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
// 返回一个属性装饰器函数,传入当前Vue类 和 属性名
return (target: Vue, key: string) => {
// 设置类型
applyMetadata(options, target, key)
// 把props push到vue-class-component的__decorators__数组中
createDecorator((componentOptions, k) => {
// 将属性放在 Vue类.props 上, 生成如下对象,
// 然后 createDecorator 会把他push到vue-class-component的__decorators__数组中
// props: {
// name: String,
// age: { default: 30, type: Number },
// sex: [String, Boolean],
// }
;(componentOptions.props || ((componentOptions.props = {}) as any))[
k
] = options
})(target, key)
}
}
文件:/src/helpers/metadata.ts
// 设置类型
export function applyMetadata(
options: PropOptions | Constructor[] | Constructor,
target: Vue,
key: string,
) {
if (reflectMetadataIsSupported) {
if (
!Array.isArray(options) &&
typeof options !== 'function' &&
!options.hasOwnProperty('type') &&
typeof options.type === 'undefined'
) {
// 获取属性类型传至 Vue
// 类型元数据使用元数据键"design:type"
// 参考文章:https://jkchao.github.io/typescript-book-chinese/tips/metadata.html#%E5%9F%BA%E7%A1%80
const type = Reflect.getMetadata('design:type', target, key)
if (type !== Object) {
options.type = type
}
}
}
}
文件:vue-class-component/src/util.ts
// 接收一个工厂函数,返回一个装饰器函数
export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
return (target: Vue | typeof Vue, key?: any, index?: any) => {
// 获取 vue-class-component 的 Component
// 是函数类型,则为装饰的类;
// 否则,为原型,通过constructor拿到构造函数
const Ctor = typeof target === 'function'
? target as DecoratedClass
: target.constructor as DecoratedClass
if (!Ctor.__decorators__) {
Ctor.__decorators__ = []
}
if (typeof index !== 'number') {
index = undefined
}
// 将这个工厂函数(传入Vue类,属性名)push到vue-class-component的__decorators__数组中
// 会在 vue-class-component 中调用
Ctor.__decorators__.push(options => factory(options, key, index))
}
}
@Watch方法装饰器
文件:/src/decorators/Watch.ts
/**
* decorator of a watch function
* @param path the path or the expression to observe
* @param watchOptions
*/
// @Watch('title', { immediate: true })
// watchTitleChange (val: string) {
// console.log('title change:', val);
// }
// path: 'title' watchOptions: { immediate: true }
// componentOptions
export function Watch(path: string, watchOptions: WatchOptions = {}) {
// 当前vue的option(在vue-class-component已经拼装好), handler: watchTitleChange 函数名
return createDecorator((componentOptions, handler) => {
componentOptions.watch ||= Object.create(null)
// 拿到
const watch: any = componentOptions.watch
if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) {
watch[path] = [watch[path]]
} else if (typeof watch[path] === 'undefined') {
watch[path] = []
}
// watch: { title: [{ handler: watchTitleChange, immediate: true }] }
watch[path].push({ handler, ...watchOptions })
})
}
createDecorator方法在@Prop中有说
总结
核心就是:向vue-class-component 中Component构造函数的__decorators__属性中放进factory函数,然后在 @Component 中统一调用,目的是将这些属性/方法添加到option里组装。
这边调用有点绕,需要多看多理解。