概述
vue-property-decorator是基於vue組織裡vue-class-component所做的擴充,先來了解一下vue-class-component
Vue-Class-Component
vue-class-component是一個Class Decorator,也就是類的裝飾器.
原理簡述
vue2.x只有Object一種宣告元件的方式, 比如這樣:
const App = Vue.extend({
// data
data() {
return {
hello: 'world',
};
},
computed: {
world() {
return this.hello + 'world';
},
},
// hooks
mounted() {
this.sayHello();
},
// methods
methods: {
sayHello() {
console.log(this.hello);
},
},
});
複製程式碼
用了vue-class-component就成了以下寫法:
import Component from 'vue-class-component';
@Component({
name: 'App'
})
class App extends Vue {
hello = 'world';
get world() {
return this.hello + 'world';
}
mounted() {
this.sayHello();
}
sayHello() {
console.log(this.hello);
}
}
複製程式碼
在這個例子中,很容易發現幾個疑點:
@Component()
是什麼?hello = 'world'
這是什麼語法?- App類沒有constructor建構函式;
- 匯出的類沒有被new就直接使用了;
疑點1:
對裝飾器的有一定了解. 裝飾器種類有好幾種, vue-class-component
中主要使用了類裝飾器.
更多關於裝飾器資訊請參閱阮老師的文章: ECMAScript6入門
看完阮老師所寫的文章已經可以解決了疑點1
簡述: @Component
就是一個修飾器, 用來修改類的行為
疑點2:
在JS語法中, class中都是需要在constructor中給屬性賦值, 在chrome上像vue-class-component
中定義class是會報錯的,
但vue-class-component
中卻又這麼做了.
然後我們看看class通過webpack + babel-loader
解析後會變成什麼樣子
// 轉換前
class App {
hello = 'world';
sayHello() {
console.log(this.hello);
}
}
// 轉換後
function App () {
this.hello = 'world'
}
App.prototype.sayHello = function () {
console.log(this.hello);
}
複製程式碼
接下來看看入口檔案index.ts所做的東西:
// Component實際上是既作為工廠函式,又作為裝飾器函式
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
if (typeof options === 'function') {
// 區別一下。這裡的命名雖然是工廠,其實它才是真正封裝裝飾器邏輯的函式
return componentFactory(options)
}
return function (Component: VueClass<Vue>) {
return componentFactory(Component, options)
}
}
複製程式碼
再看看componentFactory所做的東西:
import Vue, { ComponentOptions } from 'vue'
import { copyReflectionMetadata, reflectionIsSupported } from './reflect'
import { VueClass, DecoratedClass } from './declarations'
import { collectDataFromConstructor } from './data'
import { hasProto, isPrimitive, warn } from './util'
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> {
// 為component的name賦值
options.name = options.name || (Component as any)._componentTag || (Component as any).name
// prototype props.
// 獲取原型
const proto = Component.prototype
// 遍歷原型
Object.getOwnPropertyNames(proto).forEach(function (key) {
// 如果是constructor, 則不處理
if (key === 'constructor') {
return
}
// hooks
// 如果原型屬性(方法)名是vue生命週期鉤子名,則直接作為鉤子函式掛載在options最外層
if ($internalHooks.indexOf(key) > -1) {
options[key] = proto[key]
return
}
// getOwnPropertyDescriptor 返回描述物件
const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
// void 0 === undefined
if (descriptor.value !== void 0) {
// methods
// 如果是方法名就掛載到methods上
if (typeof descriptor.value === 'function') {
(options.methods || (options.methods = {}))[key] = descriptor.value
} else {
// typescript decorated data
// 把成員變數作為mixin放到options上,一個變數一個mixin,而不是直接統計好放到data或者同一個mixin中
// 因為data我們已經作為了保留欄位,可以在類中宣告成員方法data()和options中宣告data同樣的方法宣告變數
(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return { [key]: descriptor.value }
}
})
}
} else if (descriptor.get || descriptor.set) {
// computed properties
// 轉換成計算屬性的getter和setter
(options.computed || (options.computed = {}))[key] = {
get: descriptor.get,
set: descriptor.set
}
}
})
// add data hook to collect class properties as Vue instance's data
// 這裡再次新增了一個mixin,會把這個類例項化,然後把物件中的值放到mixin中
// 只有在這裡我們宣告的class的constructor被呼叫了
;(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return collectDataFromConstructor(this, Component)
}
})
// decorate options
// 如果這個類還有其他的裝飾器,也逐個呼叫. vue-class-component只提供了類裝飾器
// props、components、watch等特殊引數只能寫在Component(options)的options引數裡
// 因此我們使用vue-property-decorator庫的屬性裝飾器
// 通過下面這個迴圈應用屬性裝飾器就可以合併options(ps: 不明白可以看看createDecorator這個函式)
const decorators = (Component as DecoratedClass).__decorators__
if (decorators) {
decorators.forEach(fn => fn(options))
delete (Component as DecoratedClass).__decorators__
}
// find super
// 找到這個類的父類,如果父類已經是繼承於Vue的,就直接呼叫它的extend方法,否則呼叫Vue.extend
const superProto = Object.getPrototypeOf(Component.prototype)
const Super = superProto instanceof Vue
? superProto.constructor as VueClass<Vue>
: Vue
// 最後生成我們要的Vue元件
const Extended = Super.extend(options)
// 處理靜態成員
forwardStaticMembers(Extended, Component, Super)
// 如果我們支援反射,那麼也把對應的反射收集的內容繫結到Extended上
if (reflectionIsSupported) {
copyReflectionMetadata(Extended, Component)
}
return Extended
}
const reservedPropertyNames = [
// Unique id
'cid',
// Super Vue constructor
'super',
// Component options that will be used by the component
'options',
'superOptions',
'extendOptions',
'sealedOptions',
// Private assets
'component',
'directive',
'filter'
]
const shouldIgnore = {
prototype: true,
arguments: true,
callee: true,
caller: true
}
function forwardStaticMembers (
Extended: typeof Vue,
Original: typeof Vue,
Super: typeof Vue
): void {
// We have to use getOwnPropertyNames since Babel registers methods as non-enumerable
Object.getOwnPropertyNames(Original).forEach(key => {
// Skip the properties that should not be overwritten
if (shouldIgnore[key]) {
return
}
// Some browsers does not allow reconfigure built-in properties
const extendedDescriptor = Object.getOwnPropertyDescriptor(Extended, key)
if (extendedDescriptor && !extendedDescriptor.configurable) {
return
}
const descriptor = Object.getOwnPropertyDescriptor(Original, key)!
// If the user agent does not support `__proto__` or its family (IE <= 10),
// the sub class properties may be inherited properties from the super class in TypeScript.
// We need to exclude such properties to prevent to overwrite
// the component options object which stored on the extended constructor (See #192).
// If the value is a referenced value (object or function),
// we can check equality of them and exclude it if they have the same reference.
// If it is a primitive value, it will be forwarded for safety.
if (!hasProto) {
// Only `cid` is explicitly exluded from property forwarding
// because we cannot detect whether it is a inherited property or not
// on the no `__proto__` environment even though the property is reserved.
if (key === 'cid') {
return
}
const superDescriptor = Object.getOwnPropertyDescriptor(Super, key)
if (
!isPrimitive(descriptor.value) &&
superDescriptor &&
superDescriptor.value === descriptor.value
) {
return
}
}
// Warn if the users manually declare reserved properties
if (
process.env.NODE_ENV !== 'production' &&
reservedPropertyNames.indexOf(key) >= 0
) {
warn(
`Static property name '${key}' declared on class '${Original.name}' ` +
'conflicts with reserved property name of Vue internal. ' +
'It may cause unexpected behavior of the component. Consider renaming the property.'
)
}
Object.defineProperty(Extended, key, descriptor)
})
}
複製程式碼
下面簡單總結一下vue-class-component做了什麼:
- 收集class中的屬性, 如果是方法就放到Methods裡, 如果是普通變數就放到mixin中的data裡
- 例項化class, 把這個class的屬性也作為mixin中的data, 我們所寫class的建構函式只會被這裡所呼叫
- 利用Options執行生成元件
- 處理靜態屬性
- 反射相關處理
相關文章:
vue-property-decorator
vue-property-decorator
是在vue-class-component
基礎上新增了幾個屬性裝飾器
這裡採用幾種常用方式做介紹
Prop:
interface PropOptions<T=any> {
type?: PropType<T>;
required?: boolean;
default?: T | null | undefined | (() => T | null | undefined);
validator?(value: T): boolean;
}
export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
return (target: Vue, key: string) => {
applyMetadata(options, target, key)
// 把props push到vue-class-component的__decorators__陣列中
createDecorator((componentOptions, k) => {
;(componentOptions.props || ((componentOptions.props = {}) as any))[
k
] = options
})(target, key)
}
}
/** @see {@link https://github.com/vuejs/vue-class-component/blob/master/src/reflect.ts} */
const reflectMetadataIsSupported =
typeof Reflect !== 'undefined' && typeof Reflect.getMetadata !== 'undefined'
// 設定型別
function applyMetadata(
options: PropOptions | Constructor[] | Constructor,
target: Vue,
key: string,
) {
if (reflectMetadataIsSupported) {
if (
!Array.isArray(options) &&
typeof options !== 'function' &&
typeof options.type === 'undefined'
) {
// 型別後設資料使用後設資料鍵"design:type"
// 參考文章:https://www.jianshu.com/p/2abb2469bcbb
options.type = Reflect.getMetadata('design:type', target, key)
}
}
}
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))
}
}
複製程式碼
Watch:
/**
* decorator of a watch function
* @param path the path or the expression to observe
* @param WatchOption
* @return MethodDecorator
*/
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 })
})
}
複製程式碼
綜上所述, 其實和 vue-class-component
一個原理 都是用裝飾器去解析出適用於vue裡的引數