裝飾器
在 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 里面刪除嗎?看了半天源碼也沒找到刪除的地方,期待大佬解答~~