幾個(gè)關(guān)于vue的面試題

在上一家公司的時(shí)候,我需要負(fù)責(zé)面試一些前端同學(xué)。收到過很多份以 Vue 為主要技術(shù)棧的同學(xué)的簡歷,毫無例外的是簡歷里寫著熟悉 Vue,甚至熟悉源碼。
但是仔細(xì)聊下來,發(fā)現(xiàn)很多人可能只是看了一些比較淺層的面經(jīng)總結(jié),這樣雖然也能答出一部分,但是如果面試官真的有去閱讀過 Vue 的源碼,就比較難通過考核了。
當(dāng)然,現(xiàn)在有個(gè)不太好的現(xiàn)象就是 Vue 的面試整體比較「內(nèi)卷」,就算你只是寫熟悉,面試官也很可能會(huì)問你原理方面的東西,假設(shè)你完全沒了解過原理,那么可能會(huì)被一部分面試官貼上對技術(shù)熱情不夠的標(biāo)簽。
我猜測原因是社區(qū)里 Vue 源碼解讀的文章或視頻做的比較到位,對這個(gè)現(xiàn)象我不做過多的評論。總而言之,為了修煉內(nèi)功而去學(xué)源碼,是不會(huì)吃虧的。
下面我通過摘錄一些社區(qū)里回答的比較淺顯的面試答案,來模擬一次不太令人滿意的 Vue 面試場景。
?? 引用部分是模擬候選人的簡略版回答。
?? 正文部分是筆者的回應(yīng)。
友情提示,文章中寶藏鏈接過多,很難一口氣消化。
為了不迷路,建議關(guān)注公眾號,點(diǎn)頂部「前端從進(jìn)階到入院」。
我為你精心挑選了「Vue 進(jìn)階精選」專題文章,幫你逐個(gè)擊破難點(diǎn)。
請說一下響應(yīng)式數(shù)據(jù)的原理
??默認(rèn) Vue 在初始化數(shù)據(jù)時(shí),會(huì)給 data 中的屬性使用 Object.defineProperty 重新定義所有屬性,當(dāng)頁面到對應(yīng)屬性時(shí),會(huì)進(jìn)行依賴收集(收集當(dāng)前組件中的 watcher)如果屬性發(fā)生變化會(huì)通知相關(guān)依賴進(jìn)行更新操作
收集當(dāng)前組件中的 watcher,我會(huì)進(jìn)一步問你什么叫當(dāng)前組件的 watcher?我面試時(shí)經(jīng)常聽到這種模糊的說法,感覺就是看了些造玩具的文章就說熟悉響應(yīng)式原理了,起碼的流程要清晰一些:
由于 Vue 執(zhí)行一個(gè)組件的 render函數(shù)是由Watcher去代理執(zhí)行的,Watcher在執(zhí)行前會(huì)把Watcher自身先賦值給Dep.target這個(gè)全局變量,等待響應(yīng)式屬性去收集它這樣在哪個(gè)組件執(zhí)行 render函數(shù)時(shí)訪問了響應(yīng)式屬性,響應(yīng)式屬性就會(huì)精確的收集到當(dāng)前全局存在的Dep.target作為自身的依賴在響應(yīng)式屬性發(fā)生更新時(shí)通知 Watcher去重新調(diào)用vm._update(vm._render())進(jìn)行組件的視圖更新
關(guān)于這個(gè)問題,有一個(gè)比較有意思的經(jīng)歷是,有一位同學(xué)前面部分都答得很好,但是我問他 watcher 是利用了什么數(shù)據(jù)結(jié)構(gòu)去存儲(chǔ)的時(shí)候,他就不太能答得出來了。所以是否真的閱讀過源碼,可以通過類似只要你看過,就一定印象深刻的細(xì)節(jié)來試探。
響應(yīng)式部分,如果你想在簡歷上寫熟悉的話,還是要抽時(shí)間好好的去看一下源碼中真正的實(shí)現(xiàn),而不是看這種模棱兩可的說法就覺得自己熟練掌握了。
為什么 Vue 采用異步渲染
??因?yàn)槿绻徊捎卯惒礁拢敲疵看胃聰?shù)據(jù)都會(huì)對當(dāng)前租金按進(jìn)行重新渲染,所以為了性能考慮,Vue 會(huì)在本輪數(shù)據(jù)更新后,再去異步更新數(shù)據(jù)
什么叫本輪數(shù)據(jù)更新后,再去異步更新數(shù)據(jù)?
輪指的是什么,在 eventLoop 里的 task 和 microTask,他們分別的執(zhí)行時(shí)機(jī)是什么樣的,為什么優(yōu)先選用 microTask,這都是值得深思的好問題。
建議看看這篇文章:Vue源碼詳解之nextTick:MutationObserver只是浮云,microtask才是核心![1]
nextTick 實(shí)現(xiàn)原理
??nextTick 方法主要是使用了宏任務(wù)和微任務(wù),定義一個(gè)異步方法,多次調(diào)用 nextTick 會(huì)將方法存在隊(duì)列中,通過這個(gè)異步方法清空當(dāng)前隊(duì)列。所以這個(gè) nextTick 方法就是異步方法
這句話說的很亂,典型的讓面試官忍不住想要深挖一探究竟的回答。(因?yàn)橐宦犇憔筒皇钦娴亩?/p>
正確的流程應(yīng)該是先去 嗅探環(huán)境,依次去檢測:
Promise 的 then -> ??MutationObserver 的回調(diào)函數(shù) -> ??setImmediate -> ??setTimeout 是否存在,找到存在的就使用它,以此來確定回調(diào)函數(shù)隊(duì)列是以哪個(gè) api 來異步執(zhí)行。
在 nextTick 函數(shù)接受到一個(gè) callback 函數(shù)的時(shí)候,先不去調(diào)用它,而是把它 push 到一個(gè)全局的 queue 隊(duì)列中,等待下一個(gè)任務(wù)隊(duì)列的時(shí)候再一次性的把這個(gè) queue 里的函數(shù)依次執(zhí)行。
這個(gè)隊(duì)列可能是 microTask 隊(duì)列,也可能是 macroTask 隊(duì)列,前兩個(gè) api 屬于微任務(wù)隊(duì)列,后兩個(gè) api 屬于宏任務(wù)隊(duì)列。
簡化實(shí)現(xiàn)一個(gè)異步合并任務(wù)隊(duì)列:
let pending = false
// 存放需要異步調(diào)用的任務(wù)
const callbacks = []
function flushCallbacks() {
pending = false
// 循環(huán)執(zhí)行隊(duì)列
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方法 在下一個(gè)微任務(wù)隊(duì)列中把函數(shù)全部執(zhí)行
// 在微任務(wù)開始之前 依然可以往callbacks里放入新的回調(diào)函數(shù)
Promise.resolve().then(flushCallbacks)
}
}
測試一下:
// 第一次調(diào)用 then方法已經(jīng)被調(diào)用了 但是 flushCallbacks 還沒執(zhí)行
nextTick(() => ??console.log(1))
// callbacks里push這個(gè)函數(shù)
nextTick(() => ??console.log(2))
// callbacks里push這個(gè)函數(shù)
nextTick(() => ??console.log(3))
// 同步函數(shù)優(yōu)先執(zhí)行
console.log(4)
// 此時(shí)調(diào)用棧清空了,瀏覽器開始檢查微任務(wù)隊(duì)列,發(fā)現(xiàn)了 flushCallbacks 方法,執(zhí)行。
// 此時(shí) callbacks 里的 3 個(gè)函數(shù)被依次執(zhí)行。
// 4
// 1
// 2
// 3
Vue 優(yōu)點(diǎn)
??虛擬 DOM 把最終的 DOM 操作計(jì)算出來并優(yōu)化,由于這個(gè) DOM 操作屬于預(yù)處理操作,并沒有真實(shí)的操作 DOM,所以叫做虛擬 DOM。最后在計(jì)算完畢才真正將 DOM 操作提交,將 DOM 操作變化反映到 DOM 樹上
看起來說的很厲害,其實(shí)也沒說到點(diǎn)上。關(guān)于虛擬 DOM 的優(yōu)缺點(diǎn),直接看 Vue 作者尤雨溪本人的知乎回答,你會(huì)對它有進(jìn)一步的理解:
網(wǎng)上都說操作真實(shí) DOM 慢,但測試結(jié)果卻比 React 更快,為什么?[2]
??雙向數(shù)據(jù)綁定通過 MVVM 思想實(shí)現(xiàn)數(shù)據(jù)的雙向綁定,讓開發(fā)者不用再操作 dom 對象,有更多的時(shí)間去思考業(yè)務(wù)邏輯
開發(fā)者不操作 dom 對象,和雙向綁定沒太大關(guān)系。React 不提供雙向綁定,開發(fā)者照樣不需要操作 dom。雙向綁定只是一種語法糖,在表單元素上綁定 value 并且監(jiān)聽 onChange 事件去修改 value 觸發(fā)響應(yīng)式更新。
我建議真正想看模板被編譯后的原理的同學(xué),可以去尤大開源的 vue-template-explorer[3] 網(wǎng)站輸入對應(yīng)的模板,就會(huì)展示出對應(yīng)的 render 函數(shù)。
??運(yùn)行速度更快,像比較與 react 而言,同樣都是操作虛擬 dom,就性能而言,vue 存在很大的優(yōu)勢
為什么快,快在哪里,什么情況下快,有數(shù)據(jù)支持嗎?事實(shí)上在初始化數(shù)據(jù)量不同的場景是不好比較的,React 不需要對數(shù)據(jù)遞歸的進(jìn)行 響應(yīng)式定義。
而在更新的場景下 Vue 可能更快一些,因?yàn)?Vue 的更新粒度是組件級別的,而 React 是遞歸向下的進(jìn)行 reconciler,React 引入了 Fiber 架構(gòu)和異步更新,目的也是為了讓這個(gè)工作可以分在不同的 時(shí)間片 中進(jìn)行,不要去阻塞用戶高優(yōu)先級的操作。
??Proxy 是 es6 提供的新特性,兼容性不好,所以導(dǎo)致 Vue3 一致沒有正式發(fā)布讓開發(fā)者使用
Vue3 沒發(fā)布不是因?yàn)榧嫒菪圆缓茫ぷ髡谟行蛲七M(jìn)中,新的語法也在不斷迭代,并且發(fā)布 rfc 征求社區(qū)意見。
??Object.defineProperty 的缺點(diǎn):無法監(jiān)控到數(shù)組下標(biāo)的變化,導(dǎo)致直接通過數(shù)組的下標(biāo)給數(shù)組設(shè)置值,不能實(shí)時(shí)響應(yīng)
事實(shí)上可以,并且尤大說只是為了性能的權(quán)衡才不去監(jiān)聽[4]。數(shù)組下標(biāo)本質(zhì)上也就是對象的一個(gè)屬性。
React 和 Vue 的比較
??React 默認(rèn)是通過比較引用的方式(diff)進(jìn)行的,React 不精確監(jiān)聽數(shù)據(jù)變化。
比較引用和 diff 有什么關(guān)系,難道 Vue 就不 diff 了嗎。
??Vue2.0 可以通過 props 實(shí)現(xiàn)雙向綁定,用 vuex 單向數(shù)據(jù)流的狀態(tài)管理框架
雙向綁定是 v-model 吧,面試的時(shí)候不要把雙向綁定和響應(yīng)式數(shù)據(jù)給搞混。
??Vue 父組件通過 props 向子組件傳遞數(shù)據(jù)或回調(diào)
Vue 雖然可以傳遞回調(diào),但是一般來說還是通過 @change 這樣的方式去綁定事件吧,這和回調(diào)是兩套機(jī)制。深入的話可以從 Vue 內(nèi)部實(shí)現(xiàn)的 eventEmitter 事件總線機(jī)制來回答。
??模板渲染方式不同,Vue 通過 HTML 進(jìn)行渲染
事實(shí)上 Vue 是自己實(shí)現(xiàn)了一套模板引擎系統(tǒng),HTML 可以被利用為模板的而已,你在 .vue 文件里寫的 template 和 HTML 本質(zhì)上沒有關(guān)系。
??React 組合不同功能方式是通過 HoC(高階組件),本質(zhì)是高階函數(shù)
事實(shí)上高階函數(shù)只是社區(qū)提出的一種方案被 React 所采納而已,其他的方案還有 renderProps 和 最近流行的Hook
Vue 也可以利用高階函數(shù) 實(shí)現(xiàn)組合和復(fù)用。
diff 算法的時(shí)間復(fù)雜度
??兩個(gè)數(shù)的完全的 diff 算法是一個(gè)時(shí)間復(fù)雜度為 o(n3), ??Vue 進(jìn)行了優(yōu)化 O(n3)復(fù)雜度的問題轉(zhuǎn)換成 O(n)復(fù)雜度的問題(只比較同級不考慮跨級問題)在前端當(dāng)中,你很少會(huì)跨級層級地移動(dòng) Dom 元素,所以 Virtual Dom 只會(huì)對同一個(gè)層級地元素進(jìn)行對比
聽這個(gè)描述來說,React 沒有對 O(n3) 的復(fù)雜度進(jìn)行優(yōu)化?事實(shí)上 React 和 Vue 都只會(huì)對 tag 相同的同級節(jié)點(diǎn)進(jìn)行 diff,如果不同則直接銷毀重建,都是 O(n) 的復(fù)雜度。
談?wù)勀銓ψ饔糜虿宀鄣睦斫?/h2>??單個(gè)插槽當(dāng)子組件模板只有一個(gè)沒有屬性的插槽時(shí), 父組件傳入的整個(gè)內(nèi)容片段將插入到插槽所在的 DOM 位置, 并替換掉插槽標(biāo)簽本身。
??單個(gè)插槽當(dāng)子組件模板只有一個(gè)沒有屬性的插槽時(shí), 父組件傳入的整個(gè)內(nèi)容片段將插入到插槽所在的 DOM 位置, 并替換掉插槽標(biāo)簽本身。
跟 DOM 沒關(guān)系,是在虛擬節(jié)點(diǎn)樹的插槽位置替換。
Vue 中 key 的作用
??如果不加 key,那么 vue 會(huì)選擇復(fù)用節(jié)點(diǎn)(Vue 的就地更新策略),導(dǎo)致之前節(jié)點(diǎn)的狀態(tài)被保留下來,會(huì)產(chǎn)生一系列的 bug
不加 key 也不一定就會(huì)復(fù)用,關(guān)于 diff 和 key 的使用,建議大家還是找一些非造玩具的文章真正深入的看一下原理。
為什么 Vue 中不要用 index 作為 key?(diff 算法詳解)
組件中的 data 為什么是函數(shù)
??因?yàn)榻M件是用來復(fù)用的,JS 里對象是引用關(guān)系,這樣作用域沒有隔離,而 new Vue 的實(shí)例,是不會(huì)被復(fù)用的,因此不存在引用對象問題
這句話反正我壓根沒聽懂,事實(shí)上如果組件里 data 直接寫了一個(gè)對象的話,那么如果你在模板中多次聲明這個(gè)組件,組件中的 data 會(huì)指向同一個(gè)引用。
此時(shí)如果在某個(gè)組件中對 data 進(jìn)行修改,會(huì)導(dǎo)致其他組件里的 data 也被污染。而如果使用函數(shù)的話,每個(gè)組件里的 data 會(huì)有單獨(dú)的引用,這個(gè)問題就可以避免了。
這個(gè)問題我同樣舉個(gè)例子來方便理解,假設(shè)我們有這樣的一個(gè)組件,其中的 data 直接使用了對象而不是函數(shù):
var Counter = {
template: `<span @click="count++"></span>`
data: {
count: 0
}
}
注意,這里的 Counter.data 僅僅是一個(gè)對象而已,它 是一個(gè)引用,也就是它是在當(dāng)前的運(yùn)行環(huán)境下全局唯一的,它真正的值在堆內(nèi)存中占用了一部分空間。
也就是說,不管利用這份 data 數(shù)據(jù)創(chuàng)建了多少個(gè)組件實(shí)例,這個(gè)組件實(shí)例內(nèi)部的 data 都指向這一個(gè)唯一的對象。
然后我們在模板中調(diào)用兩次 Counter 組件:
<div>
<Counter id="a" />
<Counter id="b" />
</div>
我們從原理出發(fā),先看看它被編譯成什么樣[5]的 render 函數(shù):
function render() {
with (this) {
return _c('div', [_c('Counter'), _c('Counter')], 1)
}
}
每一個(gè) Counter 會(huì)被 _c 所調(diào)用,也就是 createElement,想象一下 createElement 內(nèi)部會(huì)發(fā)生什么,它會(huì)直接拿著 Counter 上的 data 這個(gè)引用去創(chuàng)建一個(gè)組件。也就是所有的 Counter 組件實(shí)例上的 data 都指向同一個(gè)引用。
此時(shí)假如 id 為 a 的 Counter 組件內(nèi)部調(diào)用了 count++,會(huì)去對 data 這個(gè)引用上的 count 屬性賦值,那么此時(shí)由于 id 為 b 的 Counter 組件內(nèi)部也是引用的同一份 data,它也會(huì)感覺到變化而更新組件,這就造成了多個(gè)組件之間的數(shù)據(jù)混亂了。
那么如果換成函數(shù)的情況呢?每創(chuàng)建一次組件實(shí)例就執(zhí)行一次 data() 函數(shù):
function data() {
return { count: 0 }
}
// 組件a創(chuàng)建一份data
const a = data()
// 組件b創(chuàng)建一份data
const b = data()
a === b // false
是不是一目了然,每個(gè)組件擁有了自己的一份全新的 data,再也不會(huì)互相污染數(shù)據(jù)了。
computed 和 watch 有什么區(qū)別
??計(jì)算屬性是基于他們的響應(yīng)式依賴進(jìn)行緩存的,只有在依賴發(fā)生變化時(shí),才會(huì)計(jì)算求值,而使用 methods,每次都會(huì)執(zhí)行相應(yīng)的方法
這也是一個(gè)一問就倒的回答,依賴變化是計(jì)算屬性就重新求值嗎?中間經(jīng)歷了什么過程,為什么說 computed 是有緩存值的?隨便挑一個(gè)點(diǎn)深入問下去就站不住。事實(shí)上 computed 會(huì)擁有自己的 watcher,它內(nèi)部有個(gè)屬性 dirty 開關(guān)來決定 computed 的值是需要重新計(jì)算還是直接復(fù)用之前的值。
以這樣的一個(gè)例子來說:
computed: {
sum() {
return this.count + 1
}
}
首先明確兩個(gè)關(guān)鍵字:
dirty 從字面意義來講就是 臟 的意思,這個(gè)開關(guān)開啟了,就意味著這個(gè)數(shù)據(jù)是臟數(shù)據(jù),需要重新求值了拿到最新值。
求值 的意思的對用戶傳入的函數(shù)進(jìn)行執(zhí)行,也就是執(zhí)行 sum 這個(gè)函數(shù)。
在 sum第一次進(jìn)行求值的時(shí)候會(huì)讀取響應(yīng)式屬性count,收集到這個(gè)響應(yīng)式數(shù)據(jù)作為依賴。并且計(jì)算出一個(gè)值來保存在自身的value上,把dirty設(shè)為 false,接下來在模板里再訪問sum就直接返回這個(gè)求好的值value,并不進(jìn)行重新的求值。而 count發(fā)生變化了以后會(huì)通知sum所對應(yīng)的watcher把自身的dirty屬性設(shè)置成 true,這也就相當(dāng)于把重新求值的開關(guān)打開來了。這個(gè)很好理解,只有count變化了,sum才需要重新去求值。那么下次模板中再訪問到 this.sum的時(shí)候,才會(huì)真正的去重新調(diào)用sum函數(shù)求值,并且再次把dirty設(shè)置為 false,等待下次的開啟……
具體的原理解析,我在Vue 的計(jì)算屬性如何實(shí)現(xiàn)緩存?這篇文章里很詳細(xì)的講解了。
Watch 中的 deep:true 是如何實(shí)現(xiàn)的
??當(dāng)用戶指定了 watch 中的 deep 屬性為 true 時(shí),如果當(dāng)前監(jiān)控的值是數(shù)組類型,會(huì)對對象中的每一項(xiàng)進(jìn)行求值,此時(shí)會(huì)將當(dāng)前 watcher 存入到對應(yīng)屬性的依賴中,這樣數(shù)組中的對象發(fā)生變化時(shí)也會(huì)通知數(shù)據(jù)更新。
不光是數(shù)組類型,對象類型也會(huì)對深層屬性進(jìn)行 依賴收集,比如deep watch了 obj,那么對 obj.a.b.c = 5 這樣深層次的修改也一樣會(huì)觸發(fā) watch 的回調(diào)函數(shù)。本質(zhì)上是因?yàn)?Vue 內(nèi)部對需要 deep watch 的屬性會(huì)進(jìn)行遞歸的訪問,而在此過程中也會(huì)不斷發(fā)生依賴收集。(只要此屬性也是響應(yīng)式屬性)
在回答這道題的時(shí)候,同樣也要考慮到 遞歸收集依賴 對性能上的損耗和權(quán)衡,這樣才是一份合格的回答。
action 和 mutation 區(qū)別
??mutation 是同步更新數(shù)據(jù)(內(nèi)部會(huì)進(jìn)行是否為異步方式更新數(shù)據(jù)的檢測)
內(nèi)部并不能檢測到是否異步更新,而是實(shí)例上有一個(gè)開關(guān)變量 _committing,
只有在 mutation 執(zhí)行之前才會(huì)把開關(guān)打開,允許修改 state 上的屬性。 并且在 mutation 同步執(zhí)行完成后立刻關(guān)閉。 異步更新的話由于已經(jīng)出了 mutation的調(diào)用棧,此時(shí)的開關(guān)已經(jīng)是關(guān)上的,自然能檢測到對 state 的修改并報(bào)錯(cuò)。具體可以查看源碼中的withCommit函數(shù)。這是一種很經(jīng)典對于js單線程機(jī)制的利用。
Store.prototype._withCommit = function _withCommit(fn) {
var committing = this._committing
this._committing = true
fn()
this._committing = committing
}
總結(jié)
關(guān)于面經(jīng),面經(jīng)其實(shí)是一個(gè)挺不錯(cuò)的文章形式,它可以讓你在不去參與面試的情況下也可以得知目前國內(nèi)的大廠主要在技術(shù)上關(guān)注哪些重點(diǎn)。但是如果你用面經(jīng)下面的簡略的答案去作為你的學(xué)習(xí)材料,那我覺得就本末倒置了。正確的方式是去針對每一個(gè)重難點(diǎn),結(jié)合你自己目前的技術(shù)水平和方向去深入學(xué)習(xí)和研究。
比如面試官問你 Vue 的原理,其實(shí)是想考察你對平常使用的框架是否有探索底層原理的興趣和熱情,相信有這份熱情的人他的技術(shù)積累和潛力一定也不會(huì)差。但是很多人現(xiàn)在為了應(yīng)付面試,就直接按照本文所說的比較水的面試文章里簡略版答案去背,大廠面試官一定會(huì)針對每一個(gè)點(diǎn)深入挖掘,挖到你說不出來為止,這樣真的是很不推薦的一種行為。
如果你真的想掌握好 Vue 的原理,并且作為你簡歷中的一個(gè)亮點(diǎn),那么你就自己打開源碼一點(diǎn)點(diǎn)花時(shí)間去研究。
如果你目前的基礎(chǔ)不夠,那也可以輔助以一些優(yōu)秀的的視頻教程或者文章。我始終覺得,紙上得來終覺淺,如果你不能去深入源碼一步步調(diào)試,你對它的認(rèn)知總歸是比較淺層的。
一些小秘密
參考資料
Vue源碼詳解之nextTick:MutationObserver只是浮云,microtask才是核心!: https://segmentfault.com/a/1190000008589736
[2]網(wǎng)上都說操作真實(shí) DOM 慢,但測試結(jié)果卻比 React 更快,為什么?: https://www.zhihu.com/question/31809713/answer/53544875
[3]vue-template-explorer: https://template-explorer.vuejs.org/
[4]尤大說只是為了性能的權(quán)衡才不去監(jiān)聽: https://segmentfault.com/a/1190000015783546
[5]編譯成什么樣: https://template-explorer.vuejs.org/#%3Cdiv%3E%0A%20%3CCounter%20%2F%3E%0A%20%3CCounter%20%2F%3E%0A%3C%2Fdiv%3E
