前言
最近兩年許多大廠都在實行“降本增效”、“優化組織架構”,然后 “為社會輸送了大量人才”,今年(2023)更是不容易,一些外資企業也陸續撤離,各行各業訂單大量減少,業務大量裁撤,導致工作崗位大幅度減少。程序員們不得不重新找工作,有人回到了老家做起了生意,有人送起了外賣,有人跑起了網約車。當然也有不少人選擇繼續堅持,不管做出了什么選擇,各位的選擇都值得被尊重。在這個時代環境下,大家都是好樣的。
而我選擇了繼續堅持,最近也在找工作的路上,總結了一些面試題,如果你的技術棧是 Vue,或者需要從 React 轉到 Vue,希望這篇面試題能幫到你。
談談你對 Vue 的理解?為什么選擇 Vue?
根據官方說法,Vue 是一套用于構建用戶界面的漸進式框架。Vue 的設計受到了 MVVM 的啟發。Vue 的兩個核心是數據驅動和組件系統。
我為什么使用 Vue,有以下幾個原因:
Vue 對于前端初學者比較友好。一個 Vue 文件的結構和原生 HTML 保持了高度相似,分為模板、腳本和樣式,這種寫法可以讓前端初學者快速入門。
其次,就是 Vue 提供一套高效的響應式系統用于更新 DOM,可以讓開發者專注于處理業務。
最后,Vue 提供了許多 JS 定制化的操作,比如指令和是修飾符,開發者可以直接使用,幫助開發者們減少了大量時間。
什么是 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 就是發布者。
為什么 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()
(攔截對象屬性的讀取)getOwnPropertyDescriptor()
(攔截對象屬性的描述)getPrototypeOf()
(攔截對象的原型)has()
(攔截對象屬性的檢查)isExtensible()
(攔截對象是否可擴展的檢查)ownKeys()
(攔截對象的屬性列表)preventExtensions()
(攔截對象是否可擴展的設置)set()
(攔截對象屬性的設置)setPrototypeOf()
(攔截對象的原型的設置)
Vue 是如何實現數據雙向綁定的?v-model 的原理?
Vue 組件可以通過使用 v-model
指令以實現雙向綁定。v-model
是 vue 的一個語法糖,它用于監聽數據的改變并將數據更新。以 input 元素為例:
<el-input v-model="foo" />
其實就等價于
<input :value="searchText" @input="searchText = $event.target.value" />
如何在組件中實現 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 操作。虛擬 DOM 的主要目的是提高性能和減少實際 DOM 操作的次數,從而改善用戶界面的渲染速度和響應性。
比如真實 DOM 如下:
<div id="hello">
<h1>123</h1>
</div>
對應的虛擬 DOM 就是(偽代碼):
const vnode = {
type: "div",
props: {
id: "hello",
},
children: [
{
type: "h1",
innerText: "123",
},
],
}
Vue 中的 key 有什么用?
在 Vue 中,key 被用來作為 VNode 的唯一標識。
-
key 主要用在虛擬 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>
watch 和 computed 分別是做什么的?有何區別?
watch 和 computed 都可以用于監聽數據,區別是使用場景不同,watch 用于監聽一個數據,當數據改變時,可以執行傳入的回調函數:
<script setup>
import { reactive, watch } from "vue"
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
// do something
}
)
</script>
computed 用于返回一個新的數據,在 Vue 3.x 中會返回一個只讀的響應式 ref 對象。但是可以接受一個帶有 get 和 set 函數的對象來創建一個可寫的 ref 對象。
<script setup>
import { reactive, computed } from "vue"
const state = reactive({ numA: 1, numB: 2 })
const plusOne = computed(() => state.numA + state.numB)
console.log(plusOne.value) // 3
</script>
有些人會提到 computed 支持緩存,不支持異步,也是和 watch 的區別。
但這里要告訴大家的是,computed 本身的設計就是為了計算,而非異步的獲取一個數據,詳情請參考官網。
至于緩存,這同樣屬于 computed 的特性,它支持緩存,這是和調用普通函數的區別,而不應該和 watch 進行比較,watch 本身用于監聽數據變化,在根本上不存在緩存的概念。
Vue 3.x 帶來了哪些新的特性和性能方面的提升?
引入了 Composition API(組合式 API)。允許開發者更靈活地組織和重用組件邏輯。它使用函數而不是選項對象來組織組件的代碼,使得代碼更具可讀性和維護性。
多根組件。可以直接在 template 中使用多個根級別的元素,而不需要額外的包裝元素。這樣更方便地組織組件的結構。
引入了 Teleport(傳送)。可以將組件的內容渲染到指定 DOM 節點的新特性。一般用于創建全局彈窗和對話框等組件。
響應式系統升級。從 defineProperty 升級到 ES2015 原生的 Proxy,不需要初始化遍歷所有屬性,就可以監聽新增和刪除的屬性。
編譯優化。重寫了虛擬 DOM,提升了渲染速度。diff 時靜態節點會被直接跳過。
源碼體積優化。移除了一些非必要的特性,如 filter,一些新增的模塊也將會被按需引入,減小了打包體積。
打包優化。更強的 Tree Shaking,可以過濾不使用的模塊,沒有使用到的組件,比如過渡(transition)組件,則打包時不會包含它。
Vue 3 移除了哪些特性
-
移除了過濾器 filter,可以使用 computed 或函數代替
filter 在 Vue 2 的用法:
<template> <p>{{ accountBalance | currencyUSD }}</p> </template> <script> export default { data() { return { accountBalance: "99", } }, filters: { currencyUSD(value) { return "$" + value }, }, } </script>
移除了 .native .sync 修飾符
移除了 $listeners
移除了 EventBus 的相關屬性:
off 和 $once
移除了 $children,可以使用 ref 代替
...
Vue 3 對 diff 算法進行了哪些優化
在 Vue 2 中,每當數據發生變化時,Vue 會創建一個新的虛擬 DOM 樹,并對整個虛擬 DOM 樹進行遞歸比較,即使其中大部分內容是靜態的,最后再找到不同的節點,然后進行更新。
Vue 3 引入了靜態標記的概念,通過靜態標記,Vue 3 可以將模板中的靜態內容和動態內容區分開來。這樣,在更新過程中,Vue 3 只會關注動態部分的比較,而對于靜態內容,它將跳過比較的步驟,從而避免了不必要的比較,提高了性能和效率。
<div>
<!-- 需靜態提升 -->
<div>foo</div>
<!-- 需靜態提升 -->
<div>bar</div>
<div>{{ dynamic }}</div>
</div>
Vue 實例的生命周期鉤子都有哪些?
生命周期鉤子是指一個組件實例從創建到卸載(銷毀)的全過程,例如,設置數據監聽、編譯模板、將實例掛載到 DOM 并在數據變化時更新 DOM 等。在這個過程中會運行一些叫做生命周期鉤子的函數,從而可以使開發者們在不同階段處理不同的業務。
Vue 2 和 Vue 3 選項式 API 的鉤子大致是一樣的,有以下鉤子:
-
beforeCreate
實例初始化之前,
$el
和data
都為undefined
。 -
created
實例創建完成,
data
已經綁定。但$el
不可用。 -
beforeMount
將
<template>
和data
生成虛擬DOM
節點,可以訪問到$el
,但還沒有渲染到html
上。 -
mounted
實例掛載完成,渲染到
html
頁面中。 -
beforeUpdate
data
更新之前,虛擬DOM
重新渲染之前。 -
updated
由于
data
更新導致的虛擬DOM
重新渲染之后。 -
beforeDestroy(Vue 2) | beforeUnmount(Vue 3)
實例銷毀之前(實例仍然可用)。
-
destroyed(Vue 2) | beforeUnmount(Vue 3)
實例銷毀之后。所有的事件監聽器會被移除,所有的子實例也會被銷毀,但
DOM
節點依舊存在。該鉤子在服務器端渲染期間不被調用。 -
activated
keep-alive
專用,實例被激活時調用。 -
deactivated
keep-alive
專用,實例被移除時調用。 -
errorCaptured
在捕獲了后代組件傳遞的錯誤時調用。
第一次頁面加載會觸發這四個鉤子:
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 更新循環結束之后執行的一個方法。一般在修改數據之后使用這個方法操作更新后的 DOM。
export default {
data() {
return {
message: "Hello Vue!",
}
},
methods: {
example() {
// 修改數據
this.message = "changed"
// DOM 尚未更新
this.$nextTick(() => {
// DOM 現在更新了
console.log("DOM 現在更新了")
})
},
},
}
原理
在 Vue2 當中,nextTick 可以理解為就是收集異步任務到隊列當中并且開啟異步任務去執行它們。它可以同時收集組件渲染的任務,以及用戶手動放入的任務。組件渲染的任務是由 watcher 的 update 觸發,并且將回調函數包裝為異步任務,最后推到 nextTick 的隊列里,等待執行。
而在 Vue3 當中,nextTick 則是利用 promise 的鏈式調用,將用戶放入的回調放在更新視圖之后的 then 里面調用,用戶調用多少次 nextTick,就接著多少個 then。
為什么 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 選項必須是一個函數,該函數返回一個獨立的拷貝,這樣就不會出現數據相互污染的問題。
Vue 組件之間如何通信?
傳送門:Vue 3 組件之間如何通信
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.js
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 中的對象,在 Vue 2 中相反。
當確實需要條件遍歷渲染的話,有以下幾個方法:
- 使用數組的 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 的優先級,這種方法更加靈活也更容易理解,但會有更深的代碼結構。
總結
這篇不總結了,就祝大家都能找到一份滿意的工作。
參考資料: