1. 程式人生 > 其它 >前端常見的Vue面試題目彙總

前端常見的Vue面試題目彙總

歡迎關注前端早茶,與廣東靚仔攜手共同進階

前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~

1. 請說一下響應式資料的原理

預設Vue在初始化資料時,會給data中的屬性使用Object.defineProperty重新定義所有屬性,當頁面到對應屬性時,會進行依賴收集(收集當前元件中的watcher)如果屬性發生變化會通知相關依賴進行更新操作

收集當前元件中的watcher,我進一步問你什麼叫當前元件的watcher?我面試時經常聽到這種模糊的說法,感覺就是看了些造玩具的文章就說熟悉響應式原理了,起碼的流程要清晰一些:

  1. 由於 Vue 執行一個元件的render函式是由Watcher去代理執行的,Watcher
    在執行前會把Watcher自身先賦值給Dep.target這個全域性變數,等待響應式屬性去收集它
  2. 這樣在哪個元件執行render函式時訪問了響應式屬性,響應式屬性就會精確的收集到當前全域性存在的Dep.target作為自身的依賴
  3. 在響應式屬性發生更新時通知Watcher去重新呼叫vm._update(vm._render())進行元件的檢視更新

響應式部分,如果你想在簡歷上寫熟悉的話,還是要抽時間好好的去看一下原始碼中真正的實現,而不是看這種模稜兩可的說法就覺得自己熟練掌握了。

2. 為什麼Vue採用非同步渲染

因為如果不採用非同步更新,那麼每次更新資料都會對當前租金按進行重新渲染,所以為了效能考慮,Vue會在本輪資料更新後,再去非同步更新資料

什麼叫本輪資料更新後,再去非同步更新資料?

輪指的是什麼,在eventLoop裡的taskmicroTask,他們分別的執行時機是什麼樣的,為什麼優先選用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是遞歸向下的進行reconcilerReact引入了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檔案裡寫的templateHTML本質上沒有關係。

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

  1. sum第一次進行求值的時候會讀取響應式屬性count,收集到這個響應式資料作為依賴。並且計算出一個值來儲存在自身的value上,把dirty設為 false,接下來在模板裡再訪問sum就直接返回這個求好的值value,並不進行重新的求值。
  2. count發生變化了以後會通知sum所對應的watcher把自身的dirty屬性設定成 true,這也就相當於把重新求值的開關開啟來了。這個很好理解,只有count變化了,sum才需要重新去求值。
  3. 那麼下次模板中再訪問到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

  1. 只有在 mutation 執行之前才會把開關開啟,允許修改 state 上的屬性。
  2. 並且在 mutation 同步執行完成後立刻關閉。
  3. 非同步更新的話由於已經出了mutation的呼叫棧,此時的開關已經是關上的,自然能檢測到對 state 的修改並報錯。具體可以檢視原始碼中的withCommit函式。這是一種很經典對於js單執行緒機制的利用。

    Store.prototype._withCommit = function _withCommit (fn) {
    var committing = this._committing;
    this._committing = true;
    fn();
    this._committing = committing;
    };

歡迎關注前端早茶,與廣東靚仔攜手共同進階

前端早茶專注前端,一起結伴同行,緊跟業界發展步伐~