Vue躬行記(1)——數據綁定

  • 2019 年 10 月 9 日
  • 筆記

  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>&lt;span&gt;content&lt;/span&gt;</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);