前端常見的Vue面試題目彙總
歡迎關注前端早茶,與廣東靚仔攜手共同進階
前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~
1. 請說一下響應式資料的原理
預設Vue在初始化資料時,會給data中的屬性使用Object.defineProperty重新定義所有屬性,當頁面到對應屬性時,會進行依賴收集(收集當前元件中的watcher)如果屬性發生變化會通知相關依賴進行更新操作
收集當前元件中的watcher,我進一步問你什麼叫當前元件的watcher
?我面試時經常聽到這種模糊的說法,感覺就是看了些造玩具的文章就說熟悉響應式原理了,起碼的流程要清晰一些:
- 由於 Vue 執行一個元件的
render
函式是由Watcher
去代理執行的,Watcher
Watcher
自身先賦值給Dep.target
這個全域性變數,等待響應式屬性去收集它 - 這樣在哪個元件執行
render
函式時訪問了響應式屬性,響應式屬性就會精確的收集到當前全域性存在的Dep.target
作為自身的依賴 - 在響應式屬性發生更新時通知
Watcher
去重新呼叫vm._update(vm._render())
進行元件的檢視更新
響應式部分,如果你想在簡歷上寫熟悉的話,還是要抽時間好好的去看一下原始碼中真正的實現,而不是看這種模稜兩可的說法就覺得自己熟練掌握了。
2. 為什麼Vue採用非同步渲染
因為如果不採用非同步更新,那麼每次更新資料都會對當前租金按進行重新渲染,所以為了效能考慮,Vue會在本輪資料更新後,再去非同步更新資料
什麼叫本輪資料更新後,再去非同步更新資料?
輪指的是什麼,在eventLoop
裡的task
和microTask
,他們分別的執行時機是什麼樣的,為什麼優先選用microTask
,這都是值得深思的好問題。
建議看看這篇文章:Vue原始碼詳解之nextTick:MutationObserver只是浮雲,microtask才是核心!
3. nextTick實現原理
nextTick方法主要是使用了巨集任務和微任務,定義一個非同步方法,多次呼叫nextTick會將方法存在佇列中,通過這個非同步方法清空當前佇列。所以這個nextTick方法就是非同步方法
這句話說的很亂,典型的讓面試官忍不住想要深挖一探究竟的回答。(因為一聽你就不是真的懂)
正確的流程應該是先去嗅探環境
,依次去檢測
Promise的then
->MutationObserver的回撥函式
->setImmediate
->setTimeout
是否存在,找到存在的就使用它,以此來確定回撥函式佇列是以哪個 api 來非同步執行。
在nextTick
函式接受到一個callback
函式的時候,先不去呼叫它,而是把它 push 到一個全域性的queue
佇列中,等待下一個任務佇列的時候再一次性的把這個queue
裡的函式依次執行。
這個佇列可能是microTask
佇列,也可能是macroTask
佇列,前兩個 api 屬於微任務佇列,後兩個 api 屬於巨集任務佇列。
簡化實現一個非同步合併任務佇列:
let pending = false // 存放需要非同步呼叫的任務 const callbacks = [] function flushCallbacks () { pending = false // 迴圈執行佇列 for (let i = 0; i < callbacks.length; i++) { callbacks[i]() } // 清空 callbacks.length = 0 } function nextTick(cb) { callbacks.push(cb) if (!pending) { pending = true // 利用Promise的then方法 在下一個微任務佇列中把函式全部執行 // 在微任務開始之前 依然可以往callbacks裡放入新的回撥函式 Promise.resolve().then(flushCallbacks) } }
測試一下:
// 第一次呼叫 then方法已經被呼叫了 但是 flushCallbacks 還沒執行 nextTick(() => console.log(1)) // callbacks裡push這個函式 nextTick(() => console.log(2)) // callbacks裡push這個函式 nextTick(() => console.log(3)) // 同步函式優先執行 console.log(4) // 此時呼叫棧清空了,瀏覽器開始檢查微任務佇列,發現了 flushCallbacks 方法,執行。 // 此時 callbacks 裡的 3 個函式被依次執行。 // 4 // 1 // 2 // 3
4. Vue優點
虛擬DOM把最終的DOM操作計算出來並優化,由於這個DOM操作屬於預處理操作,並沒有真實的操作DOM,所以叫做虛擬DOM。最後在計算完畢才真正將DOM操作提交,將DOM操作變化反映到DOM樹上
看起來說的很厲害,其實也沒說到點上。關於虛擬 DOM 的優缺點,直接看 Vue 作者尤雨溪本人的知乎回答,你會對它有進一步的理解:
網上都說操作真實 DOM 慢,但測試結果卻比 React 更快,為什麼?
雙向資料繫結通過MVVM思想實現資料的雙向繫結,讓開發者不用再操作dom物件,有更多的時間去思考業務邏輯
開發者不操作dom物件,和雙向繫結沒太大關係。React不提供雙向繫結,開發者照樣不需要操作dom。雙向繫結只是一種語法糖,在表單元素上繫結value
並且監聽onChange
事件去修改value
觸發響應式更新。
我建議真正想看模板被編譯後的原理的同學,可以去尤大開源的vue-template-explorer網站輸入對應的模板,就會展示出對應的 render 函式。
執行速度更快,像比較與react而言,同樣都是操作虛擬dom,就效能而言,vue存在很大的優勢
為什麼快,快在哪裡,什麼情況下快,有資料支援嗎?事實上在初始化資料量不同的場景是不好比較的,React
不需要對資料遞迴的進行響應式定義
。
而在更新的場景下Vue
可能更快一些,因為Vue
的更新粒度是元件級別的,而React
是遞歸向下的進行reconciler
,React
引入了Fiber
架構和非同步更新,目的也是為了讓這個工作可以分在不同的時間片
中進行,不要去阻塞使用者高優先順序的操作。
Proxy是es6提供的新特性,相容性不好,所以導致Vue3一致沒有正式釋出讓開發者使用
Vue3 沒釋出不是因為相容性不好,工作正在有序推進中,新的語法也在不斷迭代,並且釋出rfc
徵求社群意見。
Object.defineProperty的缺點:無法監控到陣列下標的變化,導致直接通過陣列的下標給陣列設定值,不能實時響應
事實上可以,並且尤大說只是為了效能的權衡才不去監聽。陣列下標本質上也就是物件的一個屬性。
5. React和Vue的比較
React預設是通過比較引用的方式(diff)進行的,React不精確監聽資料變化。
比較引用和diff
有什麼關係,難道 Vue 就不diff
了嗎。
Vue2.0可以通過props實現雙向繫結,用vuex單向資料流的狀態管理框架
雙向繫結是v-model
吧。
Vue 父元件通過props向子元件傳遞資料或回撥
Vue 雖然可以傳遞迴調,但是一般來說還是通過v-on:change
或者@change
的方式去繫結事件吧,這和回撥是兩套機制。
模板渲染方式不同,Vue通過HTML進行渲染
事實上 Vue 是自己實現了一套模板引擎系統,HTML
可以被利用為模板的而已,你在.vue
檔案裡寫的template
和HTML
本質上沒有關係。
React組合不同功能方式是通過HoC(高階元件),本質是高階函式
事實上高階函式只是社群提出的一種方案被 React 所採納而已,其他的方案還有renderProps
和 最近流行的Hook
Vue 也可以利用高階函式實現組合和複用。
6. diff演算法的時間複雜度
兩個數的完全的diff演算法是一個時間複雜度為o(n3), Vue進行了優化O(n3)複雜度的問題轉換成O(n)複雜度的問題(只比較同級不考慮跨級問題)在前端當中,你很少會跨級層級地移動Dom元素,所以Virtual Dom只會對同一個層級地元素進行對比
聽這個描述來說,React 沒有對O(n3)
的複雜度進行優化?事實上 React 和 Vue 都只會對tag
相同的同級節點進行diff
,如果不同則直接銷燬重建,都是O(n)
的複雜度。
7. 談談你對作用域插槽的理解
單個插槽當子元件模板只有一個沒有屬性的插槽時, 父元件傳入的整個內容片段將插入到插槽所在的 DOM 位置, 並替換掉插槽標籤本身。
跟 DOM 沒關係,是在虛擬節點樹的插槽位置替換。
如果不加key,那麼vue會選擇複用節點(Vue的就地更新策略),導致之前節點的狀態被保留下來,會產生一系列的bug
不加 key 也不一定就會複用,關於 diff 和 key 的使用,建議大家還是找一些非造玩具的文章真正深入的看一下原理。
為什麼 Vue 中不要用 index 作為 key?(diff 演算法詳解)
8. 元件中的data為什麼是函式
因為元件是用來複用的,JS裡物件是引用關係,這樣作用域沒有隔離,而new Vue的例項,是不會被複用的,因此不存在引用物件問題
這句話反正我壓根沒聽懂,事實上如果元件裡 data 直接寫了一個物件的話,那麼如果你在模板中多次宣告這個元件,元件中的 data 會指向同一個引用。
此時如果在某個元件中對 data 進行修改,會導致其他元件裡的 data 也被汙染。 而如果使用函式的話,每個元件裡的 data 會有單獨的引用,這個問題就可以避免了。
這個問題我同樣舉個例子來方便理解,假設我們有這樣的一個元件,其中的 data 直接使用了物件而不是函式:
var Counter = { template: `<span @click="count++"></span>` data: { count: 0 } }
注意,這裡的Counter.data
是一個引用,也就是它是在當前的執行環境下全域性唯一
的,它在堆記憶體中佔用了一部分空間。
然後我們在模板中呼叫兩次Counter
元件:
<div> <Counter id="a" /> <Counter id="b" /> </div>
我們從原理出發,先看看它被編譯成什麼樣的render
函式:
function render() { with(this) { return _c('div', [_c('Counter'), _c('Counter')], 1) } }
每一個Counter
會被_c
所呼叫,也就是createElement
,想象一下createElement
內部會發生什麼,它會直接拿著Counter
上的data
這個引用去建立一個元件。 也就是所有的Counter
元件例項上的data
都指向同一個引用。
此時假如 id 為 a 的 Counter 元件內部呼叫了count++
,會去對data
這個引用上的 count 屬性賦值,那麼此時由於 id 為 b 的 Counter 元件內部也是引用的同一份 data,它也會感覺到變化而更新元件,這就造成了多個元件之間的資料混亂了。
9. computed和watch有什麼區別
計算屬性是基於他們的響應式依賴進行快取的,只有在依賴發生變化時,才會計算求值,而使用 methods,每次都會執行相應的方法
這也是一個一問就倒的回答,依賴變化是計算屬性就重新求值嗎?中間經歷了什麼過程,為什麼說computed
是有快取值的?隨便挑一個點深入問下去就站不住。 事實上computed
會擁有自己的watcher
,它內部有個屬性dirty
開關來決定computed
的值是需要重新計算還是直接複用之前的值。
以這樣的一個例子來說:
computed: { sum() { return this.count + 1 } }
首先明確兩個關鍵字:
「dirty」 從字面意義來講就是髒
的意思,這個開關開啟了,就意味著這個資料是髒資料,需要重新求值了拿到最新值。
「求值」 的意思的對使用者傳入的函式進行執行,也就是執行return this.count + 1
- 在
sum
第一次進行求值的時候會讀取響應式屬性count
,收集到這個響應式資料作為依賴。並且計算出一個值來儲存在自身的value
上,把dirty
設為 false,接下來在模板裡再訪問sum
就直接返回這個求好的值value
,並不進行重新的求值。 - 而
count
發生變化了以後會通知sum
所對應的watcher
把自身的dirty
屬性設定成 true,這也就相當於把重新求值的開關開啟來了。這個很好理解,只有count
變化了,sum
才需要重新去求值。 - 那麼下次模板中再訪問到
this.sum
的時候,才會真正的去重新呼叫sum
函式求值,並且再次把dirty
設定為 false,等待下次的開啟……
後續我會考慮單獨出一篇文章進行詳細講解。
10. Watch中的deep:true是如何實現的
當用戶指定了watch中的deep屬性為true時,如果當前監控的值是陣列型別,會對物件中的每一項進行求值,此時會將當前watcher存入到對應屬性的依賴中,這樣陣列中的物件發生變化時也會通知資料更新。
不光是陣列型別,物件型別也會對深層屬性進行依賴收集
,比如監聽了obj
,假如設定了deep: true
,那麼對obj.a.b.c = 5
這樣深層次的修改也一樣會觸發 watch 的回撥函式。本質上是因為 Vue 內部對設定了deep
的 watch,會進行遞迴的訪問
(只要此屬性也是響應式屬性
),而在此過程中也會不斷髮生依賴收集。
在回答這道題的時候,同樣也要考慮到遞迴收集依賴
對效能上的損耗和權衡,才是一份合格的回答。
11. action和mutation區別
mutation是同步更新資料(內部會進行是否為非同步方式更新資料的檢測)
內部並不能檢測到是否非同步更新,而是例項上有一個開關變數_committing
,
- 只有在 mutation 執行之前才會把開關開啟,允許修改 state 上的屬性。
- 並且在 mutation 同步執行完成後立刻關閉。
-
非同步更新的話由於已經出了
mutation
的呼叫棧,此時的開關已經是關上的,自然能檢測到對 state 的修改並報錯。具體可以檢視原始碼中的withCommit
函式。這是一種很經典對於js單執行緒機制
的利用。Store.prototype._withCommit = function _withCommit (fn) { var committing = this._committing; this._committing = true; fn(); this._committing = committing; };
歡迎關注前端早茶,與廣東靚仔攜手共同進階
前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~