Indeed, you are correct. The decorator performed all the necessary magic here. This functionality is not exclusive to TypeScript, as it can also be achieved using JavaScript in conjunction with the babel decorator plugin. While the source code of vue-class-components provides a comprehensive explanation, let's attempt to create a basic version ourselves using only JavaScript.
Our objective is to design a decorator that can transform a class into a Vue component object, resembling this structure:
class MyComponent {
data() {
return {
count: 0,
};
}
plus() {
this.count++;
}
}
// will be converted to something like
const MyComponent = {
data() {
return {
count: 0,
};
},
methods: {
plus() {
this.count++;
}
}
}
The process is relatively straightforward. We create a new object and transfer all the methods from the class to this object. Let's begin by defining our decorator function:
function MagicDecorator(ComponentClass) {
const options = {};
return options;
}
The options
object will serve as our transformed output. Next, we need to iterate through the class to identify its properties and methods.
function MagicDecorator(ComponentClass) {
const options = {};
Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
console.log(key); // data, plus
});
return options;
}
It's important to note that
Object.keys(ComponentClass.prototype)
will not suffice since these are non-enumerable properties established using
Object.defineProperty()
.
For intrinsic hook methods such as mounted
, created
, or data
, we simply copy them directly. You can refer to Vue's source code for a complete list of hook methods.
const hooks = [
'data',
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeDestroy',
'destroyed',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'render'
];
function MagicDecorator(ComponentClass) {
const options = {};
Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
if (hooks.includes(key)) {
options[key] = ComponentClass.prototype[key];
}
});
return options;
}
Simply copying these methods into our options
suffices.
Subsequently, for custom methods, we need to place them within a methods
object.
function MagicDecorator(ComponentClass) {
const options = {
methods: {},
};
Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
if (hooks.includes(key)) {
options[key] = ComponentClass.prototype[key];
return
}
if (typeof ComponentClass.prototype[key] === 'function') {
options.methods[key] = ComponentClass.prototype[key];
}
});
return options;
}
At this stage, our implementation already functions efficiently and manages numerous simple components effectively. The previously mentioned counter component is now fully supported by our decorator.
However, considering that Vue incorporates computed properties, let's extend our support to accommodate this feature as well.
Computed properties are facilitated through getters and setters. It becomes slightly intricate because accessing them directly triggers the getter:
ComponentClass.prototype[key]; // This invokes the getter
Thankfully, by utilizing
Object.getOwnPropertyDescriptor()
, we can retrieve the actual getter and setter functions. Afterwards, we just need to incorporate them into the
computed
field.
const options = {
methods: {},
computed: {},
};
// remaining...
const descriptor = Object.getOwnPropertyDescriptor(
ComponentClass.prototype,
key
);
if (descriptor.get || descriptor.set) {
options.computed[key] = {
get: descriptor.get,
set: descriptor.set
};
}
In the vue-class-components source code, they manage methods
through descriptors as well:
if (typeof descriptor.value === 'function') {
options.methods[key] = descriptor.value;
return;
}
Lastly, we opt not to handle the constructor, adding a conditional check at the outset of the loop to ignore it:
if (key === 'constructor') {
return;
}
As a result, a functional example has been achieved. Witness it in action here: https://codesandbox.io/s/stackoverflow-vue-class-component-uhh2jg?file=/src/MagicDecorator.js
Note 1: our basic example does not currently support data initialization via a simple class property:
class MyComponent {
count = 0 // This type is unsupported
// Only this format is accommodated
data() {
return { count: 0 }
}
}
To enable support for class properties, they must be converted into reactive properties manually.
Note 2: Babel backs two versions of decorators. In alignment with vue-class-component's source code, I've opted for the legacy variant. Therefore, remember to specify {legacy: true}
options within the
@babel/plugin-proposal-decorators
plugin.