談談你對 Vue 的理解?
根據官方說法,Vue 是一套用于構建用戶界面的漸進式框架。Vue 的設計受到了 MVVM 的啟發。Vue 的兩個核心是數據驅動和組件系統。
但我為什么使用 Vue,有以下幾個原因:
Vue 對于前端初學者比較友好。一個 Vue 文件的結構和原生 HTML 保持了高度相似,分為靜態頁面,用于放置 html 標簽,和 script,用于處理用戶操作和業務邏輯,最后是 style 樣式,用于書寫 CSS 代碼,這種寫法可以讓初學者感到習慣。
其次,Vue 提供了許多 JS 定制化的操作,比如 v-bind 和事件監聽的 @ 符號,開發者可以直接使用,從而減少一些重復代碼的書寫。
最后,就是 Vue 提供一套高效的響應式的系統用于更新 DOM,可以讓開發者專注于處理業務而非技術實現。
什么是 MVVM,可以介紹一下嗎?
MVVM,即 Model–View–ViewModel,是一種軟件架構模式。
-
Model
即模型,是指代表真實狀態內容的領域模型(面向對象),或指代表內容的數據訪問層(以數據為中心)。
-
View
即視圖,是用戶在屏幕上看到的結構、布局和外觀(UI)。
-
ViewModel
即視圖模型,是暴露公共屬性和命令的視圖的抽象。用于把
Model
和View
關聯起來。ViewModel
負責把Model
的數據同步到View
顯示出來,還負責把View
的修改同步回Model
。
在 MVVM 架構下,View
和 Model
之間并沒有直接的聯系,而是通過 ViewModel
進行交互,Model
和 ViewModel
之間的交互是雙向的,View
數據的變化會同步到 Model
中,而 Model
數據的變化也會立即反應到 View
上。
因此開發者只需關注業務邏輯,不需要手動操作 DOM
,不需要關注數據狀態的同步問題,復雜的數據狀態維護完全由 MVVM 來統一管理。
Vue 是如何實現數據雙向綁定的?
Vue 實現數據雙向綁定主要是采用數據劫持結合發布者-訂閱者模式的方式。具體實現就是整合 Observer,Compiler 和 Watcher 三者。
-
Observer
觀察者。Vue 通過 Observer 對數據對象的所有屬性進行監聽,當把一個普通對象傳給 Vue 實例的
data
選項時,Observer 將遍歷它的所有屬性,并為其添加getter
和setter
。getter
將收集此屬性所有的訂閱者,setter
將在屬性發生變動的時候,重新為此屬性賦值,并通知訂閱者調用其對應的更新函數。在 Vue 2 中是通過 ES5 的
Object.defineProperty()
方法實現。在 Vue 3 中是通過 ES6 的
new Proxy()
實現的。 -
Compiler
模板編譯器。它的作用是對每個元素節點的指令
v-
和模板語法{{}}
進行掃描,替換對應的真實數據,或綁定相應的事件函數。 -
Watcher
發布者/訂閱者。Watcher 作為連接 Observer 和 Compiler 的橋梁,能夠訂閱并收到每個屬性變動的通知,然后執行相應的回調函數。Compiler 在編譯時通過 Watcher 綁定對應的數據更新回調函數,Observer 在監聽到數據變化時執行此回調。在 Observer 中,Watcher 就是訂閱者,在 Compiler 中,Watcher 就是發布者。
v-model 的原理?
v-model
是 vue 的一個語法糖,它用于監聽數據的改變并將數據更新。以 input 元素為例:
<el-input v-model="foo" />
其實就等價于
<el-input :value="foo" @input="foo = $event" />
如何在組件中實現 v-model ?
在 Vue 2 組件中實現 v-model
,只需定義 model
屬性即可。
export default {
model: {
prop: "value", // 屬性
event: "input", // 事件
},
}
在 Vue 3 組合式 API 實現 v-model
,需要定義 modelValue
參數,和 emits
方法。
defineProps({
modelValue: { type: String, default: "" },
})
const emits = defineEmits(["update:modelValue"])
function onInput(val) {
emits("update:modelValue", val)
}
當數據改變時,Vue 是如何更新 DOM 的?(Diff 算法和虛擬 DOM)
當我們修改了某個數據時,如果直接重新渲染到真實 DOM,開銷是很大的。Vue 為了減少開銷和提高性能采用了 Diff 算法。當數據發生改變時,Observer 會通知所有 Watcher,Watcher 就會調用 patch()
方法(Diff 的具體實現),把變化的內容更新到真實的 DOM,俗稱打補丁。
Diff 算法會對新舊節點進行同層級比較,當兩個新舊節點是相同節點的時候,再去比較他們的子節點(如果是文本則直接更新文本內容),逐層比較然后找到最小差異部分,進行 DOM 更新。如果不是相同節點,則刪除之前的內容,重新渲染。
patch()
方法先根據真實 DOM 生成一顆虛擬 DOM,保存到變量 oldVnode
,當某個數據改變后會生成一個新的 Vnode
,然后 Vnode
和 oldVnode
進行對比,發現有不一樣的地方就直接修改在真實 DOM 上,最后再返回新節點作為下次更新的 oldVnode
。
什么是虛擬 DOM ?
虛擬 DOM(Virtual DOM)就是將真實 DOM 的主要數據抽取出來,并以對象的形式表達。
比如真實 DOM 如下:
<div id="id" class="cls">
<h1>123</h1>
</div>
對應的虛擬 DOM 就是(偽代碼):
{
tag: 'div',
sel: 'div#id.cls',
children: [
{ tag: 'h1', text: '123' }
]
}
Vue 中的 key 有什么用?
在 Vue 中,key 被用來作為 VNode 的唯一標識。
-
key 主要用在 Vue 的虛擬 DOM Diff 算法,在新舊節點對比時作為識別 VNode 的一個線索。然后找到正確的位置插入或更新節點。如果新舊節點中提供了 key,能更快速地進行比較及復用。反之,Vue 會盡可能復用相同類型元素。
<ul> <li v-for="item in items" :key="item.id">{{ item.name }}</li> </ul>
-
手動改變 key 值,可以強制 DOM 進行重新渲染。
<transition> <span :key="text">{{ text }}</span> </transition>
Vue 3 對 diff 算法進行了哪些優化
在 Vue 2 中,每當數據發生變化時,Vue 會創建一個新的虛擬 DOM 樹,并對整個虛擬 DOM 樹進行遞歸比較,即使其中大部分內容是靜態的,最后再找到不同的節點,然后進行更新。
Vue 3 引入了靜態標記的概念,通過靜態標記,Vue 3 可以將模板中的靜態內容和動態內容區分開來。這樣,在更新過程中,Vue 3 只會關注動態部分的比較,而對于靜態內容,它將跳過比較的步驟,從而避免了不必要的比較,提高了性能和效率。
Vue 實例的生命周期鉤子都有哪些?
每個 Vue 實例在被創建時都要經過一系列的初始化過程——例如,需要設置數據監聽、編譯模板、將實例掛載到 DOM 并在數據變化時更新 DOM 等。同時在這個過程中也會運行一些叫做生命周期鉤子的函數,這給了用戶在不同階段添加自己的代碼的機會。
Vue 2 有以下鉤子:
-
beforeCreate
實例初始化之前,
$el
和data
都為undefined
。 -
created
實例創建完成,
data
已經綁定。 -
beforeMount
將
<template>
和data
生成虛擬DOM
節點,可以訪問到$el
,但還沒有渲染到html
上。 -
mounted
實例掛載完成,渲染到
html
頁面中。 -
beforeUpdate
data
更新之前,虛擬DOM
重新渲染之前。 -
updated
由于
data
更新導致的虛擬DOM
重新渲染之后。 -
activated
keep-alive
專用,實例被激活時調用。 -
deactivated
keep-alive
專用,實例被移除時調用。 -
beforeDestroy
實例銷毀之前(實例仍然可用)。
-
destroyed
實例銷毀之后。所有的事件監聽器會被移除,所有的子實例也會被銷毀,但
DOM
節點依舊存在。該鉤子在服務器端渲染期間不被調用。
第一次頁面加載會觸發這四個鉤子:
beforeCreate
created
beforeMount
mounted
Vue 3 組合式 API 有以下鉤子:
-
onBeforeMount()
在組件被掛載之前被調用。
-
onMounted()
在組件掛載完成后執行。
-
onBeforeUpdate()
在組件即將因為響應式狀態變更而更新其 DOM 樹之前調用。
-
onUpdated()
在組件因為響應式狀態變更而更新其 DOM 樹之后調用。
-
onBeforeUnmount()
在組件實例被卸載之前調用。
-
onUnmounted()
在組件實例被卸載之后調用。相當于 Vue 2 的
destroyed
。 -
onErrorCaptured()
在捕獲了后代組件傳遞的錯誤時調用。
-
onRenderTracked()
當組件渲染過程中追蹤到響應式依賴時調用。只在開發環境生效。
-
onRenderTriggered()
當響應式依賴的變更觸發了組件渲染時調用。只在開發環境生效。
-
onActivated()
keep-alive
專用,當組件被插入到 DOM 中時調用。 -
onDeactivated()
keep-alive
專用,當組件從 DOM 中被移除時調用。 -
onServerPrefetch()
在組件實例在服務器上被渲染之前調用。只在 SSR 模式下生效。
$nextTick 的使用場景和原理
-
$nextTick 是什么?
在下次 DOM 更新循環結束之后執行的一個方法。
export default { data() { return { message: "Hello Vue!", } }, methods: { example() { // 修改數據 this.message = "changed" // DOM 尚未更新 this.$nextTick(() => { // DOM 現在更新了 console.log("DOM 現在更新了") }) }, }, }
-
$nextTick 的使用場景
在修改數據之后使用這個方法,用于獲取更新后的 DOM。
-
使用 $nextTick 的原理
在下次循環結束之后,Vue 會自動觸發一個
update
事件,在這個事件中會調用所有的 $nextTick 回調。
為什么 Vue 組件中的 data 必須是函數?
因為在 Vue 中組件是可以被復用的,組件復用其實就是創建多個 Vue 實例,實例之間共享 prototype.data 屬性,當 data 的值引用的是同一個對象時,改變其中一個就會影響其他組件,造成互相污染,而改用函數的形式將數據 return 出去,則每次復用都是嶄新的對象。
這里我們舉個例子:
function Component() {}
Component.prototype.data = {
name: "vue",
language: "javascript",
}
const A = new Component()
const B = new Component()
A.data.language = "typescript"
console.log(A.data) // { name: 'vue', language: 'typescript' }
console.log(B.data) // { name: 'vue', language: 'typescript' }
此時,A 和 B 的 data 都指向了同一個內存地址,language
都變成了 'typescript'。
我們改成函數式的寫法,就不會有這樣的問題了。
function Component() {
this.data = this.data()
}
Component.prototype.data = function () {
return { name: "vue", language: "javascript" }
}
const A = new Component()
const B = new Component()
A.data.language = "typescript"
console.log(A.data) // { name: 'vue', language: 'typescript' }
console.log(B.data) // { name: 'vue', language: 'javascript' }
所以組件的 data 選項必須是一個函數,該函數返回一個獨立的拷貝,這樣就不會出現數據相互污染的問題。
組件之間是如何進行通信?
-
父子組件通信
通過
props
傳參通過
$emit
觸發通過
$refs
調用子組件方法
-
兄弟組件通信
狀態管理
vuex
-
事件總線
EventBus
// event-bus.js import Vue from "vue" export default new Vue()
// 組件 A import Bus from "event-bus.js" export default { methods: { handleClick(val) { Bus.$emit("functionName", val) }, }, }
// 組件 B import Bus from "event-bus.js" export default { created() { Bus.$on("functionName", val => { console.log(val) }) }, }
localStorage
、sessionStorage
或Cookies
Vue 項目中做過哪些性能優化?
-
UI 庫按需加載,減小打包體積,以 ElementUI 為例:
// main.js import { Button, Select } from "element-ui" Vue.use(Button) Vue.use(Select)
-
路由按需加載
// router.js export default new VueRouter({ routes: [ { path: "/", component: () => import("@/components/Home") }, { path: "/about", component: () => import("@/components/About") }, ], })
-
組件銷毀后把同時銷毀全局變量和移除事件監聽和清除定時器,防止內存泄漏
beforeDestroy() { clearInterval(this.timer) window.removeEventListener('resize', this.handleResize) },
合理使用 v-if 和 v-show 使用
Vue 和 React 的區別?
看這里
Vue 3.x 帶來了哪些新的特性和性能方面的提升?
引入了 Composition API(組合式 API)。允許開發者更靈活地組織和重用組件邏輯。它使用函數而不是選項對象來組織組件的代碼,使得代碼更具可讀性和維護性。
多根組件??梢灾苯釉?template 中使用多個根級別的元素,而不需要額外的包裝元素。這樣更方便地組織組件的結構。
引入了 Teleport(傳送)。可以將組件的內容渲染到指定 DOM 節點的新特性。一般用于創建全局彈窗和對話框等組件。
響應式系統升級。從 defineProperty 升級到 ES2015 原生的 Proxy,不需要初始化遍歷所有屬性,就可以監聽新增和刪除的屬性。
編譯優化。重寫了虛擬 DOM,提升了渲染速度。diff 時靜態節點會被直接跳過。
源碼體積優化。移除了一些非必要的特性,如 filter,一些新增的模塊也將會被按需引入,減小了打包體積。
打包優化。更強的 Tree Shaking,可以過濾不使用的模塊,沒有使用到的組件,比如過渡(transition)組件,則打包時不會包含它。
為什么 Vue 3.x 采用了 Proxy 拋棄了 Object.defineProperty()
?
-
Proxy 可以代理任何對象,包括數組,而 Vue 2 中是通過重寫數組的以下七種方法實現的。
push()
(將一個或多個元素添加到數組的末尾,并返回該數組的新長度)pop()
(移除并返回數組的最后一個元素)unshift()
(將一個或多個元素添加到數組的開頭,并返回該數組的新長度)shift()
(移除并返回數組的第一個元素)splice()
(刪除數組中的一個或多個元素,并將其返回)sort()
(對數組進行排序)reverse()
(對數組進行反轉)
Proxy 可以直接監聽整個對象而非屬性,而
Object.defineProperty()
只能先遍歷對象屬性再去進行監聽。相比之下 Proxy 更加簡潔,更加高效,更加安全。-
Proxy 返回的是一個新對象,我們可以只操作新的對象達到目的。
const cat = { name: "Tom", } const myCat = new Proxy(cat, { get(target, property) { console.log(`我的 ${property} 被讀取了`) return property in target ? target[property] : undefined }, set(target, property, value) { console.log(`我的 ${property} 被設置成了 ${value}`) target[property] = value return true }, }) myCat.name // expected output: 我被讀取了:name myCat.name = "Kitty" // expected output: 我的 name 被設置成了 Kitty
-
Object.defineProperty()
的本質是在一個對象上定義一個新屬性,或者修改一個現有屬性。const cat = { name: "Tom", } Object.defineProperty(cat, "name", { get() { console.log(`我被讀取了`) }, set(value) { console.log(`我被設置成了 ${value}`) }, }) cat.name // expected output: 我被讀取了 cat.name = "Kitty" // expected output: 我被設置成了 Kitty
-
而 Proxy 天生用于代理一個對象,它有 13 種基本操作的攔截方法,是
Object.defineProperty()
不具備的。apply()
(攔截函數的調用)construct()
(攔截構造函數的調用)defineProperty()
(攔截屬性的定義)deleteProperty()
(攔截屬性的刪除)get()
(攔截對象屬性的讀?。?/p>getOwnPropertyDescriptor()
(攔截對象屬性的描述)getPrototypeOf()
(攔截對象的原型)has()
(攔截對象屬性的檢查)isExtensible()
(攔截對象是否可擴展的檢查)ownKeys()
(攔截對象的屬性列表)preventExtensions()
(攔截對象是否可擴展的設置)set()
(攔截對象屬性的設置)setPrototypeOf()
(攔截對象的原型的設置)
Composition API(組合式 API)與 Options API(選項式 API)有什么區別?
Options API 會將組件中的同一邏輯相關的代碼拆分到不同選項,比如
data
、props
、methods
等,而使用 Composition API 較為靈活,開發者可以將同一個邏輯的相關代碼放在一起。Composition API 通過 Vue 3.x 新增的 setup 選項進行使用,該選項會在組件創建之前執行,第一個參數
props
,第二個參數context
,return 的所有內容都會暴露給組件的其余部分 (計算屬性、方法、生命周期鉤子等等) 以及組件的模板。Composition API 上的生命周期鉤子與 Options API 基本相同,但需要添加前綴
on
,比如onMounted
、onUpdated
等。
v-for 和 v-if 可以同時使用嗎
可以同時使用,但不推薦,具體原因參考官方說明。
在 Vue 3 中,當 v-if 和 v-for 同時存在于一個節點上時,v-if 比 v-for 的優先級更高,此時 v-if 無法訪問 v-for 中的對象。
當確實需要條件遍歷渲染的話,有以下幾個方法:
- fitler
<li v-for="todo in todos.filter(todo => !todo.isDone)">{{ todo.name }}</li>
使用數組的 filter 的方法可以提前對不需要的數據進行過濾,根源上解決這個問題。
- 使用 v-show
<li v-for="todo in todos" v-show="!todo.isDone">{{ todo.name }}</li>
v-show 和 v-if 都可以用于隱藏某個元素,但 v-if 用于決定是否渲染,而 v-show 則使用 display 屬性決定是否顯示。此時可以避免 v-if 和 v-for 同時使用造成的的渲染問題。
- 添加額外的標簽
<template v-for="todo in todos">
<li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>
添加額外的標簽,根據層級的不同,可以自己決定 v-if 和 v-for 的優先級,這種方法更加靈活也更容易理解,但會有更深的代碼結構。
參考資料: