前端常見的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 里的 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 是遞歸向下的進行 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 文件里寫的 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

  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;
    };

     

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM