扒一扒vue-class-component

使用Typescript + Vue + vue-class-component已经有小半年了,终于有机会扒一扒vue-class-component的源码。

注意:vue-class-component不一定需要typescript配合才能使用,装饰器不是typescript特有的,ES6也有, 只不过需要开启 decorators

vue-class-component(官方提供,出自尤大之手)的目标,正如其名,是可以让开发者使用es6 class`的方式开发vue组件,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
@Component({
components: {
HelloWorld,
},
})
export default class Home extends Vue {
@Prop({ required: true })
someProp;
greeting = 'hello';
mounted() {
console.log('This is home');
}
}
</script>

vue-property-decorator出自第三方,只是提供了常用的class写法中Vue选项简洁写法,本身还是直接引用了vue-class-componentvue-property-decorator已经被集成进官方脚手架ts插件@vue/cli-plugin-typescript

那用class写组件相比于『普通options对象式』有什么好处呢?
有些人可能不喜欢写computed, data, methods这种『特殊』选项,以及不喜欢在方法与方法之间写逗号(比如我),class的写法更加纯粹简洁。更有甚者,可能有些人React和Vue同时在用,使用class写法可以减少切换成本。

再进一步,使用class写法加上typescript可以得到更好的编辑器/IDE代码提示的支持。

经过这几个月的尝试,还是能体会到这些好处的。

接下来进入正题,vue-class-component到底做了什么事可以实现这个目的呢?

简单讲,就是『在运行时』根据我们写下的class(会被ts/babel转义成es5构造函数),提取方法,构造出『普通options对象式』写法中的options对象, 并通过直接或间接调用Vue.extend创建『正宗』的Vue组件。

然后我们一点点解开她神秘的面纱。

核心是Component工厂函数,也即vue-class-component的默认导出,vue-property-decorator导出的Component也是它。当我们在源码里写:

1
2
@Component({})
export default class extends Vue {}

在『运行时』即调用了Component工厂函数(我们这里跳过ts/babel的转义过程),我们来瞅瞅这个函数做了啥(src/component.ts部分源码@6.2.0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// vue专属钩子方法名
export const $internalHooks = [
'data',
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeDestroy',
'destroyed',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'render',
'errorCaptured' // 2.5
]
export function componentFactory (
// 这是我们写的class组件
Component: VueClass<Vue>,
// 剩余选项配置,也就是将来要传给Vue.extend的普通对象,例如上面例子中的`{
// components: {
// HelloWorld,
// },
//}`
options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
// 设置选项的name
options.name = options.name || (Component as any)._componentTag || (Component as any).name
// 从原型中提取class中声明的方法
const proto = Component.prototype
// Object.getOwnPropertyNames会提取所有可枚举以及不可枚举的属性(包括getter和setter)
// chrome调试时proto会显示$isServer,$ssrContext等属性,然而这几个都是Vue原型上的方法,通过`proto.hasOwnProperty('$isServer') === false`可推断此现象疑似chrome的bug
Object.getOwnPropertyNames(proto).forEach(function (key) {
// 忽略构造函数
if (key === 'constructor') {
return
}
// 将vue钩子方法设置给options
if ($internalHooks.indexOf(key) > -1) {
options[key] = proto[key]
return
}
// 拿到描述符
// 之所以不直接取proto[key], 是因为,对于getter类型的方法,proto[key]的结果是对这个getter方法求值了,而拿不到方法,这与预期不符
const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
if (typeof descriptor.value === 'function') {
// 如果是function类型,则挂到methods下
(options.methods || (options.methods = {}))[key] = descriptor.value
} else if (descriptor.get || descriptor.set) {
// 如果描述符带有get或set属性,则为setter/getter, 挂到computed下
(options.computed || (options.computed = {}))[key] = {
get: descriptor.get,
set: descriptor.set
}
}
})
// 这是提取data的关键入口,这里不得不感叹很精妙,后面还有更精妙的
// 声明mixin,并添加data方法,作为提取data的入口
;(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return collectDataFromConstructor(this, Component)
}
})
// 处理其他装饰器,比如prop,emit等,这些只是边角料
const decorators = (Component as DecoratedClass).__decorators__
if (decorators) {
decorators.forEach(fn => fn(options))
delete (Component as DecoratedClass).__decorators__
}
// 获取当前组件(类)的原型
const superProto = Object.getPrototypeOf(Component.prototype)
// 如果是普通对象,说明是直接继承Vue的,否则是Vue的子类
const Super = superProto instanceof Vue
? superProto.constructor as VueClass<Vue>
: Vue
// 抛弃我们自己写的class,使用我们继承的父类扩展一个新的组件
// 此处的options就是我们『普通options对象式』写法中的options对象
const Extended = Super.extend(options)
// 复制静态成员
forwardStaticMembers(Extended, Component, Super)
return Extended
}

代码注释中提到的chrome bug截图:

最后,vue文件(模块)导出的就是最后这个Extended。在实例化这个组件的时候,会按照正常的Vue生命周期,调用所有钩子,需要重点分析一下这里提取data的过程。

在Vue组件实例化时执行上述mixin中的data方法时,调用了collectDataFromConstructor函数, 继续瞅瞅这个函数(src/data.ts):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// vm即为真正的vue组件实例,Component为原始的我们写的那个class(其实在运行时是es5的构造函数)
export function collectDataFromConstructor(vm: Vue, Component: VueClass<Vue>) {
// 临时重写_init方法,因为下面会new Component(), 而由于Component一定是Vue的直接或间接子类,一定会执行_init方法
// 而原始的_init方法会做大量的事情,这里没有必要,而且这里需要做别的一些事情,干脆重写_init方法
const originalInit = Component.prototype._init;
Component.prototype._init = function (this: Vue) {
// proxy to actual vm
const keys = Object.getOwnPropertyNames(vm);
// 兼容Vue@2.2.0, 把props属性push进代理列表
if (vm.$options.props) {
for (const key in vm.$options.props) {
if (!vm.hasOwnProperty(key)) {
keys.push(key);
}
}
}
// 将原始class上的属性代理到真正的Vue实例属性上(下划线开头的属性除外)
// 从我遇到过的情况来看,这里主要用于:在原始class中contrustor方法中调用实例方法保证`this`能够指向真正的Vue实例
// 有些复杂的data属性初始化,有可能是调用成员方法完成的,所以this要指向正确,否则有些Vue相关的属性会拿不到
keys.forEach(function (key) {
if (key.charAt(0) !== '_') {
Object.defineProperty(this, key, {
get: () => vm[key],
set: value => vm[key] = value,
configurable: true
})
});
};
// 精妙之处始于此
// new一个原始class,会触发原始class的constructor执行,而class写法中的实例成员,经过ts/babel转义,会被转义到constructor中,
// 而这些class实例成员,会变成这个临时对象『本身的属性』,可以通过`Object.keys`遍历出来!!!
const data = new Component();
// restore original _init to avoid memory leak (#209)
Component.prototype._init = originalInit;
// 准备返回的纯对象
const plainData = {};
// 这里就便利出Vue所需的data来啦
Object.keys(data).forEach(function (key) {
if (data[key] !== undefined) {
plainData[key] = data[key];
}
});
if (process.env.NODE_ENV !== 'production') {
if (!(Component.prototype instanceof Vue) && Object.keys(plainData).length > 0) {
warn('Component class must inherit Vue or its descendant class ' +
'when class property is used.');
}
}
return plainData;
}

代码注释中提到的转义后的es5运行时构造函数:

然后,就跟之前的『普通options对象式』写法一样,运行时该干嘛干嘛了。

最后

国庆时,尤大在伦敦Vue Conf中透露了Vue3.0的计划,其中有一点,3.0将会使用Typescript重写,未来会对Typescript有更好的支持。
你开始用Typescript了吗?