Vue是一款高度封裝的、開箱即用的、一棧式的前端框架,既可以結合webpack進行編譯式前端開發,也適用基于gulp、grunt等自動化工具直接掛載至全局window
使用。本文成文于Vue2.4.x版本發布之初,筆者生產環境當前使用的最新版本為2.5.2。在經歷多個前端重度交互項目的開發實踐之后,筆者結合官方文檔對Vue技術棧進行了全面的梳理、歸納和注解,因此本文可以作為Vue2官方tutorial的補充性讀物。建議暫不具備Vue2開發經驗的同學,完成官方tutorial的學習之后再行閱讀本文。
Vue2.2.x之后,Vue框架及其技術棧功能日趨完善,Vue更加貼近W3C技術規范(例如實現仍處于W3C草案階段的<template>
、<slot>
、is
等新特性,提供了良好易用的模板書寫環境),并且技術棧和開源生態更加完整和易于配置,將React中大量需要手動編碼處理的位置,整合成最佳實踐并抽象為簡單的語法糖(比如Vuex中提供的store
的模塊化特性),讓開發人員始終將精力聚焦于業務邏輯本身。
Vue2的API結構相比Angular2更加簡潔,可以自由的結合TypeScript或是ECMAScript6使用,并不特定于具體的預處理語言去獲得最佳使用體驗,框架本身的特性也并不強制依賴于各類炫酷的語法糖。Vue2總體是一款非常輕量的技術棧,設計實現上緊隨W3C技術規范,著力于處理HTML模板組件化、事件和數據的作用域分離、多層級組件通信三個單頁面前端開發當中的重點問題。本文在行文過程中,穿插描述了Angular、React等前端框架的異同與比較,供徘徊于各類前端技術選型的開發人員參考。
Vue與Angular的比較
組件化
Angular的設計思想照搬了Java Web開發當中MVC分層的概念,通過Controller
切割并控制頁面作用域,然后通過Service
來實現復用,是一種對頁面進行縱向分層的解耦思想。而Vue允許開發人員將頁面抽象為若干獨立的組件,即將頁面DOM結構進行橫向切割,通過組件的拼裝來完成功能的復用、作用域控制。每個組件只提供props
作為單一接口,并采用Vuex進行state tree
的管理,從而便捷的實現組件間狀態的通信與同步。
Angular在1.6.x版本開始提供component()
方法和Component Router
來提供組件化開發的體驗,但是依然需要依賴于controller
和service
的劃分,實質上依然沒有擺脫MVC縱向分層思想的桎梏。
雙向綁定與響應式綁定
Vue遍歷data對象上的所有屬性,并通過原生Object.defineProperty()
方法將這些屬性轉換為getter/setter
(只支持IE9及以上瀏覽器)。Vue內部通過這些getter/setter追蹤依賴,在屬性被修改時觸發相應變化,從而完成模型到視圖的雙向綁定。每個Vue組件實例化時,都會自動調用$watch()
遍歷自身的data屬性,并將其記錄為依賴項,當這些依賴項的setter被觸發時會通知watcher重新計算新值,然后觸發Vue組件的render()
函數重新渲染組件。
與Aangular雙向數據綁定不同,Vue組件不能檢測到實例化后data屬性的添加、刪除,因為Vue組件在實例化時才會對屬性執行getter/setter處理,所以data對象上的屬性必須在實例化之前存在,Vue才能夠正確的進行轉換。因而,Vue提供的并非真正意義上的雙向綁定,更準確的描述應該是單向綁定,響應式更新,而Angular即可以通過$scope
影響view上的數據綁定,也可以通過視圖層操作$scope
上的對象屬性,屬于真正意義上的視圖與模型的雙向綁定。
var vm = new Vue({
data:{
a:1
}
})
vm.a = 1 // 響應的
vm.b = 2 // 非響應的
因此,Vue不允許在已經實例化的組件上添加新的動態根級響應屬性(即直接掛載在data下的屬性),但是可以使用Vue.set(object, key, value)
方法添加響應式屬性。
Vue.set(vm.someObject, "b", 2)
// vm.$set()實例方法是Vue.set()全局方法的別名
this.$set(this.someObject, "b",2)
// 使用Object.assign()或_.extend()也可以添加響應式屬性,但是需要創建
// 同時包含原屬性、新屬性的對象,從而有效觸發watch()方法
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
Vue對DOM的更新是異步的,觀察到數據變化后Vue將開啟一個隊列,緩沖在同一事件循環(Vue的event loop被稱為tick* [t?k] n.標記,記號*)中發生的所有數據變化。如果同一個watcher被多次觸發,只會向這個隊列中推入一次。
Vue內部會通過原生JavaScript的
Promise.then
、MutationObserver
、setTimeout(fn, 0)
來執行異步隊列當中的watcher。
在需要人為操作DOM的場景下,為了在Vue響應數據變化之后再更新DOM,可以手動調用Vue.nextTick(callback)
,并將DOM操作邏輯放置在callback回調函數中,從而確保響應式更新完成之后再進行DOM操作。
<div id="example">{{message}}</div>
<script>
// 使用Vue實例上的.$nextTick()
var vue = new Vue({
el: "#example",
data: {
message: "123"
}
})
vue.message = "new message" // 更改數據
vue.$el.textContent === "new message" // false
vue.nextTick(function () {
vm.$el.textContent === "new message" // true
})
</script>
<script>
// 組件內使用vm.$nextTick(),不需要通過全局Vue,且回調函數中this自動指向當前Vue實例
Vue.component("example", {
template: "<span>{{ message }}</span>",
data: function () {
return {
message: "沒有更新"
}
},
methods: {
updateMessage: function () {
this.message = "更新完成"
console.log(this.$el.textContent) // 沒有更新
this.$nextTick(function () {
console.log(this.$el.textContent) // 更新完成
})
}
}
})
</script>
虛擬DOM
Vritual DOM這個概念最先由React引入,是一種DOM對象差異化比較方案,即將DOM對象抽象成為Vritual DOM對象(即render()函數渲染的結果),然后通過差異算法對Vritual DOM進行對比并返回差異,最后通過一個補丁算法將返回的差異對象應用在真實DOM結點。
Vue當中的Virtual DOM對象被稱為VNode(template
當中的內容會被編譯為render()函數,而render()函數接收一個createElement()函數,并最終返回一個VNode對象),補丁算法來自于另外一個開源項目snabbdom,即將真實的DOM操作映射成對虛擬DOM的操作,通過減少對真實DOM的操作次數來提升性能。
? vdom git:(dev) tree
├── create-component.js
├── create-element.js
├── create-functional-component.js
├── helpers
│ ├── extract-props.js
│ ├── get-first-component-child.js
│ ├── index.js
│ ├── is-async-placeholder.js
│ ├── merge-hook.js
│ ├── normalize-children.js
│ ├── resolve-async-component.js
│ └── update-listeners.js
├── modules
│ ├── directives.js
│ ├── index.js
│ └── ref.js
├── patch.js
└── vnode.js
VNode的設計出發點與Angular的$digest
循環類似,都是通過減少對真實DOM的操作次數來提升性能,但是Vue的實現更加輕量化,摒棄了Angular為了實現雙向綁定而提供的$apply()
、$eval()
封裝函數,有選擇性的實現Angular中$compile()
、$watch()
類似的功能。
Vue對象的選項
通過向構造函數new Vue()
傳入一個option
對象去創建一個Vue實例。
var vm = new Vue({
// 數據
data: "聲明需要響應式綁定的數據對象",
props: "接收來自父組件的數據",
propsData: "創建實例時手動傳遞props,方便測試props",
computed: "計算屬性",
methods: "定義可以通過vm對象訪問的方法",
watch: "Vue實例化時會調用$watch()方法遍歷watch對象的每個屬性",
// DOM
el: "將頁面上已存在的DOM元素作為Vue實例的掛載目標",
template: "可以替換掛載元素的字符串模板",
render: "渲染函數,字符串模板的替代方案",
renderError: "僅用于開發環境,在render()出現錯誤時,提供另外的渲染輸出",
// 生命周期鉤子
beforeCreate: "在Vue實例初始化之后,data observer和event/watcher事件被配置之前",
created: "發生在Vue實例初始化以及data observer和event/watcher事件被配置之后",
beforeMount: "掛載開始之前被調用,此時render()首次被調用",
mounted: "el被新建的vm.$el替換,并掛載到實例上之后調用",
beforeUpdate: "數據更新前調用,在虛擬DOM重新渲染和打補丁之前觸發",
updated: "數據更改導致虛擬DOM重新渲染和打補丁之后被調用",
activated: "keep-alive組件激活時調用",
deactivated: "keep-alive組件停用時調用",
beforeDestroy: "實例銷毀之前調用,Vue實例依然可用",
destroyed: "Vue實例銷毀后調用,事件監聽和子實例全部被移除,釋放系統資源",
// 資源
directives: "包含Vue實例可用指令的哈希表",
filters: "包含Vue實例可用過濾器的哈希表",
components: "包含Vue實例可用組件的哈希表",
// 組合
parent: "指定當前實例的父實例,子實例用this.$parent訪問父實例,父實例通過$children數組訪問子實例",
mixins: "將屬性混入Vue實例對象,并在Vue自身實例對象的屬性被調用之前得到執行",
extends: "用于聲明繼承另一個組件,從而無需使用Vue.extend,便于擴展單文件組件",
provide&inject: "2個屬性需要一起使用,用來向所有子組件注入依賴,類似于React的Context",
// 其它
name: "允許組件遞歸調用自身,便于調試時顯示更加友好的警告信息",
delimiters: "改變模板字符串的風格,默認為{{}}",
functional: "讓組件無狀態(沒有data)和無實例(沒有this上下文)",
model: "允許自定義組件使用v-model時定制prop和event",
inheritAttrs: "默認情況下,父作用域的非props屬性綁定會應用在子組件的根元素上。
當編寫嵌套有其它組件或元素的組件時,可以將該屬性設置為false關閉這些默認行為",
comments: "設為true時會保留并且渲染模板中的HTML注釋"
});
Vue實例通常使用
vm
變量(View Model)來命名。
屬性計算computed
在HTML模板表達式中放置太多業務邏輯,會讓模板過重且難以維護。因此,可以考慮將模板中比較復雜的表達式拆分到computed屬性當中進行計算。
<!-- 不使用計算屬性 -->
<div id="example">
{{ message.split("").reverse().join("") }}
</div>
<!-- 將表達式抽象到計算屬性 -->
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
<script>
var vm = new Vue({
el: "#example",
data: {
message: "Hello"
},
computed: {
reversedMessage: function () {
return this.message.split("").reverse().join("")
}
}
})
</script>
計算屬性只在相關依賴發生改變時才會重新求值,這意味只要上面例子中的message沒有發生改變,多次訪問reversedMessage計算屬性總會返回之前的計算結果,而不必再次執行函數,這是computed和method的一個重要區別。
計算屬性默認只擁有getter方法,但是可以自定義一個setter方法。
<script>
... ... ...
computed: {
fullName: {
// getter
get: function () {
return this.firstName + " " + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(" ")
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
... ... ...
// 下面語句觸發setter方法,firstName和lastName也會被相應更新
vm.fullName = "John Doe"
</script>
觀察者屬性watch
通過watch屬性可以手動觀察Vue實例上的數據變動,當然也可以調用實例上的vm.$watch
達到相同的目的。
<div id="watch-example">
<p>Ask a yes/no question: <input v-model="question"></p>
<p>{{ answer }}</p>
</div>
<script>
var watchExampleVM = new Vue({
el: "#watch-example",
data: {
question: "",
answer: "I cannot give you an answer until you ask a question!"
},
watch: {
// 如果question發生改變,該函數就會運行
question: function (newQuestion) {
this.answer = "Waiting for you to stop typing..."
this.getAnswer()
}
},
methods: {
// _.debounce是lodash當中限制操作頻率的函數
getAnswer: _.debounce(
function () {
if (this.question.indexOf("?") === -1) {
this.answer = "Questions usually contain a question mark. ;-)"
return
}
this.answer = "Thinking..."
var vm = this
axios.get("https://yesno.wtf/api")
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = "Error! Could not reach the API. " + error
})
},
// 這是用戶停止輸入等待的毫秒數
500
)
}
})
</script>
使用watch屬性的靈活性在于,當監測到數據變化的時候,可以做一些設置中間狀態之類的過渡處理。
生命周期
每個Vue實例在創建時,都需要經過一系列初始化過程(設置數據監聽、編譯模板、掛載實例到DOM、在數據變化時更新DOM),并在同時運行一些鉤子函數,讓開發人員能夠在特定生命周期內執行自己的代碼。
不要在Vue實例的屬性和回調上使用箭頭函數,比如
created: () => console.log(this.a)
或vm.$watch("a", newValue => this.myMethod())
。因為箭頭函數的this與父級上下文綁定,并不指向Vue實例本身,所以前面代碼中的this.a
或this.myMethod
將會是undefined
。
通過jQuery對DOM進行的操作可以放置在
Mounted
屬性上進行,即當Vue組件已經完成在DOM上掛載的時候。
數據綁定
Vue視圖層通過Mustache["m?st??]
語法與Vue實例中的data屬性進行響應式綁定,但是也可以通過內置指令v-once
完成一個單向的綁定,再或者通過v-html
指令將綁定的字符串輸出為HTML,雖然這樣很容易招受XSS攻擊。
<span>Message: {{ result }}</span>
<span v-once>一次性綁定: {{ msg }}</span>
<div v-html="rawHtml"></div>
Mustache不能用于HTML屬性,此時需要借助于v-bind
指令。
<div v-bind:id="dynamicId"></div>
<button v-bind:disabled="isButtonDisabled">Button</button>
綁定HTML的class和style
直接操作class
與style
屬性是前端開發當中的常見需求,Vue通過v-bind:class
和v-bind:style
指令有針對性的對這兩種操作進行了增強。
v-bind:class
綁定HTML的class
屬性。
<!-- Vue對象中的data -->
<script>
... ...
data: {
isActive: true,
hasError: false,
classObject: {
active: true,
"text-danger": false
}
}
... ...
</script>
<!-- 直接綁定class到一個對象 -->
<div v-bind:class="classObject"></div>
<!-- 直接綁定class到對象的屬性 -->
<div class="static" v-bind:class="{ active: isActive,
text-danger: hasError }"></div>
<!-- 渲染結果 -->
<div class="static active"></div>
可以傳遞一個數組給v-bind:class
從而同時設置多個class屬性。
<!-- Vue對象中的data -->
<script>
... ...
data: {
activeClass: "active",
errorClass: "text-danger"
}
... ...
</script>
<!-- 綁定class到計算屬性 -->
<div v-bind:class="[activeClass, errorClass]"></div>
<!-- 渲染結果 -->
<div class="active text-danger"></div>
<!-- 使用三目運算符,始終添加errorClass,只在isActive為true時添加activeClass -->
<div v-bind:class="[isActive ? activeClass : "", errorClass]"></div>
<!-- 在數組中使用對象可以避免三目運算符的繁瑣 -->
<div v-bind:class="[{ active: isActive }, errorClass]"></div>
當在自定義組件上使用class
屬性時,這些屬性將會被添加到該組件的根元素上面,這一特性同樣適用于v-bind:class
。
<!-- 聲明一個組件 -->
<script>
Vue.component("my-component", {
template: "<p class="foo bar">Hi</p>",
data: {
isActive: true
},
})
</script>
<!-- 添加2個class屬性 -->
<my-component class="baz boo"></my-component>
<!-- 渲染結果 -->
<p class="foo bar baz boo">Hi</p>
<!-- 使用v-bind:class -->
<my-component v-bind:class="{ active: isActive }"></my-component>
<!-- 渲染結果 -->
<p class="foo bar active">Hi</p>
v-bind:style
綁定HTML的style
屬性。
<script>
... ...
data: {
styleObject: {
color: "red",
fontSize: "13px"
},
styleHeight: {
height: 10rem;
}
styleWidth: {
width: 20rem;
}
}
... ...
</script>
<div v-bind:style="styleObject"></div>
<!-- 使用數組可以將多個樣式合并到一個HTML元素上面 -->
<div v-bind:style="[styleHeight, styleWidth]"></div>
使用v-bind:style
時Vue會自動添加prefix前綴,常見的prefix前綴如下:
-
-webkit-
Chrome、Safari、新版Opera、所有iOS瀏覽器(包括iOS版Firefox),幾乎所有WebKit內核瀏覽器。 -
-moz-
針對Firefox瀏覽器。 -
-o-
未使用WebKit內核的老版本Opera。 -
-ms-
微軟的IE以及Edge瀏覽器。
使用JavaScript表達式
Vue對于所有數據綁定都提供了JavaScript表達式支持,但是每個綁定只能使用1個表達式。
<span>{{ number + 1 }}</span>
<button>{{ ok ? "YES" : "NO" }}</button>
<p>{{ message.split("").reverse().join("") }}</p>
<div v-bind:id=""list-" + id"></div>
<!-- 這是語句,不是表達式 -->
{{ var a = 1 }}
<!-- if流程控制屬于多個表達式,因此不會生效,但可以使用三元表達式 -->
{{ if (ok) { return message } }}
v-model雙向數據綁定
v-model
指令實質上是v-on
和v-bind
的糖衣語法,該指令會接收一個value屬性
,存在新值時則觸發一個input事件
。
<!-- 使用v-model的版本 -->
<input v-model="something">
<!-- 使用v-on和v-bind的版本 -->
<input v-bind:value="something"
v-on:input="something = $event.target.value">
<!-- 也可以自定義輸入域的響應式綁定 -->
<custom-input
v-bind:value="something"
v-on:input="something = arguments[0]">
</custom-input>
單選框、復選框一類的輸入域將value屬性作為了其它用途,因此可以通過組件的
model
選項來避免沖突:
內置指令
帶有v-
前綴,當表達式值發生變化時,會響應式的將影響作用于DOM。指令可以接收后面以:
表示的參數(被指令內部的arg屬性接收),或者以.
開頭的修飾符(指定該指令以特殊方式綁定)。
<p v-if="seen">Hello world!</p>
<!-- 綁定事件 -->
<a v-bind:href="url"></a>
<!-- 綁定屬性 -->
<a v-on:click="doSomething">
<!-- .prevent修飾符會告訴v-on指令對于觸發的事件調用event.preventDefault() -->
<form v-on:submit.prevent="onSubmit"></form>
Vue為v-bind
和v-on
這兩個常用的指令提供了簡寫形式:
和@
。
<!-- v-bind -->
<a v-bind:href="url"></a>
<a :href="url"></a>
<!-- v-on -->
<a v-on:click="doSomething"></a>
<a @click="doSomething"></a>
目前,Vue在2.4.2版本當中提供了如下的內置指令:
<html
v-text = "更新元素的textContent"
v-html = "更新元素的innerHTML"
v-show = "根據表達式的true/false,切換HTML元素的display屬性"
v-for = "遍歷內部的HTML元素"
v-pre = "跳過表達式渲染過程,可以顯示原始的Mustache標簽"
v-cloak = "保持在HTML元素上直到關聯實例結束編譯,可以隱藏未編譯的Mustache"
v-once = "只渲染元素和組件一次"
></html>
<!-- 根據表達式的true和false來決定是否渲染元素 -->
<div v-if="type === "A"">A</div>
<div v-else-if="type === "B"">B</div>
<div v-else-if="type === "C"">C</div>
<div v-else>Not A/B/C</div>
<!-- 動態地綁定屬性或prop到表達式 -->
<p v-bind:attrOrProp
.prop = "被用于綁定DOM屬性"
.camel = "將kebab-case特性名轉換為camelCase"
.sync = "語法糖,會擴展成一個更新父組件綁定值的v-on監聽器"
></p>
<!-- 綁定事件監聽器 -->
<button
v-on:eventName
.stop = "調用event.stopPropagation()"
.prevent = "調用event.preventDefault()"
.capture = "添加事件監聽器時使用capture模式"
.self = "當事件是從監聽器綁定的元素本身觸發時才觸發回調"
.native = "監聽組件根元素的原生事件"-
.once = "只觸發一次回調"
.left = "點擊鼠標左鍵觸發"
.right = "點擊鼠標右鍵觸發"
.middle = "點擊鼠標中鍵觸發"
.passive = "以{passive: true}模式添加監聽器"
.{keyCode | keyAlias} = "觸發特定鍵觸事件"
>
</button>
<!-- 表單控件的響應式綁定 -->
<input
v-model
.lazy = "取代input監聽change事件"
.number = "輸入字符串轉為數字"
.trim = "過濾輸入的首尾空格" />
組件
組件可以擴展HTML元素功能,并且封裝可重用代碼。可以通過Vue.component( id, [definition] )
注冊或者獲取全局組件。
// 注冊組件,傳入一個擴展過的構造器
Vue.component("my-component", Vue.extend({ ... }))
// 注冊組件,傳入一個option對象(會自動調用Vue.extend)
Vue.component("my-component", { ... })
// 獲取注冊的組件(始終返回構造器)
var MyComponent = Vue.component("my-component")
下面代碼創建了一個Vue實例,并將自定義組件my-component
掛載至HTML當中。
<script>
// 注冊自定義組件
Vue.component("my-component", {
template: "<div>A custom component!</div>"
})
// 創建Vue根實例
new Vue({
el: "#example"
})
</script>
<!-- 原始模板 -->
<div id="example">
<my-component></my-component>
</div>
<!-- 渲染結果 -->
<div id="example">
<div>A custom component!</div>
</div>
- is屬性
瀏覽器解析完HTML之后才會渲染Vue表達式,但是諸如<ul> <ol> <table> <select>
限制了可以被包裹的HTML元素,而<option>
只能出現在某些HTML元素內部,造成Vue表達式可能不會被正確的渲染。因此,Vue提供is
作為屬性別名來解決該問題。
<!-- 不正確的方式 -->
<table>
<my-row>...</my-row>
</table>
<!-- 使用is的正確方式 -->
<table>
<tr is="my-row"></tr>
</table>
- data必須是函數
Vue.component()
傳入的data屬性不能是對象,而必須是函數。這樣做的目的是避免組件在相同模板的多個位置被復用時,僅僅返回對象會造成組件間的數據被相互污染,而通過函數每次都返回全新的data對象能完美的規避這個問題。
Vue.component("simple-counter", {
template: "<button v-on:click="counter += 1">{{ counter }}</button>",
data: function () {
return {
a: "",
b: ""
}
}
});
- 父子組件之間的通信
父組件通過props
向下傳遞數據給子組件,子組件通過events
給父組件發送消息,即props 向下傳, events 向上傳。
props
雖然每個組件的作用域都是獨立的,但是可以通過props屬性
向子組件傳遞數據,這是一種單向數據流的體現形式。
Vue.component("child", {
// 聲明props
props: ["message"],
// 和data屬性一樣,prop也可以在vm通過this.message進行引用
template: "<span>{{ message }}</span>"
})
不要在子組件內部修改props,這樣會導致后臺報錯。
命名方式轉換
因為HTML并不區分大小寫,所以kebab-case(駝峰)風格命名的props,在組件中會以camelCased(短橫線隔開)風格被接收。
<!-- camelCase in JavaScript -->
<script>
Vue.component("child", {
props: ["myMessage"],
template: "<span>{{ myMessage }}</span>"
})
<script>
<!-- kebab-case in HTML -->
<child my-message="hello!"></child>
動態props
可以通過v-bind
指令,響應式的綁定父組件數據到子組件的props。當父組件數據變化時,該變化也會傳導至子組件。
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
使用v-bind
可以讓其參數值能夠以JavaScript表達式的方式被解析,否則所有傳入的props都會被子組件認為是字符串類型。
<!-- 傳遞的是字符串"1" -->
<comp some-prop="1"></comp>
<!-- 傳遞實際的 number -->
<comp v-bind:some-prop="1"></comp>
驗證props
可以為組件的props指定驗證規則,如果傳入數據不符合要求,Vue會發出相應警告,這樣可以有效提高組件的健壯性。
Vue.component("example", {
props: {
// 基礎類型檢測
propA: Number,
// 多種類型
propB: [String, Number],
// 必傳且是字符串
propC: {
type: String,
required: true
},
// 數字,有默認值
propD: {
type: Number,
default: 100
},
// 數組或對象的默認值由1個工廠函數返回
propE: {
type: Object,
default: function () {
return { message: "hello" }
}
},
// 自定義驗證函數
propF: {
validator: function (value) {
return value > 10
}
}
}
});
props
會在組件實例創建之前進行校驗。
組件的非props屬性
組件可以接收任意傳入的屬性,這些屬性都會被添加到組件HTML模板的根元素上(無論有沒有在props中定義)。
<!-- 帶有屬性的自定義組件 -->
<bs-date-input
data-3d-date-picker="true"
class="date-picker-theme-dark">
</bs-date-input>
<!-- 渲染出來的組件,class屬性被合并 -->
<input type="date" data-3d-date-picker="true"
class="form-control date-picker-theme-dark">
父組件傳遞給子組件的屬性可能會覆蓋子組件本身的屬性,因而會對子組件造成破壞和污染。
事件
子組件可以通過Vue的自定義事件與父組件進行通信。
每個Vue實例都實現了如下API,但是并不能直接通過$on監聽子組件冒泡的事件,而必須使用v-on指令。
-
$on(eventName)
監聽事件 -
$emit(eventName)
觸發事件
$on
和$emit
并不是addEventListener
和dispatchEvent
的別名。
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
<script>
Vue.component("button-counter", {
template: "<button v-on:click="incrementCounter">{{counter}}</button>",
data: function () {
return {
counter: 0
}
},
methods: {
// 子組件事件
incrementCounter: function () {
this.counter += 1
this.$emit("increment") //向父組件冒泡事件
}
},
})
new Vue({
el: "#counter-event-example",
data: {
total: 0
},
methods: {
// 父組件事件
incrementTotal: function () {
this.total += 1
}
}
})
</script>
-
.native
修飾符
開發人員也可以在組件的根元素上監聽原生事件,這個時候需要借助到.native
修飾符。
<my-component v-on:click.native="doTheThing"></my-component>
-
.sync
修飾符
Vue中的props
本質是不能進行響應式綁定的,以防止破壞單向數據流,造成多個子組件對父組件狀態形成污染。但是生產環境下,props
響應式綁定的需求是切實存在的。因此,Vue將.sync
修飾符封裝為糖衣語法,父組件在子組件的props使用該修飾符后,父組件會為props自動綁定v-on
事件,子組件則在監聽到props變化時向父組件$emit
更新事件,從而讓父組件的props
能夠與子組件進行同步。
<!-- 使用.sync修飾符 -->
<comp :foo.sync="bar"></comp>
<!-- 被自動擴展為如下形式,該組件的子組件會通過this.$emit("update:foo", newValue)
顯式觸發更新事件 -->
<comp :foo="bar" @update:foo="val => bar = val"></comp>
- 平行組件通信
非父子關系的組件進行通信時,可以使用一個空的Vue實例作為中央事件總線。
var bus = new Vue()
// 觸發組件A中的事件
bus.$emit("id-selected", 1)
// 在組件B監聽事件
bus.$on("id-selected", function (id) {
... ... ...
})
更好的方式是借助VueX或者Redux之類的flux狀態管理庫。
slot
可以將父組件的內容混入到子組件的模板當中,此時可以在子組件中使用<slot>
作為父組件內容的插槽。
父組件模板的內容在父組件作用域內編譯,子組件模板的內容在子組件作用域內編譯。
匿名插槽
當子組件只有一個沒有屬性的<slot>
時,父組件全部內容片段將插入到插槽所在的DOM位置,并替換插槽標簽本身。
<!-- 子組件my-component的模板 -->
<div>
<h2>Child</h2>
<slot>
父組件沒有需要插入的內容時顯示
</slot>
</div>
<!-- 父組件模板中使用my-component -->
<div>
<h1>Parent</h1>
<child>
<p>Content 1</p>
<p>Content 2</p>
</child>
</div>
<!-- 渲染結果 -->
<div>
<h1>Parent</h1>
<div>
<h2>Child</h2>
<p>Content 1</p>
<p>Content 2</p>
</div>
</div>
<slot>
標簽中的內容會在子組件作用域內編譯,并在父組件沒有需要插入的內容時才會顯示。
具名插槽
可以通過<slot>
元素的name
屬性來配置如何分發內容。
<!-- 子組件 -->
<div id="app">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!-- 父組件 -->
<app>
<div slot="header">Header</div>
<p>Content 1</p>
<p>Content 2</p>
<div slot="footer">Footer</div>
</app>
<!-- 渲染結果 -->
<div id="app">
<header>
<div>Header</div>
</header>
<main>
<p>Content 1</p>
<p>Content 2</p>
</main>
<footer>
<p>Footer</p>
</footer>
</div>
匿名slot會作為沒有匹配內容的父組件片段的插槽。
作用域插槽
子組件通過props
傳遞數據給<slot>
插槽,父組件使用帶有scope
屬性的<template>
來表示表示當前作用域插槽的模板,scope
值對應的變量會接收子組件傳遞來的props對象。
<!-- 子組件通過props傳遞數據給插槽 -->
<div class="child">
<slot text="hello from child"></slot>
</div>
<!-- 父組件使用帶有scope屬性的<template> -->
<div class="parent">
<child>
<template scope="props">
<span>hello from parent</span>
<span>{{ props.text }}</span>
</template>
</child>
</div>
<!-- 渲染結果 -->
<div class="parent">
<div class="child">
<span>hello from parent</span>
<span>hello from child</span>
</div>
</div>
函數化組件
即無狀態(沒有data)無實例(沒有this上下文)的組件,渲染開銷較小,且不會出現在Vue devtools
當中。
Vue.component("my-component", {
functional: true,
// 通過提供context參數為沒有實例的函數組件提供上下文信息
render: function (createElement, context) {},
// Props可選
props: {}
})
動態組件
使用<component>
元素并動態綁定其is
屬性,可以讓多個組件使用相同的Vue對象掛載點,并實現動態切換。
<script>
var vm = new Vue({
el: "#example",
data: {
currentView: "home"
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
</script>
<component v-bind:is="currentView">
<!-- 組件在vm.currentview變化時改變! -->
</component>
如果需要將切換的組件保持在內存,保留其狀態并且避免重新渲染,可以使用Vue內置的keep-alive
指令。
<keep-alive>
<component :is="currentView">
<!-- 非活動組件將被緩存! -->
</component>
</keep-alive>
組件異步加載
Vue允許將組件定義為工廠函數,從而異步的解析組件定義。Vue只會在組件渲染時才觸發工廠函數,并將結果緩存起來用于后續渲染。定義組件的工廠函數將會接收resolve(接收到從服務器下載的Vue組件options時被調用)和reject(當遠程Vue組件options加載失敗時調用)回調函數作為參數。
Vue.component("async-example", function (resolve, reject) {
setTimeout(function () {
// 將組件定義傳遞到resolve回調函數當中
resolve({
template: "<div>I am async!</div>"
})
}, 1000)
})
可以結合Webpack提供的代碼切割功能,將Vue組件的options對象提取到單獨JavaScript文件,從而實現異步的按需加載。
// 使用webpack的require()來進行異步代碼塊切割
Vue.component("async-webpack-example", function (resolve) {
require(["./my-async-component"], resolve)
})
// 使用webpack的import()來進行異步代碼塊切割
Vue.component(
"async-webpack-example", () => import("./my-async-component")
)
從Vue 2.3.0版本開始,可以通過下面的方式來定義一個異步組件。
const AsyncWebpackExample = () => ({
component: import("./MyComp.vue"), // 需要加載的組件
loading: LoadingComp, // loading時渲染的組件
error: ErrorComp, // 出錯時渲染的組件
delay: 200, // 渲染loading組件前的等待時間(默認:200ms)
timeout: 3000 // 最長等待時間,超出則渲染error組件(默認:Infinity)
})
在路由組件上使用這種寫法,需要使用vue-router的2.4.0以上版本。
組件的循環引用
循環引用,即兩個組件互相引用對方,例如下面代碼中tree-folder
、tree-folder-contents
兩個組件同時成為了對方的父或子節點,如果使用Webpack模塊化管理工具requiring
/importing
組件的時候,會報出Failed to mount component: template or render function not defined.
錯誤。
<template>
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children"/>
</p>
</template>
<template>
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>
</template>
因為tree-folder
、tree-folder-contents
相互引用對方之后,無法確定組件加載的先后順序陷入死循環,所以需要事先指明webpack組件加載的優先級。解決上面例子中Vue組件循環引用的問題,可以在tree-folder
組件的beforeCreate()
生命周期函數內注冊引發問題的tree-folder-contents
組件。
beforeCreate: function () {
this.$options.components.TreeFolderContents
= require("./tree-folder-contents.vue").default
}
組件命名約定
JavaScript中命名組件組件時可以使用kebab-case
、camelCase
、PascalCase
,但HTML模板中只能使用kebab-case
格式。
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<pascal-cased-component></pascal-cased-component>
<!-- 也可以通過自關閉方式使用組件 -->
<kebab-cased-component />
<script>
components: {
"kebab-cased-component": {},
"camelCasedComponent": {},
"PascalCasedComponent": {}
}
</script>
推薦JavaScript中通過
PascalCase
方式聲明組件, HTML中則通過kebab-case
方式使用組件。
組件遞歸
當局部注冊的Vue組件遞歸調用自身時,需要在創建組件時添加name
選項,全局注冊的組件則可以省略該屬性,因為Vue會自動進行添加。
// 局部注冊
new Vue({
el: "#my-component",
name: "my-component",
template: "<div><my-component></my-component></div>"
})
// 全局注冊
Vue.component("my-component", {
// name: "my-component", 可以省略name屬性
template: "<div><my-component></my-component></div>"
})
組件遞歸出現死循環時,會提示
max stack size exceeded
錯誤,所以需要確保遞歸操作都擁有一個終止條件(比如使用v-if并返回false)。
組件模板
- 可以在Vue組件上使用
inline-template
屬性,組件會將內嵌的HTML內容作為組件本身的模板進行渲染,而非將其作為slot
分發的內容。
<my-component inline-template>
<div>
<p>These are compiled as the component"s own template.</p>
<p>Not parent"s transclusion content.</p>
</div>
</my-component>
- 也可以通過在
<script>
標簽內使用type="text/x-template"
和id
屬性來定義一個內嵌模板。
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
<script>
Vue.component("hello-world", {
template: "#hello-world-template"
})
</script>
混合屬性mixins
用來將指定的mixin對象復用到Vue組件當中。
// mixin對象
var mixin = {
created: function () {
console.log("混合對象的鉤子被調用")
},
methods: {
foo: function () {
console.log("foo")
},
conflicting: function () {
console.log("from mixin")
}
}
}
// vue屬性
var vm = new Vue({
mixins: [mixin],
created: function () {
console.log("組件鉤子被調用")
},
methods: {
bar: function () {
console.log("bar")
},
conflicting: function () {
console.log("from self")
}
}
})
// => "混合對象的鉤子被調用"
// => "組件鉤子被調用"
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"
同名組件option對象的屬性會被合并為數組依次進行調用,其中mixin對象里的屬性會被首先調用。如果組件option對象的屬性值是一個對象,則mixin中的屬性會被忽略掉。
渲染函數render()
用來創建VNode,該函數接收createElement()
方法作為第1個參數,該方法調用后會返回一個虛擬DOM(即VNode)。
直接使用表達式,或者在render()
函數內通過createElement()
進行手動渲染,Vue都會自動保持blogTitle
屬性的響應式更新。
<h1>{{ blogTitle }}</h1>
<script>
render: function (createElement) {
return createElement("h1", this.blogTitle)
}
</script>
如果組件是一個函數組件,render()還會接收一個context參數,以便為沒有實例的函數組件提供上下文信息。
通過render()函數實現虛擬DOM比較麻煩,因此可以使用Babel插件babel-plugin-transform-vue-jsx
在render()函數中應用JSX語法。
import AnchoredHeading from "./AnchoredHeading.vue"
new Vue({
el: "#demo",
render (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
Vue對象全局API
Vue.extend(options) // 通過繼承一個option對象來創建一個Vue實例。
Vue.nextTick([callback, context]) // 在下次DOM更新循環結束之后執行延遲回調。
Vue.set(target, key, value) // 設置對象的屬性,如果是響應式對象,將會觸發視圖更新。
Vue.delete(target, key) // 刪除對象的屬性,如果是響應式對象,將會觸發視圖更新。
Vue.directive(id, [definition]) // 注冊或獲取全局指令。
Vue.filter(id, [definition]) // 注冊或獲取全局過濾器。
Vue.component(id, [definition]) // 注冊或獲取全局組件。
Vue.use(plugin) // 安裝Vue插件。
Vue.mixin(mixin) // 全局注冊一個mixin對象。
Vue.compile(template) // 在render函數中編譯模板字符串。
Vue.version // 提供當前使用Vue的版本號。
Vue.mixin(mixin)
使用全局mixins將會影響到所有之后創建的Vue實例。
// 為自定義選項myOption注入一個處理器。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: "hello!"
})
// => "hello!"
Vue.directive(id, [definition])
Vue允許注冊自定義指令,用于對底層DOM進行操作。
Vue.directive("focus", {
bind: function() {
// 指令第一次綁定到元素時調用,只會調用一次,可以用來執行一些初始化操作。
},
inserted: function (el) {
// 被綁定元素插入父節點時調用。
},
update: function() {
// 所在組件的VNode更新時調用,但是可能發生在其子VNode更新之前。
},
componentUpdated: function() {
// 所在組件VNode及其子VNode全部更新時調用。
},
unbind: function() {
// 指令與元素解綁時調用,只會被調用一次。
}
})
鉤子之間共享數據可以通過
HTMLElement
的dataset
屬性來進行(即HTML標簽上通過data-
格式定義的屬性)。
上面的鉤子函數擁有如下參數:
- el: 指令綁定的HTML元素,可以用來直接操作DOM。
- vnode: Vue編譯生成的虛擬節點。
- oldVnode: 之前的虛擬節點,僅在
update
、componentUpdated
鉤子中可用。 - binding: 一個對象,包含以下屬性:
- name: 指令名稱,不包括
v-
前綴。 - value: 指令的綁定值,例如
v-my-directive="1 + 1"
中value
的值是2
。 - oldValue: 指令綁定的之前一個值,僅在
update
、componentUpdated
鉤子中可用。 - expression: 綁定值的字符串形式,例如
v-my-directive="1 + 1"
當中expression
的值為"1 + 1"
。 - arg: 傳給指令的參數,例如
v-my-directive:foo
中arg
的值是"foo"
。 - modifiers: 包含修飾符的對象,例如
v-my-directive.foo.bar
的modifiers
的值是{foo: true, bar: true}
。
- name: 指令名稱,不包括
上面參數除
el
之外,其它參數都應該是只讀的,盡量不要對其進行修改操作。
Vue.filter(id, [definition])
Vue可以通過定義過濾器,進行一些常見的文本格式化,可以用于mustache插值和v-bind表達式當中,使用時通過管道符|
添加在表達式尾部。
<!-- in mustaches -->
{{ message | capitalize }}
<!-- in v-bind -->
<div v-bind:id="rawId | formatId"></div>
<!-- capitalize filter -->
<script>
new Vue({
filters: {
capitalize: function (value) {
if (!value) return ""
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
})
</script>
過濾器可以串聯使用,也可以傳入參數。
<span>{{ message | filterA | filterB }}</span>
<span>{{ message | filterA("arg1", arg2) }}</span>
Vue.use(plugin)
Vue通過插件來添加一些全局功能,Vue插件都會覆寫其install()
方法,該方法第1個參數是Vue構造器
, 第2個參數是可選的option對象
:
MyPlugin.install = function (Vue, options) {
// 1\. 添加全局方法或屬性
Vue.myGlobalMethod = function () {}
// 2\. 添加全局資源
Vue.directive("my-directive", {
bind (el, binding, vnode, oldVnode) {}
})
// 3\. 注入組件
Vue.mixin({
created: function () {}
})
// 4\. 添加實例方法
Vue.prototype.$myMethod = function (methodOptions) {}
}
通過全局方法Vue.use()
使用指定插件,使用的時候也可以傳入一個option對象。
Vue.use(MyPlugin, {someOption: true})
vue-router等插件檢測到Vue是全局對象時會自動調用
Vue.use()
,如果在CommonJS模塊環境中,則需要顯式調用Vue.use()
。
實例屬性和方法
Vue實例暴露了一系列帶有前綴$的實例屬性與方法。
let vm = new Vue();
vm = {
// Vue實例屬性的代理
$data: "被watch的data對象",
$props: "當前組件收到的props",
$el: "Vue實例使用的根DOM元素",
$options: "當前Vue實例的初始化選項",
$parent: "父組件Vue對象的實例",
$root: "根組件Vue對象的實例",
$children: "當前實例的直接子組件",
$slots: "訪問被slot分發的內容",
$scopedSlots: "訪問scoped slots",
$refs: "包含所有擁有ref注冊的子組件",
$isServer: "判斷Vue實例是否運行于服務器",
$attrs: "包含父作用域中非props的屬性綁定",
$listeners: "包含了父作用域中的v-on事件監聽器",
// 數據
$watch: "觀察Vue實例變化的表達式、計算屬性函數",
$set: "全局Vue.set的別名",
$delete: "全局Vue.delete的別名",
// 事件
$on: "監聽當前實例上的自定義事件,事件可以由vm.$emit觸發",
$once: "監聽一個自定義事件,觸發一次之后就移除監聽器",
$off: "移除自定義事件監聽器",
$emit: "觸發當前實例上的事件",
// 生命周期
$mount: "手動地掛載一個沒有掛載的Vue實例",
$forceUpdate: "強制Vue實例重新渲染,僅影響實例本身和插入插槽內容的子組件",
$nextTick: "將回調延遲到下次DOM更新循環之后執行",
$destroy: "完全銷毀一個實例",
}
$refs屬性
子組件指定ref
屬性之后,可以通過父組件的$refs
實例屬性對其進行訪問 。
<div id="parent">
<user-profile ref="profile"></user-profile>
</div>
<script>
var parent = new Vue({ el: "#parent" })
var child = parent.$refs.profile // 訪問子組件
</script>
$refs
會在組件渲染完畢后填充,是非響應式的,僅作為需要直接訪問子組件的應急方案,因此要避免在模板或計算屬性中使用$refs
。