本文首發於微信公眾號「程式設計師面試官」
前言
Vue框架部分我們會涉及一些高頻且有一定探討價值的面試題,我們不會涉及一些非常初級的在官方文件就能檢視的純記憶性質的面試題,比如:
- vue常用的修飾符?
- vue-cli 工程常用的 npm 命令有哪些?
- vue中 keep-alive 元件的作用?
首先,上述型別的面試題在文件中可查,沒有比官方文件更權威的答案了,其次這種問題沒有太大價值,除了考察候選人的記憶力,最後,這種面試題只要用過vue的都知道,沒有必要佔用我們的篇幅.
我們的問題並不多,但是難度可能會高一些,如果你真的搞懂了這些問題,在絕大多數情況下會有舉一反三的效果,可以說基本能拿下Vue相關的所有重要知識點了.
你對MVVM的理解?
MVVM是什麼?
MVVM 模式,顧名思義即 Model-View-ViewModel 模式。它萌芽於2005年微軟推出的基於 Windows 的使用者介面框架 WPF ,前端最早的 MVVM 框架 knockout 在2010年釋出。
Model 層: 對應資料層的域模型,它主要做域模型的同步。通過 Ajax/fetch 等 API 完成客戶端和服務端業務 Model 的同步。在層間關係裡,它主要用於抽象出 ViewModel 中檢視的 Model。
View 層:作為檢視模板存在,在 MVVM 裡,整個 View 是一個動態模板。除了定義結構、佈局外,它展示的是 ViewModel 層的資料和狀態。View 層不負責處理狀態,View 層做的是 資料繫結的宣告、 指令的宣告、 事件繫結的宣告。
ViewModel 層:把 View 需要的層資料暴露,並對 View 層的 資料繫結宣告、 指令宣告、 事件繫結宣告 負責,也就是處理 View 層的具體業務邏輯。ViewModel 底層會做好繫結屬性的監聽。當 ViewModel 中資料變化,View 層會得到更新;而當 View 中宣告瞭資料的雙向繫結(通常是表單元素),框架也會監聽 View 層(表單)值的變化。一旦值變化,View 層繫結的 ViewModel 中的資料也會得到自動更新。
MVVM的優缺點?
優點:
- 分離檢視(View)和模型(Model),降低程式碼耦合,提高檢視或者邏輯的重用性: 比如檢視(View)可以獨立於Model變化和修改,一個ViewModel可以繫結不同的"View"上,當View變化的時候Model不可以不變,當Model變化的時候View也可以不變。你可以把一些檢視邏輯放在一個ViewModel裡面,讓很多view重用這段檢視邏輯
- 提高可測試性: ViewModel的存在可以幫助開發者更好地編寫測試程式碼
- 自動更新dom: 利用雙向繫結,資料更新後檢視自動更新,讓開發者從繁瑣的手動dom中解放
缺點:
- Bug很難被除錯: 因為使用雙向繫結的模式,當你看到介面異常了,有可能是你View的程式碼有Bug,也可能是Model的程式碼有問題。資料繫結使得一個位置的Bug被快速傳遞到別的位置,要定位原始出問題的地方就變得不那麼容易了。另外,資料繫結的宣告是指令式地寫在View的模版當中的,這些內容是沒辦法去打斷點debug的
- 一個大的模組中model也會很大,雖然使用方便了也很容易保證了資料的一致性,當時長期持有,不釋放記憶體就造成了花費更多的記憶體
- 對於大型的圖形應用程式,檢視狀態較多,ViewModel的構建和維護的成本都會比較高
你對Vue生命週期的理解?
生命週期是什麼
Vue 例項有一個完整的生命週期,也就是從開始建立、初始化資料、編譯模版、掛載Dom -> 渲染、更新 -> 渲染、解除安裝等一系列過程,我們稱這是Vue的生命週期。
各個生命週期的作用
生命週期 | 描述 |
---|---|
beforeCreate | 元件例項被建立之初,元件的屬性生效之前 |
created | 元件例項已經完全建立,屬性也繫結,但真實dom還沒有生成,$el 還不可用 |
beforeMount | 在掛載開始之前被呼叫:相關的 render 函式首次被呼叫 |
mounted | el 被新建立的 vm.$el 替換,並掛載到例項上去之後呼叫該鉤子 |
beforeUpdate | 元件資料更新之前呼叫,發生在虛擬 DOM 打補丁之前 |
update | 元件資料更新之後 |
activited | keep-alive專屬,元件被啟用時呼叫 |
deadctivated | keep-alive專屬,元件被銷燬時呼叫 |
beforeDestory | 元件銷燬前呼叫 |
destoryed | 元件銷燬後呼叫 |
生命週期示意圖
非同步請求適合在哪個生命週期呼叫?
官方例項的非同步請求是在mounted生命週期中呼叫的,而實際上也可以在created生命週期中呼叫。
Vue元件如何通訊?
Vue元件通訊的方法如下:
props/$emit+v-on
: 通過props將資料自上而下傳遞,而通過$emit和v-on來向上傳遞資訊。- EventBus: 通過EventBus進行資訊的釋出與訂閱
- vuex: 是全域性資料管理庫,可以通過vuex管理全域性的資料流
$attrs/$listeners
: Vue2.4中加入的$attrs/$listeners
可以進行跨級的元件通訊- provide/inject:以允許一個祖先元件向其所有子孫後代注入一個依賴,不論元件層次有多深,並在起上下游關係成立的時間裡始終生效,這成為了跨元件通訊的基礎
還有一些用solt插槽或者ref例項進行通訊的,使用場景過於有限就不贅述了。
computed和watch有什麼區別?
computed:
computed
是計算屬性,也就是計算值,它更多用於計算值的場景computed
具有快取性,computed的值在getter執行後是會快取的,只有在它依賴的屬性值改變之後,下一次獲取computed的值時才會重新呼叫對應的getter來計算computed
適用於計算比較消耗效能的計算場景
watch:
- 更多的是「觀察」的作用,類似於某些資料的監聽回撥,用於觀察
props
$emit
或者本元件的值,當資料變化時來執行回撥進行後續操作 - 無快取性,頁面重新渲染時值不變化也會執行
小結:
- 當我們要進行數值計算,而且依賴於其他資料,那麼把這個資料設計為computed
- 如果你需要在某個資料變化時做一些事情,使用watch來觀察這個資料變化
Vue是如何實現雙向繫結的?
利用Object.defineProperty
劫持物件的訪問器,在屬性值發生變化時我們可以獲取變化,然後根據變化進行後續響應,在vue3.0中通過Proxy代理物件進行類似的操作。
// 這是將要被劫持的物件
const data = {
name: '',
};
function say(name) {
if (name === '古天樂') {
console.log('給大家推薦一款超好玩的遊戲');
} else if (name === '渣渣輝') {
console.log('戲我演過很多,可遊戲我只玩貪玩懶月');
} else {
console.log('來做我的兄弟');
}
}
// 遍歷物件,對其屬性值進行劫持
Object.keys(data).forEach(function(key) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log('get');
},
set: function(newVal) {
// 當屬性值發生變化時我們可以進行額外操作
console.log(`大家好,我係${newVal}`);
say(newVal);
},
});
});
data.name = '渣渣輝';
//大家好,我係渣渣輝
//戲我演過很多,可遊戲我只玩貪玩懶月
複製程式碼
Proxy與Object.defineProperty的優劣對比?
Proxy的優勢如下:
- Proxy可以直接監聽物件而非屬性
- Proxy可以直接監聽陣列的變化
- Proxy有多達13種攔截方法,不限於apply、ownKeys、deleteProperty、has等等是
Object.defineProperty
不具備的 - Proxy返回的是一個新物件,我們可以只操作新的物件達到目的,而
Object.defineProperty
只能遍歷物件屬性直接修改 - Proxy作為新標準將受到瀏覽器廠商重點持續的效能優化,也就是傳說中的新標準的效能紅利
Object.defineProperty的優勢如下:
- 相容性好,支援IE9
你是如何理解Vue的響應式系統的?
響應式系統簡述:
- 任何一個 Vue Component 都有一個與之對應的 Watcher 例項。
- Vue 的 data 上的屬性會被新增 getter 和 setter 屬性。
- 當 Vue Component render 函式被執行的時候, data 上會被 觸碰(touch), 即被讀, getter 方法會被呼叫, 此時 Vue 會去記錄此 Vue component 所依賴的所有 data。(這一過程被稱為依賴收集)
- data 被改動時(主要是使用者操作), 即被寫, setter 方法會被呼叫, 此時 Vue 會去通知所有依賴於此 data 的元件去呼叫他們的 render 函式進行更新。
虛擬DOM的優劣如何?
優點:
- 保證效能下限: 虛擬DOM可以經過diff找出最小差異,然後批量進行patch,這種操作雖然比不上手動優化,但是比起粗暴的DOM操作效能要好很多,因此虛擬DOM可以保證效能下限
- 無需手動操作DOM: 虛擬DOM的diff和patch都是在一次更新中自動進行的,我們無需手動操作DOM,極大提高開發效率
- 跨平臺: 虛擬DOM本質上是JavaScript物件,而DOM與平臺強相關,相比之下虛擬DOM可以進行更方便地跨平臺操作,例如伺服器渲染、移動端開發等等
缺點:
- 無法進行極致優化: 在一些效能要求極高的應用中虛擬DOM無法進行鍼對性的極致優化,比如VScode採用直接手動操作DOM的方式進行極端的效能優化
虛擬DOM實現原理?
- 虛擬DOM本質上是JavaScript物件,是對真實DOM的抽象
- 狀態變更時,記錄新樹和舊樹的差異
- 最後把差異更新到真正的dom中
詳細實現見虛擬DOM原理?
既然Vue通過資料劫持可以精準探測資料變化,為什麼還需要虛擬DOM進行diff檢測差異?
考點: Vue的變化偵測原理
前置知識: 依賴收集、虛擬DOM、響應式系統
現代前端框架有兩種方式偵測變化,一種是pull一種是push
pull: 其代表為React,我們可以回憶一下React是如何偵測到變化的,我們通常會用setState
API顯式更新,然後React會進行一層層的Virtual Dom Diff操作找出差異,然後Patch到DOM上,React從一開始就不知道到底是哪發生了變化,只是知道「有變化了」,然後再進行比較暴力的Diff操作查詢「哪發生變化了」,另外一個代表就是Angular的髒檢查操作。
push: Vue的響應式系統則是push的代表,當Vue程式初始化的時候就會對資料data進行依賴的收集,一但資料發生變化,響應式系統就會立刻得知,因此Vue是一開始就知道是「在哪發生變化了」,但是這又會產生一個問題,如果你熟悉Vue的響應式系統就知道,通常一個繫結一個資料就需要一個Watcher,一但我們的繫結細粒度過高就會產生大量的Watcher,這會帶來記憶體以及依賴追蹤的開銷,而細粒度過低會無法精準偵測變化,因此Vue的設計是選擇中等細粒度的方案,在元件級別進行push偵測的方式,也就是那套響應式系統,通常我們會第一時間偵測到發生變化的元件,然後在元件內部進行Virtual Dom Diff獲取更加具體的差異,而Virtual Dom Diff則是pull操作,Vue是push+pull結合的方式進行變化偵測的.
Vue為什麼沒有類似於React中shouldComponentUpdate的生命週期?
考點: Vue的變化偵測原理
前置知識: 依賴收集、虛擬DOM、響應式系統
根本原因是Vue與React的變化偵測方式有所不同
React是pull的方式偵測變化,當React知道發生變化後,會使用Virtual Dom Diff進行差異檢測,但是很多元件實際上是肯定不會發生變化的,這個時候需要用shouldComponentUpdate進行手動操作來減少diff,從而提高程式整體的效能.
Vue是pull+push的方式偵測變化的,在一開始就知道那個元件發生了變化,因此在push的階段並不需要手動控制diff,而元件內部採用的diff方式實際上是可以引入類似於shouldComponentUpdate相關生命週期的,但是通常合理大小的元件不會有過量的diff,手動優化的價值有限,因此目前Vue並沒有考慮引入shouldComponentUpdate這種手動優化的生命週期.
Vue中的key到底有什麼用?
key
是為Vue中的vnode標記的唯一id,通過這個key,我們的diff操作可以更準確、更快速
diff演算法的過程中,先會進行新舊節點的首尾交叉對比,當無法匹配的時候會用新節點的key
與舊節點進行比對,然後超出差異.
diff程可以概括為:oldCh和newCh各有兩個頭尾的變數StartIdx和EndIdx,它們的2個變數相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設定了key,就會用key進行比較,在比較的過程中,變數會往中間靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一個已經遍歷完了,就會結束比較,這四種比較方式就是首、尾、舊尾新頭、舊頭新尾.
- 準確: 如果不加
key
,那麼vue會選擇複用節點(Vue的就地更新策略),導致之前節點的狀態被保留下來,會產生一系列的bug. - 快速: key的唯一性可以被Map資料結構充分利用,相比於遍歷查詢的時間複雜度O(n),Map的時間複雜度僅僅為O(1).