Vue.js的核心是通過基於HTML的模板語法聲明式地將數據綁定到DOM結構中,即通過模板將數據顯示在頁面上,如下所示。
<div id="container">{{content}}</div> <script> var vm = new Vue({ el: "#container", data: { content: "strick" } }); </script>
其中<div>元素的內容是一個模板的插值,vm是一個Vue實例。
一、實例
如果要使用Vue的功能,那么需要通過Vue()構造函數創建一個Vue實例,而Vue實例相當於MVVM模式中的ViewModel。注意,所有的Vue組件(后面篇章將會分析)都是Vue實例。
1)選項對象
Vue的構造函數能接收一個選項對象,包含數據、計算屬性、方法、模板、生命周期鈎子等成員。上面代碼中的el是Vue實例的掛載目標,既可以是CSS選擇器,也可以是DOM元素;data是Vue實例的數據對象,其屬性會被加到Vue的響應式系統中,當修改data的屬性時,視圖會響應變更而重新渲染,即vm實例的對應屬性也會更新,反之亦然,如下所示。
var data = { content: "strick" }; var vm = new Vue({ el: "#container", data: data }); data.content = "freedom"; console.log(vm.content); //"freedom" vm.content = "justify"; console.log(data.content); //"justify"
注意,如果data屬性使用了箭頭函數,那么this不會指向vm實例。
在實例被創建之后,就能通過vm.$data訪問原來的數據對象,而vm.content是vm.$data.content的簡寫。注意,被凍結后的對象(即調用了Object.freeze()方法),其屬性是無法響應式的。
除了$data屬性之外,Vue實例還提供了很多其它的屬性和方法,它們都會以“$”符號為前綴,而為了避免與內置的沖突,Vue實例不會代理以“_”或“$”開頭的用戶自定義的屬性和方法。
2)生命周期
Vue實例的生命周期包括初始化數據、編譯模板、掛載、渲染、更新和銷毀等,每個階段都存在對應的鈎子,以便執行相關的業務邏輯。由於生命周期鈎子都會自動把this和實例綁定在一起,因此不要用箭頭函數來聲明鈎子。
常用的8個生命周期可分為4組(如下所列),每組有一個名稱帶before前綴,顧名思義,先於另一個鈎子執行,圖1描繪了實例的生命周期。
圖1 Vue實例的生命周期
(1)beforeCreate:實例初始化之后回調,無法訪問data、methods、computed等之中的數據或方法。
(2)created:實例創建完成后回調,可訪問data、methods、computed等之中的數據或方法,由於還未掛載到DOM中,因此不能成功讀取$el。
(3)beforeMount:實例掛載之前回調,將要使用的模板編譯成render()函數。
(4)mounted:實例掛載到DOM后回調,已替換模板中的插值,可獲取el中的DOM元素,但要注意,不能保證其子組件也已被掛載。
(5)beforeUpdate:數據更新時回調,發生在虛擬DOM之前,可操作現有DOM元素,例如移除其事件監聽器等。
(6)updated:DOM重新渲染后回調,可執行依賴於DOM的操作,但要在此期間盡量不要更改狀態,以免陷入死循環,並且不能保證其子組件也已被重繪。
(7)beforeDestroy:實例銷毀之前回調,此時實例還存在,this仍然能指向它。
(8)destroyed:實例銷毀后回調,會解除數據綁定、移除事件、銷毀子組件等。
除了這8個鈎子之外,還有3個鈎子,如下所列。
(1)activated:<keep-alive>元素激活時回調。
(2)deactivated:<keep-alive>元素停用時回調。
(3)errorCaptured:捕獲到后代組件的錯誤時回調。
二、模板語法
Vue的模板是一段特殊的HTML代碼,其語法包括插值、指令和修飾符。雖然Vue的模板語法非常簡潔,但是在內部Vue會進行一系列操作,例如將模板編譯成虛擬DOM的渲染函數render(),結合響應系統最大程度的優化DOM操作次數以及用最少的代價渲染組件等。
1)插值
Vue會以插值的方式將數據傳遞給模板,而插值可以是文本、HTML代碼、特性和表達式。
(1)文本插值是最常見的數據綁定形式,其寫法與Mustache中的占位符類似,也需要用兩個花括號包裹數據。當和v-once指令配合時,能實現單次插值,即阻止數據變化時的視圖更新。如下代碼所示,在修改數據對象的text屬性后,兩個<p>元素所生成的內容會有所不同,具體可參考對應的注釋。
<div id="container"> <!-- <p>strick</p> --> <p>{{text}}</p> <!-- <p>text</p> --> <p v-once>{{text}}</p> </div> <script> var data = { text: "text" }; var vm = new Vue({ el: "#container", data: data }); data.text = "strick"; </script>
(2)由於模板占位符中的數據會被解釋成普通文本(為了預防XSS攻擊),因此如果要輸出HTML代碼,需要使用v-html指令。如下代碼所示,第一個<p>元素在輸出HTML標簽時,它的兩個特殊字符都被轉義了。
<div id="container"> <!-- <p><span>content</span></p> --> <p>{{html}}</p> <!-- <p><span>content</span></p> --> <p v-html="html"></p> </div> <script> var vm = new Vue({ el: "#container", data: { html: "<span>content</span>" } }); </script>
(3)如果要將數據對象的屬性值插到DOM元素的特性(即定義在HTML標簽中的標准或非標准屬性)中,那么得使用v-bind指令,如下代碼所示。注意,當屬性值為null、undefined或false時,相應的特性不會被輸出到元素中。
<div id="container"> <!-- <p id="row"></p> --> <p v-bind:id="id"></p> </div> <script> var vm = new Vue({ el: "#container", data: { id: "row" } }); </script>
(4)模板占位符還支持表達式運算,如下代碼所示。注意,語句是不被允許的,並且在表達式中,只能訪問白名單里的全局變量,例如Math和Date。
<div id="container"> <!-- <p>success</p> --> <p>{{result ? "success" : "failure"}}</p> </div> <script> var vm = new Vue({ el: "#container", data: { result: true } }); </script>
2)指令
Vue中的指令(Directives)是一組以“v-”為前綴的DOM元素特性,它能接收一個表達式或參數。其職責是告知Vue如何處理提供給它的數據,並且當表達式的結果發生變化時,將其產生的影響反映到DOM上。
指令和參數之間會用冒號隔開,例如前文用於更新DOM元素特性的v-bind。還有一個常用的v-on指令,用於監聽事件,如下所示,其中click是事件類型,dot是事件處理程序。
<button v-on:click="dot">提交</button>
Vue為v-bind和v-on兩個指令提供了專用的縮寫(如下所示),分別用“:”和“@”符號表示。
<!-- v-bind的縮寫 --> <p :id="id"></p> <!-- v-on的縮寫 --> <button @click="dot">提交</button>
從Vue 2.6.0開始,引入了動態參數的概念,在冒號后面跟一個用方括號包裹的表達式,如下所示,其中type是數據對象的屬性,其值會作為參數來使用。
<button v-on:[type]="dot">提交</button>
動態參數中的表達式會有一些語法約束,例如運算結果得是字符串類型、不能包含空格和引號、避免駝峰方式的變量命名,如下所示。
<button v-on:[1234567]="dot">提交</button> <button v-on:[type + ""]="dot">提交</button> <button v-on:[eventType]="dot">提交</button>
在DOM中使用模板時,eventType會被強制轉換成全小寫的eventtype,從而就無法在數據對象中讀取到它的值了。
3)修飾符
Vue的修飾符(Modifier)是一種以“.”開頭的特殊后綴,能讓指令完成某種特殊行為,例如用.prevent修飾符取消默認操作,即調用事件對象的preventDefault()方法,如下所示。
<form v-on:submit.prevent="dot"></form>
三、過濾器
過濾器可用來格式化模板中的文本,存在於占位符和v-bind指令中,緊跟在表達式之后,其寫法如下所示,name是數據對象的屬性,lowercase是一個過濾器,兩者用“|”符號隔開。
{{ name | lowercase }} <button v-bind:name="name | lowercase"></button>
注意,自Vue 2.0起,所有的內置過濾器(例如capitalize、uppercase、json等)都已被移除,官方推薦按需加載更專業的庫來實現過濾。
1)創建
Vue允許用戶自定義過濾器,可在實例的filters選項中注冊局部過濾器,如下所示。
var vm = new Vue({ filters: { lowercase: function(value) { return value.toLowerCase(); } } });
也可以在創建Vue實例之前,通過Vue.filter()方法注冊全局過濾器,如下所示。
Vue.filter("lowercase", function (value) { return value.toLowerCase(); }); var vm = new Vue({...});
當局部過濾器和全局過濾器重名時,會優先采用局部過濾器。
2)鏈式調用
多個過濾器可通過“|”符號串聯實現鏈式調用,如下所示。
{{ name | lowercase | capitalize }}
lowercase過濾器會接收name的值,然后將其計算結果傳給capitalize過濾器。
3)傳遞參數
由於過濾器本質上還是一個函數,因此它支持多個參數的傳入,如下所示,compare過濾器會接收三個參數,分別是number和threshold兩個數據對象的屬性,以及一個常量10。
<p>{{number | compare(10, threshold)}}</p>
注意,Vue 2.0取消了用空格來標記過濾器參數的方式,下面的調用是無效的。
<p>{{number | compare 10 threshold }}</p>
四、計算屬性
在模板中適合簡單的聲明式邏輯,而應避免頻繁的進行復雜計算,這樣既不利於維護,也會讓模板結構變得臃腫而混亂。為了能合理的執行復雜表達式,Vue引入了計算屬性的概念。
計算屬性在模板中的數據綁定和普通屬性一樣,但需要以函數的方式來定義。在下面的代碼中,newName是一個計算屬性,用來讓name屬性重復兩次再提取末尾兩個字符,在它的getter函數中引用了一個指向vm實例的this。
<div id="container"> <p>{{newName}}</p> </div> <script> var vm = new Vue({ el: "#container", data: { name: "strick" }, computed: { newName: function() { return this.name.repeat(2).substr(-2); } } }); </script>
注意,計算屬性往往會依賴數據對象的屬性或其它計算屬性,也就是說,當依賴的屬性被修改時,計算屬性會自動更新。
1)緩存
計算屬性和方法有一個很大的不同,那就是它能被緩存。在下面的代碼中,聲明了一個getName()方法,雖然它的返回結果和之前的計算屬性newName的值相同,但是當依賴的name屬性不發生變化時,兩者的執行方式會有所不同。
var vm = new Vue({ methods: { getName: function() { return this.name.repeat(2).substr(-2); } } });
當多次訪問newName時,讀取的是其緩存的值,不會執行它的getter函數,而方法每次都會執行一遍。由於計算屬性能減少冗余的運算,因此它很適合處理那些耗時且性能開銷巨大的操作。
2)寫入
默認情況下只需要定義計算屬性的getter函數,不過Vue也為其提供了setter函數,使得計算屬性在寫入時能處理更為復雜的業務邏輯,如下所示。
var vm = new Vue({ el: "#container", data: { price: 10.2 }, computed: { total: { get: function() { return this.price * 10; }, set: function(value) { this.price = value + Math.round(this.price); } } } });
當為計算屬性total賦值時(如下所示),就會調用它的setter函數,並更新vm.price。
vm.total = 10;
五、響應式原理
Vue采用了非侵入性的響應式系統,當把數據對象傳給Vue實例的data屬性時,Vue會通過Object.defineProperty()方法將它的每個屬性替換成getter和setter兩個函數,下面用一個簡單的示例展示Vue的基本思路。
const data = { //數據對象 name: "strick" }; const proxyData = { name: data.name }; Object.defineProperty(data, "name", { get() { //注入監聽邏輯,並在必要時通知變更 return proxyData.name; }, set(value) { //注入監聽邏輯,並在必要時通知變更 proxyData.name = value; }, configurable: true, enumerable: true });
經過這波操作后,就能讓Vue擁有追蹤屬性變化的能力,並在屬性被訪問和修改時通知關聯的視圖重新渲染。在體驗響應式所帶來的便利的同時,也要知曉它的一些限制,接下來會分析Vue檢測對象和數組發生變動時的注意事項。
1)對象
由於JavaScript無法監聽對象屬性的添加或刪除,因此只有在Vue實例化時才能對數據對象的根屬性做getter和setter的替換,即轉換成響應式的屬性。Vue不允許動態添加根級的響應式屬性,這些屬性必須預先聲明,如下所示,雖然age是vm實例的一個根屬性,但它是在實例化后聲明的,所以也就無法成為響應式的屬性了。
var vm = new Vue({ data: { name: "strick" //響應式屬性 } }); vm.age = 28; //非響應式屬性
除了內部的技術限制之外,提前聲明響應式屬性,也便於開發人員理解代碼的意圖。對於已創建的實例,有兩種方式聲明非根級的響應式屬性,第一種是用全局的Vue.set()方法或Vue實例的$set()方法,在下面的代碼中,為people對象聲明了一個響應式的age屬性。
var vm = new Vue({ data: { people: { name: "freedom" } } }); Vue.set(vm.people, "age", 28);
第二種是用Object.assign()方法,可一次性添加多個屬性,如下所示,將原對象和新增的屬性合並成一個新對象,再賦給vm.people。
vm.people = Object.assign({}, vm.people, { age: 28, school: "university" });
2)數組
Vue無法檢測下面兩種數組的變動,以vm實例的names屬性為例。
(1)通過索引設置數組的元素。
(2)縮短數組的長度。
var vm = new Vue({ data: { names: ["strick", "freedom"] } }); vm.names[1] = "justify"; //第一種變動 vm.names.length = 1; //第二種變動
要檢測第一種變動,可以使用Vue.set()方法或數組的splice()方法,而要檢測第二種變動,就只能使用splice()方法了,如下所示。
//檢測第一種變動 Vue.set(vm.names, 1, "justify"); vm.names.splice(1, 1, "justify"); //檢測第二種變動 vm.names.splice(1, 1);