您好,登錄后才能下訂單哦!
vue-class-component是vue作者尤大推出的一個支持使用class方式來開發vue單文件組件的庫。但是,在使用過程中我卻發現了幾個奇怪的地方。
首先,我們看一個簡單的使用例子:
// App.vue <script> import Vue from 'vue' import Component from 'vue-class-component' @Component({ props: { propMessage: String } }) export default class App extends Vue { // initial data msg=123 // use prop values for initial data helloMsg='Hello, '+this.propMessage // lifecycle hook mounted () { this.greet() } // computed get computedMsg () { return'computed '+this.msg } // method greet () { alert('greeting: '+this.msg) } } </script> //main.js import App from './App.vue' newVue({ el: '#app', router, store, components: { App }, template: '<App/>' })
在這個例子中,很容易發現幾個疑點:
1. App類居然沒有constructor構造函數;
2. 導出的類居然沒有被new就直接使用了。
3. msg=123,這是什么語法?
首先,針對前兩個疑問,需要說明一下,class不一定非得有構造函數,同樣也不一定非得使用new才能使用。熟悉原理的朋友應該知道,class只是一個ES6的語法糖,說白了還是一個Function而已。但是,這兩點無疑是class這個語法糖的重要價值所在,可這里卻偏偏沒用,不由讓人奇怪,甚至會想,既然不當class用,那為什么不干脆就用Function呢?
而第三點,卻是妥妥點的語法錯誤啊,為此我還特意打開了Chrome控制臺試驗了一下,確實報錯了。實驗結果如下:
那這到底是怎么回事呢?出于程序員的好奇心,我對vue-class-component的源碼探索了一番。下面就一起來看看,相信看完就可以解答上面的疑惑了。
第一步,在看源碼之前,必須對裝飾器的知識有一定了解。裝飾器種類有好幾種,vue-class-component中主要用了類裝飾器,本文只對類裝飾器做簡單介紹,更多信息請參閱阮老師的文章:ECMAScript 6入門。
類裝飾器,顧名思義,就是用來裝飾一個類的,說的直白點就是用于修改一個類的。它具體有兩種用法。如下:
// 用法一 function Decorator (target) { // 處理target return target } @Decorator class ClassTest () {} // 用法二 function DecoratorFactory (options) { return function Decorator (target) { //@todo 利用options一起處理target // 然后返回 return target } } @DecoratorFctory(options) class ClassTest () {}
在兩個用法中,我們將Decorator稱為裝飾器函數,DecoratorFactory稱為裝飾器工廠。
類裝飾器函數規定只能接收類構造函數本身,如果還需要額外的參數傳入,則需要使用裝飾器工廠函數。
我們以裝飾器工廠函數為例,說明其執行流程:
1. JS引擎首先會執行工廠函數,然后保存其返回的裝飾器函數;
2. 然后解析class,將其轉化為一個構造函數;
3. 將上述構造函數作為參數執行第一步得到的裝飾器函數。
4. 如果裝飾器函數有返回值,則會將類變量(如例子中的ClassTest變量)指向返回值,否則類變量仍然指向構造函數,基于JS引用變量的特點,即使仍指向原構造函數,這個構造函數也可能在裝飾器中被改造過了。
直接使用裝飾器函數的情況類似上面,只是少了裝飾器工廠這一步處理過程。
了解了基本知識,我們開始第二步,解析vue-class-component執行流程。這里將根據裝飾器的執行流程,分三個部分講解。第一,工廠函數做了什么;第二,class解析之后是什么樣的;第三,裝飾器函數又做了什么。
工廠函數做了什么?
// vue-class-component使用的是TS語法 // Component實際上是既作為工廠函數,又作為裝飾器函數 function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any { if (typeofoptions==='function') { // 區別一下。這里的命名雖然是工廠,其實它才是真正封裝裝飾器邏輯的函數 return componentFactory (options) } return function (Component:VueClass<Vue>){ return componentFactory(Component,options) } }
從源碼中可以看出,Component函數只是對參數進行了判斷,說明它既可以用作工廠函數,也可以用作裝飾器函數。而實際裝飾器的邏輯則被封裝在componentFactory函數里,這里對命名需要注意區分下,此工廠非彼工廠。
Class解析之后是什么樣的
在文章開頭我們就有疑問,在class中不經過constructor直接給其屬性賦值是不符合JS語法的,而且我們還在Chrome上試驗過了,確實會報錯。但我們在使用component-class-component時卻又實實在在那么干了,并且也沒什么問題,這是怎么回事呢?
事實上,Chrome等主流瀏覽器對于ES6以及更高級的ES7、ES8的支持是不完整的,很多功能特性都不支持,這也是我們平時為什么都會使用babel來將高級的ES語法轉換成ES5的原因。而我們前面提及的這點疑惑正是這個原因,Chrome不支持,不代表babel不支持。
不過,即便如此,我們又產生了一個新的疑惑,這種語法我沒見過,那么經過babel轉換后的class會是什么樣的呢?畢竟這個轉換結果會作為參數傳遞 給Component裝飾器來處理,要想了解Component的處理過程,這個參數需要先了解。
于是,我在Component函數內添加了一條console.log(),得到了打印后的結果,只是我使用的webpack+babel-loader執行的編譯,結果比較難以閱讀,我簡單翻譯了一下,并和class源碼一起對比如下:
// 轉換前 class User { name = 'yl' age = 10 get computeMethod () { cnsole.log(1) } method () { console.log(2) } } // 轉換后 function User () { this.name = 'yl' this.age = 10 } // 計算屬性定義 User.prototype.defineProperty(this, 'computeValue', { get () { console.log(1) return this.name } }) User.prototype.method = function () { console.log(2) }
由此,我們也可以推測出,一個.vue文件導出的類會被解析成什么樣子。
裝飾器函數又做了什么
此時,我們已經知曉了傳遞給裝飾器函數的參數是什么樣了。這個參數應該是一個構造函數,它的主體會對類實例的屬性進行賦值,它的原型則攜帶著各種屬性和方法。
而我們知道的,如果不使用vue-class-component,那么一個.vue文件應該導出如下對象:
export default { name: 'test', data () { return {...} }, computed: { com1 () {...}, com2 () {...} }, methods: {...}, // 各種hook函數 }
很顯然,裝飾器函數必然是將傳入的組件構造函數轉換成了一個vue配置對象。那么,具體內部是怎么做的呢?我們來看看源碼。(源碼筆者加上了詳細注釋,但較長,可以直接跳過看后面的總結。)
// 這個函數就是封裝了裝飾器邏輯的函數,接受兩個參數: // 第一個是所裝飾的類的構造函數;第二個是開發者傳入的mixins對象 function componentFactory ( Component: VueClass<Vue>, options: ComponentOptions<Vue> = {} ): VueClass<Vue> { // 首先給options.name賦值,確保最終生成的對象具有name屬性。 options.name = options.name || (Component as any)._componentTag || (Component as any).name // 獲取構造函數原型,這個原型上掛在了該類的method const proto = Component.prototype // 遍歷原型 Object.getOwnPropertyNames(proto).forEach(function (key) { // 如果是constructor,則不處理。 // 這也是為什么vue單文件組件類不需要constructor的直接原因,因為有也不會做任何處理 if (key === 'constructor') { return } // 如果原型屬性(方法)名是vue生命周期鉤子名,則直接作為鉤子函數掛載在options最外層 if ($internalHooks.indexOf(key) > -1) { options[key] = proto[key] return } // 先獲取到原型屬性的descriptor。 // 在前文已提及,計算屬性其實也是掛載在原型上的,所以需要對descriptor進行判斷 const descriptor = Object.getOwnPropertyDescriptor(proto, key)! if (descriptor.value !== void 0) { // 如果屬性值是一個function,則認為這是一個方法,掛載在methods下 if (typeof descriptor.value === 'function') { (options.methods || (options.methods = {}))[key] = descriptor.value } else { // 如果不是,則認為是一個普通的data屬性。 // 但是這是原型上,所以更類似mixins,因此掛在mixins下。 (options.mixins || (options.mixins = [])).push({ data (this: Vue) { return { [key]: descriptor.value } } }) } } else if (descriptor.get || descriptor.set) { // 如果value是undefined(ps:void 0 === undefined)。 // 且描述符具有get或者set方法,則認為是計算屬性。不理解的參考我上面關于class轉換成構造函數的例子 // 這里可能和普通的計算屬性不太一樣,因為一般計算屬性只是用來獲取值的,但這里卻有setter。 // 不過如果不使用setter,與非class方式開發無異,但有這一步處理,在某些場景會有特效。 (options.computed || (options.computed = {}))[key] = { get: descriptor.get, set: descriptor.set } } }) // 收集構造函數實例化對象的屬性作為data,并放入mixins (options.mixins || (options.mixins = [])).push({ data (this: Vue) { // 實例化Component構造函數,并收集其自身的(非原型上的)屬性導出,內部還針對不同vue版本做了兼容。 // 感興趣的可以自己去瞅瞅源碼,不復雜,在此不贅述。 return collectDataFromConstructor(this, Component) } }) // 處理屬性裝飾器,vue-class-component只提供了類裝飾器。 // 像props、components等特殊參數只能寫在Component(options)的options參數里。 // 通過這個接口可以擴展出屬性裝飾器,像vue-property-decorator庫那種的屬性裝飾器 const decorators = (Component as DecoratedClass).__decorators__ if (decorators) { decorators.forEach(fn => fn(options)) delete (Component as DecoratedClass).__decorators__ } // 獲取Vue對象 const superProto = Object.getPrototypeOf(Component.prototype) const Super = superProto instanceof Vue ? superProto.constructor as VueClass<Vue> : Vue // 通過vue.extend生成一個vue實例 const Extended = Super.extend(options) // 在前面只處理了Component構造函數原型和其實例化對象的屬性和方法。 // 對于構造函數本身的靜態屬性還沒有處理,在此處理,處理過程類似前面,不贅述。 forwardStaticMembers(Extended, Component, Super) // 反射相關處理,這個是新特性,本人了解也不多,但到此已經不影響理解了,所以可以略過。 // 如有對此了解的,歡迎補充。 if (reflectionIsSupported) { copyReflectionMetadata(Extended, Component) } // 最終返回這個vue實例對象 return Extended }
源碼較長,在此總結一下。這里主要做了四件事:
第一,將傳入的構造函數原型上的屬性放入data中,將方法根據是否是生命周期鉤子、是否是計算屬性,來分別放入對應的位置。
第二,實例化構造函數,將構造函數實例化對象的屬性放入data,實例化對象本身(不算原型上的)是不帶有方法的,即使某個屬性的值是function類型,也應該作為data來處理。
第三、對構造函數自身的靜態屬性和方法處理,處理方式同原型的處理方式。
第四,提供屬性裝飾器的拓展功能,Component只裝飾了類,如果想對類中的屬性做進一步的處理,可以從此入手,比如vue-property-decorator庫提供的那些裝飾器就是依賴這個拓展功能。
說到此,想必大家對前面的疑惑也釋然了,同時對vue-class-component的實現原理也有了一個大體的思路。因本人技術有限,文中可能存在膚淺、錯誤的地方,如有發現,還請不吝賜教,感謝!
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。