使用過一段時間 class 來定義組件,要用 vue-property-decorator 提供定義好的裝飾器,輔助完成所需功能,對這個過程好奇,就研究了源碼。內部主要依靠 vue-class-component 實現,因此將重點放在對 vue-class-component 的解讀上。javascript
本文主要內容有:vue
沒有使用 class 方式定義組件時,一般導出一個選項對象:java
<script>
export default {
props: {
name: String
},
data() {
return {
message: '新消息'
}
},
watch: {
message(){
console.log('message改變觸發')
}
},
computed:{
hello: {
get(){
return this.message + 'hello';
},
set(newValue){}
}
},
methods:{
clickHandler(){}
}
mounted(){
console.log('掛載完畢');
}
}
</script>
複製代碼
這個對象告訴 Vue 你要作什麼事情,須要哪些功能。 根據字段的不一樣做用,把須要添加的屬性和方法,寫在指定的位置,例如,須要響應式數據寫在 data 中、計算屬性寫在 computed 中、事件函數寫在 methods中、直接寫生命週期函數等 。Vue 內部會調用 Vue.extend() 建立組件的構造函數,以便在模板中使用時,經過構造函數初始化此組件。git
若是使用了 class 來定義組件,上面的字段可省略,但要符合 Vue 內部使用數據的規則,就須要重組這些數據。github
定義 class 組件:markdown
<script lang="ts">
class Home extends Vue {
message = '新數據';
get hello(){
return this.message + 'hello';
}
set hello(newValue){}
clickHandler(){}
mounted(){}
}
Home.prototype.age = '年齡'
</script>
複製代碼
message 做爲響應式的數據,應該放在 data 中,但問題是 message 寫在類中,爲初始化後實例上的屬性,就要想辦法在初始化後拿到 message,放在 data 中。app
age 直接寫在原型上,值不是函數,也應該放在 data 中。函數
hello 寫了訪問器,做爲計算屬性,寫在 computed 中;clickHandler做爲方法,寫在 methods 中;mounted 是生命週期函數,掛載原型上就能夠,不須要動。這三個都是方法,定義在原型上,須要拿到原型對象,找到這三類方法,按照特性放在指定位置。工具
這就引起一個問題,怎麼把這些定義的屬性放在 Vue 須要解析的數據中,「上帝的歸上帝,凱撒的歸凱撒」。oop
最終處理成這樣:
{
data:{
message: '新數據',
age: '年齡'
},
methods:{
clickHandler(){}
},
computed:{
hello:{
get(){
return this.message + 'hello';
}
}
},
mounted(){}
}
複製代碼
最好是無入侵式的添加功能,開發者無感知,正常寫業務代碼,提供封裝好功能來完成歸類數據這件事。
裝飾器模式,在不改變自身對象的基礎上,動態增長額外的功能,這個模式的思路符合上述內容的要求。具體可參考一篇文章詳細瞭解,裝飾者模式和TypeScript裝飾器
vue-class-component 的代碼使用 ts 書寫,若是對 ts 語法不熟悉,能夠忽略定義的類型,直接看函數體內的邏輯,不影響閱讀。或者直接看打包後,沒有壓縮的代碼,也很少,大約200行左右。
本文分析的代碼主要文件在:倉庫地址
先來看大體結構和如何使用:
function Component(options) {
// options 是 function類型,是要裝飾的類
if (typeof options === 'function') {
return componentFactory(options);
}
// 執行後,這個函數做爲裝飾器函數,接收要裝飾的類
// options 爲傳入的選項數據。
return function (Component) {
return componentFactory(Component, options);
};
}
// 使用1
@Component
class Home Extend Vue {}
// 使用2
@Component({
components:{}
data:{newMessage: '增長的消息'},
methods:{
moveHandler(){}
},
computed:{
reveserMessage(){
return this.newMessage + '翻轉'
}
}
// ... vue中選項對象其餘值
})
class Home Extend Vue {}
複製代碼
Component 做爲裝飾器函數,接受的 options 就是要裝飾的類 Home, js 中類不過是一種語法糖,typeof Home 獲得爲 function 類型。
Component 函數做爲工廠函數,執行並傳入參數 options(爲了稱呼方便,後面把這個參數叫作 裝飾器選項數據),工廠函數執行後,返回裝飾器函數,一樣是接受要裝飾的類 Home。
從代碼中能夠看出來,都調用了 componentFactory ,第一個參數爲要裝飾的類,第二參數可選,傳入的話就是裝飾器選項數據。
從名字上能夠看出來,componentFactory 用來產生組件的工廠,通過一系列的執行後,返回新的組件函數。省略其餘,先看關鍵代碼 代碼地址:
function componentFactory(Component) {
// 省略其餘代碼...
// 參數爲兩個,說明第二個是傳入的部分選項數據;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// 獲得繼承的父類,不出意外爲 Vue
var superProto = Object.getPrototypeOf(Component.prototype);
// 若是原型鏈上確實有 Vue,則獲得構造函數;不爲 Vue,則直接使用 Vue;
// 目的是爲了找到 extend 函數。
var Super = superProto instanceof Vue ? superProto.constructor : Vue;
// 根據選項對象,新建一個組件的構造函數
var Extended = Super.extend(options);
// 返回新的構造函數
return Extended;
}
複製代碼
驗證了上面的猜想,調用了 Vue.extend 返回新的組件函數。但在返回以前,要處理原來組件上的屬性,和原型上的方法。
首先對選項上的方法歸類,方法歸 methods;非方法歸 data;有訪問器歸 computed。
// 須要忽略的屬性
const $internalHooks = [
'data',
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeDestroy',
'destroyed',
'beforeUpdate',
'updated',
'activated',
'deactivated',
'render',
'errorCaptured', // 2.5
'serverPrefetch' // 2.6
]
function componentFactory(Component) {
// 其餘代碼省略...
// 拿到原型對象
const proto = Component.prototype
// 返回對象上全部自身屬性,包括不可枚舉的屬性
Object.getOwnPropertyNames(proto).forEach(function (key) {
// 構造函數,不作處理
if (key === 'constructor') {
return
}
// 鉤子函數之類的屬性,直接賦值到 options對象上,不須要歸類
if ($internalHooks.indexOf(key) > -1) {
options[key] = proto[key]
return
}
// 拿到對應屬性的描述對象,用這個方法能避免繼續查找原型鏈上的屬性
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
// 若是此屬性的值不爲 undefined,說明有值
if (descriptor.value !== void 0) {
// methods
// 若是爲函數,則直接歸爲 methods
if (typeof descriptor.value === 'function') {
(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) {
// value 爲空,可是有 get或set的訪問器,則歸爲computed
(options.computed || (options.computed = {}))[key] = {
get: descriptor.get,
set: descriptor.set
}
}
})
}
複製代碼
從上述代碼能夠看出來,拿到屬性對應的描述對象,根據屬性對應的值,進行類型判斷,來決定歸爲哪一類。
值得注意的是這段代碼,目的是把非函數的屬性,混合在 data 中:
if(typeof descriptor.value === 'function'){/*省略*/}
else{// 處理原型上不是函數的狀況
(options.mixins || (options.mixins = [])).push({
data (this: Vue) {
return { [key]: descriptor.value }
}
})
}
複製代碼
通常寫在類中的只有是函數才能放在原型上,但有別的方式能夠把非函數的值添加到原型上:
// 第一種,直接給原型添加屬性
Home.prototype.age = 18;
// 第二種,用屬性裝飾器
function ageDecorator(prototype, key){
return { // 裝飾器返回描述對象,會在 prototype增長key這個屬性
enumerable: false,
value: 18
}
}
class Home extends Vue {
@ageDecorator
age: number = 18;
}
複製代碼
若是用了 ts 的屬性裝飾器,並返回描述對象,就會在 prototype 增長這個屬性,因此在上面 componentFactory 源碼中要處理這種狀況,通常在項目中比較少見。
寫在類中的屬性,不添加在原型上,只有經過獲得實例後拿到這些值,能夠沿着這個思路進行分析。
先看實例上屬性的狀況:
class Home {
message: '新消息',
clickHandler(){}
}
let home = new Home();
console.log(home);
// 打印實例,簡化後:
{
message: "新消息"
__proto__:
constructor: class Home
clickHandler: ƒ clickHandler()
__proto__: Object
}
複製代碼
在 componentFactory 中作了單獨的處理:
function componentFactory(Component){
// 省略其餘代碼
;(options.mixins || (options.mixins = [])).push({
data () {
return collectDataFromConstructor(this, Component)
}
})
}
複製代碼
這裏依然使用混合 data 的方式,混合功能很強大,敲黑板記下來。mixins 會在初始化組件時,調用 data 對應的函數,獲得要混合的數據,又調用了 collectDataFromConstructor,傳入 this,爲組件實例,跟平時寫項目在 mounted 中使用的那個 this 同樣,都爲渲染組件的實例;第二參數爲 Component,是原來裝飾的類,上面例子中就是 Home 類。
這個函數的目的是把原來裝飾的類,初始以後,拿到實例上的屬性組成對象返回。代碼地址
來看代碼:
// 用來收集被裝飾類中定義的屬性
// vm 爲要渲染的組件實例
// Component 爲原來要裝飾的組件類
function collectDataFromConstructor(vm, Component) {
// 先保存原有的 _init,目的是不執行 Vue上的 _init 作其餘初始化動做
var originalInit = Component.prototype._init;
// 在被裝飾的類的原型上手動增長 _init,在Vue實例化事內部會調用
Component.prototype._init = function () {
var _this = this;
// 拿到渲染組件對象上的屬性,包括不可枚舉的屬性,包含組件內定義的 $開頭屬性 和 _開頭屬性,還有自定義的一些方法
var keys = Object.getOwnPropertyNames(vm);
// 若是渲染組件含有,props,可是並無放在原組件實例上,則添加上
if (vm.$options.props) {
for (var key in vm.$options.props) {
if (!vm.hasOwnProperty(key)) {
keys.push(key);
}
}
}
// 把給原組件實例上 Vue 內置屬性設置爲不可遍歷。
keys.forEach(function (key) {
if (key.charAt(0) !== '_') {
Object.defineProperty(_this, key, {
get: function get() {
return vm[key];
},
set: function set(value) {
vm[key] = value;
},
configurable: true
});
}
});
};
// 手動初始化要包裝的類,目的是拿到初始化後實例
var data = new Component();
// 從新還原回原來的 _init,防止一直引用原有的實例,形成內存泄漏
Component.prototype._init = originalInit;
// 從新定義對象
var plainData = {};
// Object.keys 拿到可被枚舉的屬性,添加到對象中
Object.keys(data).forEach(function (key) {
if (data[key] !== undefined) {
plainData[key] = data[key];
}
});
return plainData;
}
複製代碼
具體要作的話,經過 new Component() 獲得被裝飾類的實例,但要注意,Component 繼承了 Vue 類,初始化後實例上有不少 Vue 內部添加上的屬性,好比 $options、$parent、$attrs、$listeners、$data 等等,還有以 _ 開頭的屬性,_watcher、_renderProxy 等等,還有咱們須要的屬性。這裏只是簡單舉幾個屬性,你能夠手動初始化,在控制檯打印輸出看一下。
以 _ 開頭的屬性,是內置方法,不可被枚舉;以 $ 開頭的屬性,也是內置方法,可是可被枚舉。若是直接循環實例,會拿到以 $ 開頭的屬性,這並非咱們須要的。
那怎麼辦呢?代碼中給了答案,在初始化一系列組件內置的屬性後,組件內部會調用 Component.prototype._init 方法,可經過改寫這個方法,來處理屬性爲不可枚舉。
最後經過 Object.keys() 獲得可以被遍歷的屬性。
上面拐的彎比較多,不免看蒙了,根據核心意思,簡化以下:
原來有個組件:
class Home {
message: '新消息'
}
複製代碼
如今有個須要渲染的組件,要把上面定義在 Home 中的 message 寫在現有組件的 data 中:
const App = Vue.extend({
// 混合功能
mixins:[{
data(){
// 初始化後拿到實例,就能拿到 message 屬性
let data = new Home();
let plainData = {};
Object.keys(data).forEach(function (key) {
if (data[key] !== undefined) {
plainData[key] = data[key];
}
});
return plainData;
}
}],
data(){
return {
other: '其餘data'
}
}
})
new App().$mounted('#app');
複製代碼
簡化後,是否是清晰不少,本質就是初始類獲得實例,拿屬性組成對象,混合到渲染的組件中。
小的優化點,簡化代碼:
// 保留原有的 _init 方法
var originalInit = Component.prototype._init;
Component.prototype._init = function(){
// 其餘代碼省略
};
Component.prototype._init = originalInit;
複製代碼
這段代碼,在改寫的 _init 內部使用了外面的引用 vm 和 Component,就會一直在內存中,爲防止內存泄漏,從新賦回原來的函數。
vue-property-decorator 依賴 vue-class-component 實現,主要用了內部提供的 createDecorator 方法。
若是你想增長更多裝飾器,也能夠經過調用 createDecorator 方法,原理很簡單,就是向選項對象上增長所需數據。
在 vue-class-component 中提供了工具函數 createDecorator 容許添加其餘額外的裝飾函數,統一掛載在 Component.decorators 上,並把 options 傳過去,對 options 增長鬚要的屬性,實際上會調用這些裝飾函數,讓這些函數有機會處理 options。
function componentFactory(Component) {
// 省略其餘代碼....
var decorators = Component.__decorators__;
if (decorators) {
decorators.forEach(function (fn) {
return fn(options);
});
delete Component.__decorators__;
}
}
複製代碼
咱們能夠利用 createDecorator,擴展其餘的裝飾器,vue-property-decorator 內部就是利用這個函數擴展了 @Prop、@Watch 等裝飾器。
function createDecorator(factory) {
return (target, key, index) => {
// 是函數類型,則爲裝飾的類;
// 不然,爲原型,經過constructor拿到構造函數
const Ctor = typeof target === 'function'
? target
: target.constructor;
if (!Ctor.__decorators__) {
Ctor.__decorators__ = [];
}
// 當爲參數裝飾器時,index爲number
if (typeof index !== 'number') {
index = undefined;
}
Ctor.__decorators__.push(options => factory(options, key, index));
};
}s
複製代碼
從源碼中能夠看出來,createDecorator 調用後會返回一個函數,這個函數能夠做爲裝飾器函數,接收的 target 若是是函數類型,說明做爲類裝飾器,target 就是被裝飾的類;不然,獲得的是原型,經過 constructor 拿到構造函數。
向要裝飾的類上添加靜態屬性 decorators,存入一個函數,得到 options。
如今來看 vue-property-decorator 中 watch 裝飾器的源碼,代碼地址
function Watch(path, options) {
if (options === void 0) { options = {}; }
return createDecorator(function (componentOptions, handler) {
if (typeof componentOptions.watch !== 'object') {
componentOptions.watch = Object.create(null);
}
var watch = 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: handler});
});
}
複製代碼
傳入 createDecorator 的回調函數,會接受兩個參數,componentOptions 爲一個對象,就是在上面 componentFactory 中調用 Component.decorators,傳入的對象,目的是向這個對象添加或增長 watch 屬性,給要裝飾的類使用;handler 是函數名字;
這樣使用:
@Component
class Home extend Vue {
message='新消息'
@watch('message')
messageHandler(){
console.log('當message改變後,執行這裏')
}
}
複製代碼
通過 @watch 裝飾器處理後,選項對象上會增長一段數據:
{
watch: {
message: 'messageHandler'
},
methods:{
messageHandler(){
console.log('當message改變後,執行這裏')
}
}
}
複製代碼
以上即是 vue-property-decorator 增長裝飾器的實現方式,對其餘裝飾器感興趣,能夠看倉庫源碼,作進一步瞭解,思路都大同小異。
以上若有誤差歡迎指正學習,謝謝。~~~~
github博客地址:github.com/WYseven/blo…,歡迎star。