本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
1. 前言
传统的vue
写法组件其实就是一个js
对象, 里面不能很好地配合编辑器做类型推断, 特别是this
的指向总是any
, 所以Vue
的作者尤雨溪大佬提供了这种class
的写法, 以提供更好的类型推断和新的组件写法
这里先分析
vue-class-component
, 后面会再写一篇分析vue-property-decorator
这里的使用方法都是
class
的装饰器写法, 所以不了解的可以去看看阮一峰《ECMAScript 6 入门》装饰器
1.1 前置知识
1.1.1 类的语法
class Component {
// 1. 实例属性新写法
msg = '消息'
constructor() {
// 2. 实例属性的传统写法
this.otherMsg = '另一条消息'
this._count = 0
}
// 3. 原型链方法
add() {
this._count++
}
// 4. 计算属性的写法
get count() {
return this._count
}
set count(count) {
this._count = count
}
}
1.1.2 装饰器语法
// 1. 装饰器可以装饰类和方法(包含属性, 如果装饰属性, 会同时增加一个同名原型方法),都是函数, 并且是在编译时(肯定是在类被用到之前)执行的, 先执行方法装饰器, 再执行类装饰器
/**
* 装饰类, 只接收一个参数
* @param classFn 它装饰的类
* @returns 装饰过的类
*/
const ClassDecorator = (classFn: any) => {
return classFn
}
/**
* 装饰方法, 接收三个参数,
* @param target 类的原型对象
* @param name 要装饰的属性名
* @param descriptor 要装饰的属性名的描述对象
* @returns 装饰过的属性的描述符
*/
const MethodDecorator = (target: any, name: string, descriptor?: any) => {
console.log(target, name, descriptor)
return descriptor
}
@ClassDecorator
class Component {
@MethodDecorator
method() {}
@MethodDecorator
a: any
@MethodDecorator
get b() {
return 0
}
}
export { Component }
// 2. 同一个类或者同一个方法可以同时被多个装饰器装饰, 会从下到上倒着装饰
const MultipleClassDecorator1 = (classFn: any) => {
console.log(`我是 MultipleClassDecorator1, 我被调用了`)
return classFn
}
const MultipleClassDecorator2 = (...args: any[]) => {
console.log(
`我是为了获取装饰器函数MultipleClassDecorator2时被调用的, 接收到参数`,
args
)
return (classFn: any) => {
console.log(`我是 MultipleClassDecorator2, 我被调用了`)
return classFn
}
}
const MultipleClassDecorator3 = (classFn: any) => {
console.log(`我是 MultipleClassDecorator3, 我被调用了`)
return classFn
}
const MultipleClassDecorator4 = (...args: any[]) => {
console.log(
`我是为了获取装饰器函数MultipleClassDecorator4时被调用的, 接收到参数`,
args
)
return (classFn: any) => {
console.log(`我是 MultipleClassDecorator4, 我被调用了`)
return classFn
}
}
@MultipleClassDecorator1
@MultipleClassDecorator2('我是最先执行的') // 这个是为了获取装饰器函数的调用, 最先调用, 并不是去装饰装饰器
@MultipleClassDecorator3
@MultipleClassDecorator4('我稍晚执行', '我传俩参数')
class MultipleClass {}
export { Component, MultipleClass }
上面的打印
好了, 有了以上知识, 开始下面的分析
2. 用法
有两种用法(传参与不传参), 都必须用
@Component
装饰一下, 不然导出的类不是组件对象, 记得那时候, 我遇到这个问题, 还找了一段时间的bug
...
// 用法一, 直接装饰类, 没有传入参数
import { Component, Vue } from 'vue-property-decorator'
@Component
export default class App extends Vue {}
// 用法二, 传入了参数, 参数可以是undefined, 也可以是一个options, 别传函数就行
import { Component, Vue } from 'vue-property-decorator'
@Component({
methods: {
myHandler() {},
},
})
export default class App extends Vue {}
3. 源码
首先这里需要注意的一点是, 在类的装饰器里, 是拿不到实例上的属性的, 需要构造一下之后, 才能拿到实例上的属性
3.1 导出了一个全局的函数
function Component(options: ComponentOptions<Vue> | VueClass<Vue>): any {
// 1. 用法一, 它是作为装饰器使用的,
if (typeof options === 'function') {
return componentFactory(options)
}
// 2. 用法二, 作为获取装饰器的函数被调用, 下面的才是装饰器函数
return function (Component: VueClass<Vue>) {
return componentFactory(Component, options)
}
}
export default Component
3.2 组件工厂函数
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选项
// 这个类装饰器函数, 功能就是从类中拿到分散的 methods computed 和 data 以及 一些生命周期 合并到 传入的 options 上, 然后通过 Vue.extend(options) 返回一个组件
options.name =
options.name || (Component as any)._componentTag || (Component as any).name
// 从原型上拿到 methods 和 computed 还有很少会在原型上出现的 data
const proto = Component.prototype
// 只读取 当前类原型的自有属性, 没有读取到它继承的类的原型上的属性
Object.getOwnPropertyNames(proto).forEach(function (key) {
if (key === 'constructor') {
return
}
// options 上原有的方法, 大多都是些生命周期, 直接赋值到 options 上
if ($internalHooks.includes(key)) {
options[key] = proto[key]
return
}
const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
// 如果方法装饰器返回不正确的描述对象, 它的值就是undefined
if (descriptor.value !== void 0) {
if (typeof descriptor.value === 'function') {
// 原型上的函数
// 这里直接对传入的 options.methods 进行赋值, 所以可能会覆盖掉传入的 options.methods 上原本的值
;(options.methods || (options.methods = {}))[key] = descriptor.value
} else {
// 不是函数的, 都认为是 data, 通过 mixins 混入
;(options.mixins || (options.mixins = [])).push({
data(this: Vue) {
return { [key]: descriptor.value }
},
})
}
} else if (descriptor.get || descriptor.set) {
// 类上的计算属性
;(options.computed || (options.computed = {}))[key] = {
get: descriptor.get,
set: descriptor.set,
}
}
})
;(options.mixins || (options.mixins = [])).push({
data(this: Vue) {
// 这里是收集类上的实例属性, 因为在 原型上读取不到 实例属性, 需要构造一下才可以拿到
// 它的调用时机是, 组件被实例化时, 获取 data 的时候 initState => initData
// 第一个参数 this, 指向的是 这个组件
// 不要跟进去看了, 只需要知道, 在这里拿到类上的实例属性就好了, 好累....
return collectDataFromConstructor(this, Component)
},
})
// 这里给方法装饰器留了个口子, vue-property-decorator 会用到, 因为方法装饰器先调用, 所以在方法装饰器中, 把预先的回调放入 __decorators__ 中, 在这里统一回调
// 之所以这样做, 是因为在方法装饰器中拿不到 options, 这里可以给它, 让它处理一下
// 这里在下一篇, 分析 vue-property-decorator 时, 还会提到
const decorators = (Component as DecoratedClass).__decorators__
if (decorators) {
decorators.forEach((fn) => fn(options))
delete (Component as DecoratedClass).__decorators__
}
// 下面就是将 合并生成的 options 通过 Vue.extend(options) 并返回
const superProto = Object.getPrototypeOf(Component.prototype)
const Super =
superProto instanceof Vue ? (superProto.constructor as VueClass<Vue>) : Vue
// 在Super.extend里面会将mixins合并到Extended.options上
const Extended = Super.extend(options)
// 将 Component 上的静态属性 搬运到 Extended 上
// forwardStaticMembers(Extended, Component, Super)
// 也是搬运一些东西
// if (reflectionIsSupported()) {
// copyReflectionMetadata(Extended, Component)
// }
return Extended
}
export function collectDataFromConstructor(vm: Vue, Component: VueClass<Vue>) {
// 保存 _init 方法, 因为想要拿到类上的实例属性需要 构造一下( new 一下), 构造时只会调 _init 方法
const originalInit = Component.prototype._init
// 这里重写 _init 方法, 除了上面的作用, 还有一个作用是, 将在vm.$options.props 但是不在 vm 上的属性 赋值成实例属性
// 如果vm.$options.props 是undefined, 可以不看下面了
Component.prototype._init = function (this: Vue) {
const keys = Object.getOwnPropertyNames(vm)
if (vm.$options.props) {
for (const key in vm.$options.props) {
if (!vm.hasOwnProperty(key)) {
keys.push(key)
}
}
}
// keys 得到的是在 vm.$options.props 但是不在 vm 上的属性
keys.forEach((key) => {
// 这里的 this 指向是 Component 实例, 最后是下面 new 出来的 data
// vm 是 组件
Object.defineProperty(this, key, {
get: () => vm[key],
set: (value) => {
vm[key] = value
},
configurable: true,
})
})
}
// 得到实例上的属性
const data = new Component()
// 恢复原型
Component.prototype._init = originalInit
// create plain data object
const plainData = {}
Object.keys(data).forEach((key) => {
if (data[key] !== undefined) {
plainData[key] = data[key]
}
})
return plainData
}
4. 总结
这个类装饰器函数, 功能就是从类中拿到分散的
methods
computed
和data
以及 一些生命周期 合并到 传入的options
上, 然后通过Vue.extend(options)
返回一个组件
$internalHooks
中的会直接覆盖赋值传入的options
, 这其中包括data
, 其它都是些生命周期函数
类型 | 拿到位置 | 合并方式 |
---|---|---|
function | 原型链上 | 覆盖赋值到options.methods 上 |
get & set | 原型链上 | 覆盖赋值到options.computed 上 |
属性 | 原型链上&实例属性上 | mixin 到data 上 |
需要注意这些覆盖赋值
5. 最后
后面准备再写一篇
vue-property-decorator
, 然后就不再写vue2
的了, 直接开始vue3
按照惯例, 附上之前写的几篇文章