使用過一段時間 class 來定義組件,要用 vue-property-decorator 提供定義好的裝飾器,輔助完成所需功能,對這個過程好奇,就研究了源碼。內部主要依靠 vue-class-component 實現,因此將重點放在對 vue-class-component 的解讀上。javascript
本文主要內容有:前端
沒有使用 class 方式定義組件時,一般導出一個選項對象:vue
<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() 建立組件的構造函數,以便在模板中使用時,經過構造函數初始化此組件。java
若是使用了 class 來定義組件,上面的字段可省略,但要符合 Vue 內部使用數據的規則,就須要重組這些數據。git
定義 class 組件:github
<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 須要解析的數據中,「上帝的歸上帝,凱撒的歸凱撒」。學習
最終處理成這樣:
{ 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博客地址:https://github.com/WYseven/blog,歡迎star。
若是對你有幫助,請關注【前端技能解鎖】: