[TOC]
- Vue 學(xué)習(xí)筆記
- Vue 源碼解析 - 主線流程
- Vue 源碼解析 - 模板編譯
- Vue 源碼解析 - 組件掛載
- Vue 源碼解析 - 數(shù)據(jù)驅(qū)動(dòng)與響應(yīng)式原理
簡(jiǎn)介
Vue 是一個(gè)用于構(gòu)建 Web 用戶界面的漸進(jìn)式 JavaScript 框架。其核心庫只關(guān)注視圖層(view layer),同時(shí)具備良好的第三方支持庫生態(tài)用以應(yīng)付構(gòu)建復(fù)雜大型單頁應(yīng)用(SPA:Single-Page Application)。
MVVM 模型
Vue 雖然沒有完全遵循 MVVM(Model-View-ViewModel)模型,當(dāng)其設(shè)計(jì)也受到了 MVVM 的啟發(fā),以數(shù)據(jù)驅(qū)動(dòng)界面,如下圖所示:
在 Vue 中,充當(dāng) ViewModel 的是一個(gè) Vue 實(shí)例(new Vue({})
),該 Vue實(shí)例 作用于某一個(gè) HTML 元素上,全權(quán)代理該元素節(jié)點(diǎn)的所有操作。
Vue實(shí)例 內(nèi)部通過 DOM Listeners 可以觀測(cè)到頁面上 DOM 元素的變化,從而將該種變化同步更改到 Model 中的對(duì)應(yīng)數(shù)據(jù)。
同時(shí)通過 Data Bindings,當(dāng) Model 中的數(shù)據(jù)改變時(shí),則會(huì)對(duì)相應(yīng)視圖上的顯示進(jìn)行更改,從而實(shí)現(xiàn)了 View 和 Model 的數(shù)據(jù)雙向綁定。
注:傳統(tǒng)的 Web 編程模型是 結(jié)構(gòu)驅(qū)動(dòng),即要對(duì)一個(gè) DOM 節(jié)點(diǎn)進(jìn)行操作,第一步就是要獲取該 DOM 節(jié)點(diǎn)對(duì)象,然后再修改數(shù)據(jù)更新到節(jié)點(diǎn)上。
而 Vue 的中心思想是 數(shù)據(jù)驅(qū)動(dòng),要更改界面,其實(shí)就是要更改數(shù)據(jù)。
簡(jiǎn)而言之,在 Vue 中,不應(yīng)當(dāng)考慮操作 DOM,而是專注于 操作數(shù)據(jù)。
安裝
Vue 的安裝有多種方法,這里主要介紹兩種方法:
- 通過
<script>
標(biāo)簽直接引入:
<!-- 對(duì)于制作原型或?qū)W習(xí),你可以這樣使用最新版本 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 或者-->
<!-- 對(duì)于生產(chǎn)環(huán)境,我們推薦鏈接到一個(gè)明確的版本號(hào)和構(gòu)建文件,以避免新版本造成的不可預(yù)期的破壞:-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
注:通過<script>
標(biāo)簽引入,Vue
會(huì)被注冊(cè)為一個(gè)全局變量
- 直接使用官方提供的快速搭建復(fù)雜單頁面應(yīng)用 (SPA) 的腳手架 vue-cli:
- 首先全局安裝該腳手架 vue-cli:
npm install -g @vue/cli
vue create <project-name>
注:使用 vue-cli 前需確保系統(tǒng)已安裝 nodejs。
以上兩步操作完成,我們便創(chuàng)建完成一個(gè) Vue 項(xiàng)目。
在項(xiàng)目的 package.json
中,可以看到 vue-cli 提供了兩個(gè)腳本命令讓我們運(yùn)行與打包項(xiàng)目:
-
npm run serve
:運(yùn)行項(xiàng)目 -
npm run build
:打包項(xiàng)目到 dist 文件夾
組件化
Vue 的兩大特性為 數(shù)據(jù)驅(qū)動(dòng) 和 組件化。
通常一個(gè)大的頁面可以劃分為許多個(gè)小區(qū)塊,這些小區(qū)塊有些結(jié)構(gòu)是相似的,我們可以將這些相似的區(qū)塊抽象出一個(gè)統(tǒng)一的結(jié)構(gòu),方便復(fù)用,這種抽象結(jié)構(gòu)的方法即稱為組件化。
在實(shí)際項(xiàng)目開發(fā)中,一個(gè)大的頁面通常都是由許多個(gè)小的組件構(gòu)造而成的,如下圖所示:
Vue 提供了兩種組件定義的方式:
-
全局組件:全局組件只需定義一次,便可被其他任意組件使用。
定義方式:Vue.component(id, [definition])
<body>
<div id="app">
<my-component />
</div>
<script>
const myComponent = Vue.component('my-component',{
data(){
return {
'message': 'Hello Global Component'
}
},
template:`
<h1>{{message}}</h1>
`
});
const vm = new Vue({
el: "#app",
});
</script>
</body>
-
局部組件:對(duì)于全局組件來說,即使頁面沒有使用該組件,組件也會(huì)被注入到最終的構(gòu)建結(jié)果中,導(dǎo)致了 JavaScript 文件的無謂增加。而局部組件可以做到按需加載,需要哪些組件,按需引入即可,更加靈活高效。
定義方式:通過一個(gè)普通的 JavaScript 對(duì)象來定義組件,然后 Vue實(shí)例 按需引入需要的組件即可。
<body>
<div id="app">
<component-a></component-a>
<component-b />
</div>
<script>
const componentA = {
template: `
<h1>Component A</h1>
`
};
const componentB = {
template: `
<h1>Component B</h1>
`
};
const vm = new Vue({
el: '#app',
components: { // 按需引入需要的組件
componentA,
componentB
}
});
</script>
</body>
最佳實(shí)踐:在 Vue 中,組件通常都定義到一個(gè)單獨(dú)的.vue
文件中,其他組件需要時(shí),導(dǎo)入相應(yīng)組件的.vue
文件即可。
// MyComponent.vue
<template>
<h1>{{message}}</h1>
</template>
<script>
export default {
name: "MyCompoent",
data() {
return {
message: "Hello MyComponent!"
};
}
};
</script>
<style scoped>
h1 {
background: red;
}
</style>
可以直接使用以下命令直接運(yùn)行.vue
文件,查看組件展示效果:
vue serve MyComponent.vue --open
也可以在其他組件內(nèi)導(dǎo)入該組件,進(jìn)行使用:
<template>
<div id="app">
<h1>Parent Component</h1>
<!-- 使用組件 -->
<my-component />
</div>
</template>
<script>
// 導(dǎo)入組件
import MyComponent from "./MyComponent.vue";
export default {
name: "app",
components: {
MyComponent // 引入組件
}
};
</script>
注:在 Vue 中,組件實(shí)質(zhì)是帶有一個(gè)名字的 Vue實(shí)例,其性質(zhì)與 Vue實(shí)例 基本一致(遵循 Vue實(shí)例 的生命周期等內(nèi)容),特點(diǎn)是多了個(gè)組件復(fù)用功能。
Vue實(shí)例
Vue 實(shí)例充當(dāng) ViewModel 角色,負(fù)責(zé) View 和 Model 之間的數(shù)據(jù)綁定:
new Vue(Options)
當(dāng)創(chuàng)建一個(gè) Vue 實(shí)例時(shí),你可以傳入一個(gè)選項(xiàng)對(duì)象Options
,該Options
的選項(xiàng)列表有如下可選:
- 選項(xiàng) / 數(shù)據(jù)
- 選項(xiàng) / DOM
- 選項(xiàng) / 生命周期鉤子
- 選項(xiàng) / 資源
- 選項(xiàng) / 組合
- 選項(xiàng) / 其他
下面列舉一些Options
常用選項(xiàng)
-
選項(xiàng) / 數(shù)據(jù):
data
描述:Vue 實(shí)例的數(shù)據(jù)對(duì)象,用于數(shù)據(jù)的存儲(chǔ)與顯示。
類型:Object | Function
<body>
<div id="app">
<h1>{{message}}</h1>
</div>
<script>
const vm = new Vue({
el: "#app",
data: {
message: "Hello Vue!"
}
});
</script>
</body>
注:Vue 將會(huì)遞歸將data
的屬性轉(zhuǎn)換為getter/setter
,從而讓data
的屬性能夠響應(yīng)數(shù)據(jù)變化。比如在控制臺(tái)輸入vm.message = 'Hi Vue!!!
,可以觀察到頁面數(shù)據(jù)發(fā)生了更改。
注:組件 中的data
屬性必須是Fcuntion
類型,其返回一個(gè)Object
,原因是組件復(fù)用時(shí),保證每個(gè)新組件都有獨(dú)一的一份數(shù)據(jù)拷貝。
// MyComponent.vue
<template>
<h1>{{message}}</h1>
</template>
<script>
export default {
name: 'MyComponent',
data(){ // 函數(shù)類型
return { // 返回?cái)?shù)據(jù)對(duì)象
'message': 'Hello Vue!'
}
}
}
</script>
-
選項(xiàng) / 數(shù)據(jù):
props
描述:該屬性用于接收來自父組件的數(shù)據(jù)。
類型:Array<string> | Object
。
當(dāng)傳遞的是Object
類型時(shí),則可以基于對(duì)象的語法使用以下選項(xiàng):
?type
:指定數(shù)據(jù)類型,該值可以為原生類型(String
,Number
,Boolean
,Array
,Object
,Date
,Function
,Symbol
),自定義構(gòu)造函數(shù),或上述內(nèi)容組成的數(shù)組。
?default:any
:為該prop
指定一個(gè)默認(rèn)值。如果該prop
沒有被傳入,則使用該默認(rèn)值。對(duì)象或數(shù)組的默認(rèn)值必須從一個(gè)工廠函數(shù)返回。
?required: Boolean
:定義該prop
是否為必填項(xiàng)。
?validator: Function
:自定義驗(yàn)證函數(shù),對(duì)該prop
進(jìn)行校驗(yàn)。
Vue.component('my-component', {
props: {
// 基礎(chǔ)的類型檢查 (`null` 和 `undefined` 會(huì)通過任何類型驗(yàn)證)
propA: Number,
// 多個(gè)可能的類型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 帶有默認(rèn)值的數(shù)字
propD: {
type: Number,
default: 100
},
// 帶有默認(rèn)值的對(duì)象
propE: {
type: Object,
// 對(duì)象或數(shù)組默認(rèn)值必須從一個(gè)工廠函數(shù)獲取
default: function () {
return { message: 'hello' }
}
},
// 自定義驗(yàn)證函數(shù)
propF: {
validator: function (value) {
// 這個(gè)值必須匹配下列字符串中的一個(gè)
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})
-
選項(xiàng) / 數(shù)據(jù):
propsData
描述:創(chuàng)建實(shí)例時(shí)傳遞props
。主要作用是方便測(cè)試。
類型:{ [key: string]: any }
var Comp = Vue.extend({
props: ['msg'],
template: '<div>{{ msg }}</div>'
})
var vm = new Comp({
propsData: {
msg: 'hello'
}
})
注:propsData
屬性只能用于new
創(chuàng)建的實(shí)例中。
-
選項(xiàng) / 數(shù)據(jù):
methods
:
描述:方法定義,Vue實(shí)例 可以直接訪問這些方法,或在指令表達(dá)式中直接調(diào)用這些方法。
類型:{ [key: string]: Function }
var vm = new Vue({
data: { a: 1 },
methods: {
plus: function () {
this.a++
}
}
})
vm.plus()
vm.a // 2
注:methods
中的this
自動(dòng)綁定到當(dāng)前 Vue實(shí)例。
-
選項(xiàng) / 數(shù)據(jù):
computed
描述:計(jì)算屬性,主要用于對(duì)data
數(shù)據(jù)進(jìn)行計(jì)算轉(zhuǎn)換。
類型:{ [key: string]: Function | { get: Function, set: Function } }
<template>
<div>
<h1>獲取數(shù)據(jù): {{computedData}}</h1>
<h1>設(shè)置數(shù)據(jù):{{setData = 3}}</h1>
<h1>獲取數(shù)據(jù): {{setData}}</h1>
</div>
</template>
<script>
export default {
name: "Computed",
data() {
return {
count: 1
};
},
computed: {
// 只讀取 data
computedData() {
return this.count + 10;
},
// 讀取和設(shè)置 data
setData: {
get() {
return this.count + 1;
},
set(value) {
this.count = value;
}
}
}
};
</script>
注:computed
類型為Object
,其具有如下特點(diǎn):
-
computed
內(nèi)部定義的屬性為訪問器屬性,即具備getter
和setter
,且其內(nèi)部this
自動(dòng)綁定到當(dāng)前 Vue實(shí)例。 -
computed
會(huì)自動(dòng) 緩存 計(jì)算結(jié)果,只有當(dāng)依賴的響應(yīng)式屬性變化時(shí),computed
才會(huì)重新進(jìn)行計(jì)算。
緩存 是computed
與methods
的最大區(qū)別之處,methods
每次調(diào)用一定會(huì)運(yùn)行函數(shù),而computed
則不一定。
-
選項(xiàng) / 數(shù)據(jù):
watch
:
描述:偵聽屬性,用于監(jiān)控data
或computed
的數(shù)據(jù),當(dāng)數(shù)據(jù)變更時(shí)進(jìn)行回調(diào)通知。
類型:{ [key: string]: string | Function | Object | Array }
注:watch
類型為Object
,其內(nèi)部屬性的類型有多種:string | Function | Object | Array
,這里簡(jiǎn)單介紹 3 種:
-
string
:字符串表示回調(diào)函數(shù)名,當(dāng)數(shù)據(jù)改變時(shí),回調(diào)該函數(shù):
const vm = new Vue({
el: '#app',
data: {
a: 1
},
methods: {
aChanged(value, oldValue) {
console.log(`a changed: new:${value} --> old:${oldValue}`);
}
},
watch: {
a: 'aChanged'
}
});
-
Function
:當(dāng)數(shù)據(jù)改變時(shí),直接回調(diào)該函數(shù):
const vm = new Vue({
el: '#app',
data: {
a: 1
},
watch: {
a(value, oldValue) {
console.log(`a changed: new:${value} --> old:${oldValue}`);
}
}
});
-
Object
:對(duì)監(jiān)控的屬性為對(duì)象時(shí),Vue 默認(rèn)只能監(jiān)控到對(duì)象重新被賦值的變化,而如果需要監(jiān)聽對(duì)象內(nèi)部屬性的變化,則可使用該選項(xiàng),其中:
handler
代表回調(diào)函數(shù)。
deep
用來控制監(jiān)聽對(duì)象屬性的層級(jí),deep=true
時(shí)只要對(duì)象內(nèi)部 property 改變(不管嵌套有多深),都會(huì)監(jiān)聽到。
immediate
用來設(shè)置是否立即產(chǎn)生回調(diào)。當(dāng)immediate=true
時(shí),回調(diào)函數(shù)會(huì)立即被調(diào)用,傳遞的是屬性當(dāng)前的值。
const vm = new Vue({
el: '#app',
data: {
a: {
aa: {
aaa: 3
}
}
},
watch: {
a: {
handler(value, oldValue) {
console.log(`a changed: new:${value} --> old:${oldValue}`);
},
deep: true // 被監(jiān)聽對(duì)象的 property 改變時(shí)被調(diào)用,無論嵌套的有多深
}
}
});
注:大多數(shù)情況下,觀察和響應(yīng)數(shù)據(jù)變更使用計(jì)算屬性(computed
)便足夠了,但是當(dāng)在數(shù)據(jù)變化時(shí)需要執(zhí)行異步或開銷較大的操作時(shí),則此時(shí)使用偵聽屬性(watch
)會(huì)更加適合。
-
選項(xiàng) / DOM:
el
描述:設(shè)置 Vue實(shí)例 的掛載目標(biāo)節(jié)點(diǎn)
類型:string | Element
new Vue({
el: '#app'
});
注:如果在實(shí)例化時(shí)存在這個(gè)選項(xiàng),實(shí)例將立即進(jìn)入編譯過程,否則,需要顯式調(diào)用vm.$mount()
手動(dòng)開啟編譯。
-
選項(xiàng) / DOM:
template
描述:字符串模板,模板會(huì) 替換 掛載的元素。
類型:string
<div id="app"></div>
const vm = new Vue({
el: '#app',
template: '<h1>template</h1>' // <div> 會(huì)被 <h1> 完全覆蓋
})
-
選項(xiàng) / DOM:
render
描述:字符串模板的代替方案,允許你發(fā)揮 JavaScript 最大的編程能力。該渲染函數(shù)接收一個(gè)createElement
方法作為第一個(gè)參數(shù)用來創(chuàng)建VNode
。
類型:(createElement: () => VNode) => VNode
<div id="app"></div>
new Vue({
render(createElement) {
return createElement('div', {
class: 'rendered'
},
[
createElement('h1', {
domProps: {
innerHTML: 'div>h1 rendered by vue'
}
})
]
);
}}).$mount('#app');
注:Vue 推薦在絕大多數(shù)情況下使用模板來創(chuàng)建你的 HTML,只有在一些特殊場(chǎng)景下,比如模板冗長(zhǎng)且具備重復(fù)元素,則此時(shí)使用渲染函數(shù)render
通過編寫 JavaScript 代碼來渲染出頁面會(huì)更加方便簡(jiǎn)潔。
生命周期
每個(gè) Vue實(shí)例 在掛載到頁面時(shí),都會(huì)經(jīng)歷一系列的初始化過程,例如,需要設(shè)置數(shù)據(jù)監(jiān)聽、編譯模板、將實(shí)例掛載到 DOM 并在數(shù)據(jù)變化時(shí)更新 DOM 等。在創(chuàng)建 Vue實(shí)例 的這整個(gè)過程中,Vue 為我們預(yù)留出了一些 Hook 點(diǎn),方便我們?cè)?Vue實(shí)例 創(chuàng)建過程的某個(gè)生命周期中進(jìn)行一些操作。如下圖所示:
注:圖片來源于網(wǎng)上,侵刪。
這些預(yù)留的生命周期鉤子函數(shù)總共有如下幾個(gè):
beforeCreate
:Vue實(shí)例 初始化之后,此時(shí)data
和methods
中的數(shù)據(jù)還未進(jìn)行初始化,因此無法獲取。created
:表示 Vue實(shí)例 創(chuàng)建完成,但還未掛載到頁面上,此時(shí)data
和methods
都已經(jīng)初始化成功,可以對(duì)其進(jìn)行調(diào)用獲取,而掛載階段未開始,所以$el
屬性目前不可見。beforeMount
:在掛載開始之前被調(diào)用,此時(shí)模板已在內(nèi)存中被編譯完成,只是尚未掛載到頁面上,因此,此時(shí)頁面上顯示的還是未渲染的結(jié)構(gòu)。mounted
:掛載完成,此時(shí)頁面會(huì)顯示我們渲染的視圖。如果想要操作頁面上的 DOM 節(jié)點(diǎn),最早的時(shí)間就是該處。
注:mounted
不會(huì) 承諾所有的子組件也都一起被掛載。如果你希望等到整個(gè)視圖都渲染完畢,可以用vm.$nextTick
替換掉mounted
:
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
beforeUpdate
:數(shù)據(jù)更新時(shí)調(diào)用,此時(shí)內(nèi)存中data
數(shù)據(jù)已更新,但頁面中顯示的數(shù)據(jù)還未更新,數(shù)據(jù)與頁面不同步。這里適合在更新之前訪問現(xiàn)有的 DOM,比如手動(dòng)移除已添加的事件監(jiān)聽器。updated
:新數(shù)據(jù)成功渲染到頁面,此時(shí)數(shù)據(jù)與頁面處于同步狀態(tài)。
注:updated
不會(huì) 承諾所有的子組件也都一起被掛載。如果你希望等到整個(gè)視圖都渲染完畢,可以用vm.$nextTick
替換掉mounted
:activated
:激活狀態(tài),表示當(dāng)前組件處于前臺(tái)頁面,用戶可與該組件進(jìn)行交互。
注:只有組件被內(nèi)置組件keep-alive
包裹時(shí),該鉤子才有可能被調(diào)用。
<template>
<keep-alive>
<my-component />
</keep-alive>
</template>
deactivated
:停用狀態(tài),表示當(dāng)前組件處于后臺(tái)頁面,用戶不能與之交互。
注:只有組件被內(nèi)置組件keep-alive
包裹時(shí),該鉤子才有可能被調(diào)用。beforeDestroy
:實(shí)例銷毀之前調(diào)用。在這一步,實(shí)例仍然完全可用,即此時(shí)實(shí)例的data
,methods
等所有數(shù)據(jù)完全可用。destroyed
:Vue 實(shí)例銷毀后調(diào)用。此時(shí) Vue實(shí)例 指向的所有東西都會(huì)解綁定,所有的事件監(jiān)聽器會(huì)被移除,所有的子實(shí)例也會(huì)被銷毀。errorCaptured
:當(dāng)捕獲一個(gè)來自子孫組件的錯(cuò)誤時(shí)被調(diào)用。此鉤子會(huì)收到三個(gè)參數(shù):錯(cuò)誤對(duì)象、發(fā)生錯(cuò)誤的組件實(shí)例以及一個(gè)包含錯(cuò)誤來源信息的字符串。此鉤子可以返回false
以阻止該錯(cuò)誤繼續(xù)向上傳播。
指令
指令 (Directives) 是帶有v-
前綴的特殊屬性。
Vue 提供了以下內(nèi)置的指令:
-
v-text
:更新元素的textContent
,該指令與使用{{ Mustache }}
插值效果一樣。
類型:string
<span v-text="msg"></span>
<!-- 和下面的一樣 -->
<span>{{msg}}</span>
-
v-html
:更新元素的innerHTML
。
類型:string
<template>
<div id="app">
<div v-html="html"></div>
</div>
</template>
<script>
export default {
name: "app",
data() {
return {
html: '<a >baidu</a>'
};
},
};
</script>
注:v-html
的內(nèi)容只會(huì)按普通 HTML 插入,不會(huì)作為 Vue 模板進(jìn)行編譯。
注:在單文件組件里,scoped
的樣式不會(huì)應(yīng)用在 v-html
內(nèi)部,因?yàn)槟遣糠?HTML 沒有被 Vue 的模板編譯器處理。
注:在網(wǎng)站上動(dòng)態(tài)渲染任意 HTML 是非常危險(xiǎn)的,因?yàn)槿菀讓?dǎo)致 XSS 攻擊。只在可信內(nèi)容上使用v-html
,永不用在用戶提交的內(nèi)容上。
-
v-show
:條件渲染,根據(jù)表達(dá)式之真假值,切換元素的display
CSS 屬性。
類型:any
<h1 v-show="ok">Hello!</h1>
注:v-show
不支持<template>
元素,也不支持v-else
。
-
v-if
:條件渲染,根據(jù)表達(dá)式的值的真假條件渲染元素。在切換時(shí)元素及它的數(shù)據(jù)綁定 / 組件被銷毀并重建。如果元素是<template>
,將提出它的內(nèi)容作為條件塊。
類型:any
<h1 v-if="awesome">Vue is awesome!</h1>
注:當(dāng)和v-if
一起使用時(shí),v-for
的優(yōu)先級(jí)比v-if
更高。
注:v-show = false
時(shí)只是把元素設(shè)置為:display:none
,元素還留著 DOM 樹上。
而v-if = false
時(shí),元素會(huì)被整個(gè)移除,其上綁定的數(shù)據(jù)/組件都會(huì)被銷毀。
-
v-else
:v-if
或v-if-else
的分支。
類型:無
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>
-
v-else-if
:v-if
的分支。
類型:any
<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>
-
v-for
:遍歷源數(shù)據(jù),渲染元素列表。
類型:Array | Object | number | string | Iterable
<div v-for="item in items">
{{ item.text }}
</div>
<!-- 另外也可以為數(shù)組索引指定別名 (或者用于對(duì)象的鍵):-->
<div v-for="(item, index) in items"></div>
<div v-for="(val, key) in object"></div>
<div v-for="(val, name, index) in object"></div>
注:v-for
渲染元素時(shí),默認(rèn)使用“就地更新”策略,即當(dāng)列表數(shù)據(jù)改變時(shí),Vue 不會(huì)移動(dòng)當(dāng)前 DOM 元素來重新匹配數(shù)據(jù)項(xiàng),而是根據(jù)索引位置重新渲染數(shù)據(jù)。比如:
現(xiàn)在我們有數(shù)據(jù)項(xiàng):
data() {
return {
datas: [
{ id: 1, name: "one" },
{ id: 2, name: "two" },
{ id: 3, name: "three" }
]
}
將這些數(shù)據(jù)項(xiàng)渲染到頁面上:
<template>
<div>
<ul>
<li v-for="(item,index) in datas">
<input type="checkbox" />
{{item.name}}
</li>
</ul>
</template>
我們使用v-for
將每條數(shù)據(jù)項(xiàng)渲染到一個(gè)<li>
上,此時(shí)顯示效果如下:
如果此時(shí)我們勾選第一個(gè)<li>
的checkbox
,即one
勾選上,然后再往數(shù)據(jù)列表前面添加一個(gè)數(shù)據(jù):this.datas.unshift({ id: 4, name: "four" })
,則可以看到顯示效果如下:
可以看到,我們想要的是one
被勾選了,但是效果是數(shù)據(jù)列表首位被勾選。出現(xiàn)這種現(xiàn)象的原因就是v-for
默認(rèn)采用的“就地更新”策略:它會(huì)復(fù)用已渲染完成的 DOM 元素,然后只對(duì)變化的數(shù)據(jù)進(jìn)行修改,比如這里復(fù)用了第一條<li><checkbox>one</li>
,添加數(shù)據(jù)項(xiàng),對(duì)第一條<li>
來說,他的數(shù)據(jù)改變了,但是<checkbox>
不包含在數(shù)據(jù)項(xiàng)里,因此只會(huì)修改數(shù)據(jù),將one
修改為four
,而checkbox
仍保持勾選狀態(tài)。
因此,“就地更新”策略是高效的,但是 只適用于不依賴子組件狀態(tài)或臨時(shí) DOM 狀態(tài)(例如:表單輸入值) 的列表渲染輸出。
而要解決上述問題,只需為v-for
提供一個(gè)key
屬性(key
必須是唯一的),這樣 Vue 就可以識(shí)別出數(shù)據(jù)項(xiàng)對(duì)應(yīng)的渲染條目,從而重用和重新排序現(xiàn)有元素:
<template>
<div>
<ul>
<li v-for="(item,index) in datas" :key="item.id">
<input type="checkbox" />
{{item.name}}
</li>
</ul>
</template>
由于新添加的數(shù)據(jù)id=4
,當(dāng)前已存在的<li>
沒有與之對(duì)應(yīng)的標(biāo)識(shí)key
,因此 Vue 會(huì)重新渲染一個(gè)新的<li>
,并將其與id=4
對(duì)應(yīng)起來,結(jié)果如下圖所示:
注:“就地更新”策略其實(shí)就是使用索引作為節(jié)點(diǎn)標(biāo)識(shí),即:key=index
。
-
v-on
:綁定事件監(jiān)聽器。
縮寫:@
類型:Function | Inline Statement | Object
參數(shù):event
修飾符:
?.stop
:調(diào)用event.stopPropagation()
,停止事件分發(fā)。
?.prevent
:調(diào)用event.preventDefault()
,取消事件的默認(rèn)動(dòng)作。
?.capture
:添加事件偵聽器時(shí)使用capture
(捕獲)模式。
?.self
:只有當(dāng)事件是從偵聽器綁定的元素本身觸發(fā)時(shí)才觸發(fā)回調(diào)。
?.{keyCode | keyAlias}
:只有當(dāng)事件是從特定鍵觸發(fā)時(shí)才觸發(fā)回調(diào)。
?.native
:監(jiān)聽組件根元素的原生事件。
?.once
:只觸發(fā)一次回調(diào)。
?.left
: 只有當(dāng)點(diǎn)擊鼠標(biāo)左鍵時(shí)觸發(fā)。
?.right
: 只有當(dāng)點(diǎn)擊鼠標(biāo)右鍵時(shí)觸發(fā)。
?.middle
: 只有當(dāng)點(diǎn)擊鼠標(biāo)中鍵時(shí)觸發(fā)。
?.passive
:以{ passive: true }
模式添加偵聽器
<!-- 方法處理器 -->
<button v-on:click="doThis"></button>
<!-- 動(dòng)態(tài)事件 (2.6.0+) -->
<button v-on:[event]="doThis"></button>
<!-- 內(nèi)聯(lián)語句 -->
<button v-on:click="doThat('hello', $event)"></button>
<!-- 縮寫 -->
<button @click="doThis"></button>
<!-- 動(dòng)態(tài)事件縮寫 (2.6.0+) -->
<button @[event]="doThis"></button>
<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>
<!-- 阻止默認(rèn)行為 -->
<button @click.prevent="doThis"></button>
<!-- 阻止默認(rèn)行為,沒有表達(dá)式 -->
<form @submit.prevent></form>
<!-- 串聯(lián)修飾符 -->
<button @click.stop.prevent="doThis"></button>
<!-- 鍵修飾符,鍵別名 -->
<input @keyup.enter="onEnter">
<!-- 鍵修飾符,鍵代碼 -->
<input @keyup.13="onEnter">
<!-- 點(diǎn)擊回調(diào)只會(huì)觸發(fā)一次 -->
<button v-on:click.once="doThis"></button>
<!-- 對(duì)象語法 (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
注:v-on
用在普通元素上時(shí),只能監(jiān)聽 原生 DOM 事件。用在自定義組件上時(shí),也可以監(jiān)聽子組件觸發(fā)的自定義事件。
<my-component @my-event="handleThis"></my-component>
<!-- 內(nèi)聯(lián)語句 -->
<my-component @my-event="handleThis(123, $event)"></my-component>
<!-- 組件中的原生事件 -->
<my-component @click.native="onClick"></my-component>
-
v-bind
:動(dòng)態(tài)綁定
縮寫::
類型:any (with argument) | Object (without argument)
參數(shù):attrOrProp (optional)
修飾符:
?.prop
:被用于綁定 DOM 屬性 (property)
?.camel
:將 kebab-case 特性名轉(zhuǎn)換為 camelCase(駝峰式)
?.sync
:會(huì)擴(kuò)展成一個(gè)更新父組件綁定值的 v-on 偵聽器
<!-- 綁定一個(gè)屬性 -->
<img :src="imageSrc">
<!-- 動(dòng)態(tài)特性名 (2.6.0+) -->
<button :[key]="value"></button>
<!-- class 綁定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]">
<!-- style 綁定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>
<!-- 綁定一個(gè)有屬性的對(duì)象 -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>
<!-- 通過 prop 修飾符綁定 DOM 屬性 -->
<div v-bind:text-content.prop="text"></div>
<!-- prop 綁定。“prop”必須在 my-component 中聲明。-->
<my-component :prop="someThing"></my-component>
<!-- 通過 $props 將父組件的 props 一起傳給子組件 -->
<child-component v-bind="$props"></child-component>
<!-- 支持綁定駝峰命名屬性 -->
<svg :view-box.camel="viewBox"></svg>
-
v-model
:表單控件與數(shù)據(jù)屬性的雙向綁定。
修飾符:
?.lazy
:使用<input>
的change
事件進(jìn)行同步。
?.number
:自動(dòng)將字符串轉(zhuǎn)為數(shù)字。
?.trim
:輸入首尾空格過濾。
<input type="text" v-model="message" />
<script>
export default {
name: 'VModel',
data() {
return {
message: ''
};
}
};
</script>
-
v-slot
:插槽。
縮寫:#
當(dāng)我們定義組件的時(shí)候,有些內(nèi)容可能需要由父組件傳入,因此,此時(shí)可以使用插槽,預(yù)留出位置給到父組件進(jìn)行自定義內(nèi)容傳入:
// 子組件:預(yù)留插槽
<template>
<div>
<h1>Son Component</h1>
<!-- 預(yù)留插槽 -->
<slot></slot>
</div>
</template>
// 父組件:傳入插槽內(nèi)容
<template>
<div>
<h1>Parent Component</h1>
<son-component>
<h2>slot: content from Parent Component</h2>
</son-component>
</div>
</template>
? 后備內(nèi)容:可以通過為<slot>
內(nèi)部提供默認(rèn)內(nèi)容,只有當(dāng)父組件顯示傳入內(nèi)容時(shí),才會(huì)覆蓋默認(rèn)內(nèi)容:
<slot>
<h1>Default Content</h1>
</slot>
? 具名插槽:我們可以給插槽進(jìn)行命名(使用name
屬性),這樣父組件就可指定名字(使用v-slot
指令)對(duì)特定的插槽進(jìn)行覆蓋:
// 子組件模板
<template>
<div>
<h1>Son Component</h1>
<!-- 預(yù)留命名插槽 -->
<slot name="header"></slot>
<main>
<!-- name="default" -->
<slot></slot>
</main>
<slot name="footer"></slot>
</div>
</template>
// 父組件
<template>
<div>
<h1>Parent Component</h1>
<son-component>
<template v-slot:header>
<h2>替換 header 插槽</h2>
</template>
<h3>替換默認(rèn)插槽</h3>
<template #footer>
<h2>替換 footer 插槽</h2>
</template>
</son-component>
</div>
</template>
注:v-slot
只能添加在一個(gè)<template>
或 組件 上。
注:默認(rèn)插槽其實(shí)也是一個(gè)具名插槽,其名稱為:default
。
? 插槽 prop:使用 插槽 prop 可以傳遞子組件的數(shù)據(jù)給到父組件,使父組件可以在覆蓋插槽的內(nèi)容上使用子組件的數(shù)據(jù):
// 子組件
<slot :msg="message"></slot>
<script>
export default {
name: 'SonComponent',
data() {
return {
message: 'Hello from Son Component!'
};
}
};
</script>
// 父組件:slotProps 接收子組件的 插槽props
<son-component #default="slotProps">
{{slotProps.msg}}
</son-component>
-
v-pre
:跳過該元素及其子元素的編譯過程。可以用來顯示原始 Mustache 標(biāo)簽。
類型:無
<span v-pre>{{ this will not be compiled }}</span>
-
v-cloak
:這個(gè)指令保持在元素上直到關(guān)聯(lián)實(shí)例結(jié)束編譯。通常結(jié)合 CSS 規(guī)則來達(dá)到隱藏未編譯的 Mustache 標(biāo)簽直到實(shí)例準(zhǔn)備完畢。
類型:無
[v-cloak] {
display: none;
}
<div v-cloak>
{{ message }}
</div>
-
v-once
:只渲染元素和組件 一次。隨后的重新渲染,元素/組件及其所有的子節(jié)點(diǎn)將被視為靜態(tài)內(nèi)容并跳過。這可以用于優(yōu)化更新性能。
<span v-once>This will never change: {{msg}}</span>
- 自定義指令:在 Vue2.0 中,代碼復(fù)用和抽象的主要形式是組件。然而,有的情況下,你仍然需要對(duì)普通 DOM 元素進(jìn)行底層操作,這時(shí)候就會(huì)用到自定義指令。
Vue 提供了兩種自定義指令的方式:
-
全局指令:使用
Vue.directive
:
// 注冊(cè)一個(gè)全局自定義指令 `v-focus`
Vue.directive('focus', {...})
-
局部指令:組件中定義一個(gè)
directives
屬性:
// 注冊(cè)一個(gè)局部自定義指令 `v-focus`
directives: {
focus: {...}
}
-
鉤子函數(shù):一個(gè)指令定義對(duì)象可以提供如下幾個(gè)鉤子函數(shù) (均為可選):
?bind
:指令第一次綁定到元素時(shí)調(diào)用。改鉤子只會(huì)被調(diào)用一次,可在此做一些初始化設(shè)置。
?inserted
:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用 (僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中)。
?update
:所在組件的 VNode 更新時(shí)調(diào)用,但是可能發(fā)生在其子 VNode 更新之前。
?componentUpdated
:指令所在組件的 VNode 及其子 VNode 全部更新后調(diào)用。
?unbind
:指令與元素解綁時(shí)調(diào)用。改鉤子只會(huì)被調(diào)用一次,可在此做一些資源釋放操作。
示例:使用自定義指令v-customtext
模擬v-text
:
<template>
<h1 v-customtext="msg"></h1>
</template>
<script>
export default {
name: 'customeDirective',
data() {
return {
msg: 'Hello Custom Directives!'
};
},
directives: {
customtext: {
inserted(el, binding, vnode, oldVnode) {
el.innerText = binding.value;
}
}
}
};
</script>
其他
- 組件間通信:
? 父?jìng)髯?/strong>:子組件通過props
屬性可接收父組件傳遞過來的變量:
// ParentComponent.vue
<template>
<son-component :msg="message" />
</template>
<script>
import SonComponent from './SonComponent';
export default {
name: 'ParentComponent',
components: {
SonComponent
},
data() {
return {
message: 'data from Parent Component'
};
}
};
</script>
// SonComponent.vue
<template>
<h1>{{msg}}</h1>
</template>
<script>
export default {
name: 'SonComponent',
props: {
msg: {
type: String,
required: true
}
}
};
</script>
? 子傳父:子組件可以通過$emit
發(fā)送自定義事件向父組件傳值,父組件直接注冊(cè)接收該事件即可:
// SonComponent.vue
<template>
<button @click="sendEvent">點(diǎn)擊發(fā)送事件</button>
</template>
<script>
export default {
name: 'SonComponent',
methods: {
sendEvent() {
// 發(fā)送自定義事件
this.$emit('eventFromChild', 'data from Son Component!!');
}
}
};
</script>
// ParentComponent.vue
<template>
<div>
<!-- 接收事件 -->
<son-component @eventFromChild="recvChildEvent" />
<h1>{{data}}</h1>
</div>
</template>
<script>
import SonComponent from './SonComponent';
export default {
name: 'ParentComponent',
components: {
SonComponent
},
data() {
return {
data: 'hhhh'
};
},
methods: {
recvChildEvent(data) {
this.data = data;
}
}
};
</script>
? 父?jìng)髯訉O:父組件/祖先組件通過provide
提供變量,子孫組件通過inject
來接收該變量:
// ParentComponent
import SonComponent from './SonComponent.vue'
export default {
name: 'ParentComponent',
components: {
SonComponent
},
provide: {
message: 'data from Parent Component'
}
}
// SonComponent
<template>
<h1>{{message}}</h1>
</template>>
<script>
export default {
name: 'SonComponent',
inject: ['message']
}
</script>>
更多組件間通信方式,請(qǐng)參考:Vue組件間通信6種方式