深入理解class和装饰器(下)
装饰器
在 vue 中,我们一般使用vue-class-component来把 vue 里面组件的写法转变为类形式的,写法如下:
<template>
<div>{{ message }}</div>
</template>
<script type='ts'>
import { Vue, Component, Watch } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
// Declared as component data
message = 'Hello World!'
@Watch('visible')
onVisibleChanged(newValue: any) {
this.$emit('input', newValue);
}
}
</script>
那么它是怎么实现的呢?主要分为 2 步:
- 在打包的时候会把装饰器打包成原始代码
- component 装饰器会对组件类做一些处理
装饰器是怎么打包的
装饰器是一个函数,它接收的三个参数:
- target(对象的 prototype,如果是类装饰器的话,就只有这一个参数)
- key(当前的方法名或属性名)
- descriptor(就是用于 defineProperty 的 config)
我们经常使用的 class 装饰器有 2 种(存在的一共有4种,我们只讨论常见的这 2 种):
- 放在 class 上的类装饰器
- 放在方法上的方法装饰器
首先我们来看一段示例代码:
function show(target, key, descriptor) {
console.log(target);
console.log(key);
console.log(descriptor);
}
// 类装饰器
@show
class A {
constructor(name) {
this.name = name;
}
// 方法装饰器
@show
say() {
console.log(this.name);
}
}
打包之后的简化代码如下:
function _applyDecoratedDescriptor(target, property, decorators, descriptor) {
var desc = {};
Object.keys(descriptor).forEach(function (key) {
desc[key] = descriptor[key];
});
desc = decorators.slice().reverse().reduce(function (desc, decorator) {
return decorator(target, property, desc) || desc;
}, desc);
return desc;
}
function show(target, key, descriptor) {
console.log(target);
console.log(key);
console.log(descriptor);
}
var A = show(_class = (_class2 = /*#__PURE__*/ function () {
function A(name) {
this.name = name;
}
A.prototype.say = function say() {
console.log(this.name);
}
return A;
}(), (_applyDecoratedDescriptor(
_class2.prototype,
"say",
[show],
Object.getOwnPropertyDescriptor(_class2.prototype, "say")
)
), _class2)) || _class;
可以看到:
- 对于类装饰器,只接收了 _class 参数,而
_class =
后面的括号里的三个值其实是一种顺序写法,最终返回的是括号里面的最后那个值也就是 _class2。 - 对于方法装饰器,会被放到一个数组里面去,然后调用 _applyDecoratedDescriptor 对被装饰的方法顺序执行各个装饰器(谁在上面谁先执行)。
- _applyDecoratedDescriptor 会收集 prototype、method key 和 property descriptor,然后传给装饰器进行执行。
到这里就很清晰了,装饰器其实并不是什么黑魔法,只是在编译的时候依次给类或者对象执行的函数罢了。
vue-class-component 是怎么实现 vue 的 class 写法的?
vue-class-component是通过 @component 装饰器来实现 vue 的 class 写法的,源码如下:
// index.ts
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()
这种不加参数的写法) - 如果传入的不是函数类型,则证明是一个配置,然后返回装饰器函数。(所以能够支持
@component(config)
这种加参数的写法)
最终,它是通过调用 componentFactory 来进行装饰的,它的源码如下:
export const $internalHooks = [
'data',
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeDestroy',
'destroyed',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'render',
'errorCaptured', // 2.5
'serverPrefetch' // 2.6
]
export function componentFactory (
Component: VueClass<Vue>,
options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
options.name = options.name || (Component as any)._componentTag || (Component as any).name
// prototype props.
const proto = Component.prototype
Object.getOwnPropertyNames(proto).forEach(function (key) {
if (key === 'constructor') {
return
}
// 加上生命周期函数、钩子函数
if ($internalHooks.indexOf(key) > -1) {
options[key] = proto[key]
return
}
const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
if (descriptor.value !== void 0) {
// 加上 methods
if (typeof descriptor.value === 'function') {
(options.methods || (options.methods = {}))[key] = descriptor.value
} else {
// 使用 mixins 的形式加上 data
(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return { [key]: descriptor.value }
}
})
}
} else if (descriptor.get || descriptor.set) {
// 加上 computed
(options.computed || (options.computed = {}))[key] = {
get: descriptor.get,
set: descriptor.set
}
}
})
// 收集 constructor 上的 data
;(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return collectDataFromConstructor(this, Component)
}
})
// 处理其它装饰器(方法装饰器、属性装饰器等)
const decorators = (Component as DecoratedClass).__decorators__
if (decorators) {
decorators.forEach(fn => fn(options))
delete (Component as DecoratedClass).__decorators__
}
// 初始化 super 里面的实例属性
const superProto = Object.getPrototypeOf(Component.prototype)
const Super = superProto instanceof Vue
? superProto.constructor as VueClass<Vue>
: Vue
const Extended = Super.extend(options)
// 处理子类和父类的静态方法
forwardStaticMembers(Extended, Component, Super)
// 复制使用 reflect 声明的属性
if (reflectionIsSupported()) {
copyReflectionMetadata(Extended, Component)
}
return Extended
}
总的来说,这段代码做了如下工作。其实就是筛选出相应的属性和方法按 options 的形式进行组装罢了。
- 保存一个内部钩子列表,筛选出生命周期钩子、路由钩子等作为相关的方法。(所以我们如果要加入路由钩子的话,首先需要先把它加到列表里面去)
- 筛选出 data、methods、computed 加到 options 上面去。(所以这些数据要遵循相应的写法)
- 收集 constructor 上的 data
- 初始化 super 里面的实例属性
- 处理子类和父类的静态方法
- 复制使用 reflect 声明的属性
这里需要注意的是,在收集 data 的时候,并不是直接把 data 进行赋值的,因为 data 可以是一个函数,所以这里使用mixins的方法进行混合。
vue-property-decorator 的 watch 装饰器的原理
vue-property-decorator是基于vue-class-component封装的库,它提供了很多方便的装饰器,现在我们来看下它的 watch 装饰器。源码如下:
// vue-class-component 库
export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
return (target: Vue | typeof Vue, key?: any, index?: any) => {
const Ctor = typeof target === 'function'
? target as DecoratedClass
: target.constructor as DecoratedClass
if (!Ctor.__decorators__) {
Ctor.__decorators__ = []
}
if (typeof index !== 'number') {
index = undefined
}
Ctor.__decorators__.push(options => factory(options, key, index))
}
}
// vue-property-decorator 库
export function Watch(path: string, options: WatchOptions = {}) {
const { deep = false, immediate = false } = options
return createDecorator((componentOptions, handler) => {
if (typeof componentOptions.watch !== 'object') {
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[path].push({ handler, deep, immediate })
})
}
这段代码其实就是在 componentOptions 上面开了一个 watch 属性,用来把各个字符串的 watch 函数推进去。值得注意的是执行过程:
- 首先执行方法装饰器,把装饰器工厂函数推到
__decorators__
保存起来,此时装饰器并没有被执行。 - 然后执行 component 装饰器,在执行过程中,会把
__decorators__
里面的装饰器取出,然后执行,这个时候方法装饰器才生效了。
有一点非常奇怪,因为在 component 装饰器里面,会先把实例方法(就是 watch 的方法)挂载到 methods 里面去,然后再执行方法装饰器,把方法作为 handler 推到相应的 watch 数组里面去。那么这个实例方法不是没有从 methods 里面删除吗?看了半天源码也没找到删除的地方,期待大佬解答~~