天天看點

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

本着對社群的小夥伴們負責的态度,有些文章裡應付面試用的一些講解實在是看不下去。

本文針對 @小明同學喲 的 《前端常見的Vue面試題目彙總》 這篇文章,提出一些錯誤。

先放一張大圖,有興趣的同學可以點開圖檔看一下原文,簡單來說就是寫了很多不知道從哪裡收集來的劣質總結,然後底下放個公衆号騙粉絲。

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

且不說原文中每個答案都過于簡略,并不能達到面試官的要求,其中還有很多錯誤的地方會誤導讀者,接下來我重點指出一下錯誤的地方。

這裡不放原文連結的原因是我希望抵制這樣的作者,這個作者的掘力值快要 5000 了,而掘金會對掘力值 5000 以上的作者進行文章首頁推薦。如果以後首頁都是這樣的低品質文章,那真的很讓人絕望。

另外比較可笑的是,昨天在這篇文章下提出了一些反駁的觀點,今早一看這篇文章的評論區,已經被作者删的一幹二淨,隻留下她的「水軍号」的一條評論了。不禁唏噓,直接删掉文章的反對觀點來掩耳盜鈴。

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

準備開始

接下來開始針對作者文章中的觀點進行逐條的反駁,注意「引用」 中的文字的即是作者原文,錯别字我也原樣保留了。

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

預設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())

    進行元件的視圖更新

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

為什麼Vue采用異步渲染

因為如果不采用異步更新,那麼每次更新資料都會對目前租金按進行重新渲染,是以為了性能考慮,Vue會在本輪資料更新後,再去異步更新資料

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

輪指的是什麼,在

eventLoop

裡的

task

microTask

,他們分别的執行時機是什麼樣的,為什麼優先選用

microTask

,這都是值得深思的好問題。

建議看看這篇文章: Vue源碼詳解之nextTick:MutationObserver隻是浮雲,microtask才是核心!

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
複制代碼           

複制

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的缺點:無法監控到數組下标的變化,導緻直接通過數組的下标給數組設定值,不能實時響應

事實上可以,并且尤大說隻是為了性能的權衡才不去監聽。數組下标本質上也就是對象的一個屬性。

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 也可以利用高階函數 實作組合和複用。

diff算法的時間複雜度

兩個數的完全的diff算法是一個時間複雜度為o(n3), Vue進行了優化O(n3)複雜度的問題轉換成O(n)複雜度的問題(隻比較同級不考慮跨級問題)在前端當中,你很少會跨級層級地移動Dom元素,是以Virtual Dom隻會對同一個層級地元素進行對比

聽這個描述來說,React 沒有對

O(n3)

的複雜度進行優化?事實上 React 和 Vue 都隻會對

tag

相同的同級節點進行

diff

,如果不同則直接銷毀重建,都是

O(n)

的複雜度。

談談你對作用域插槽的了解

單個插槽當子元件模闆隻有一個沒有屬性的插槽時, 父元件傳入的整個内容片段将插入到插槽所在的 DOM 位置, 并替換掉插槽标簽本身。

跟 DOM 沒關系,是在虛拟節點樹的插槽位置替換。

如果不加key,那麼vue會選擇複用節點(Vue的就地更新政策),導緻之前節點的狀态被保留下來,會産生一系列的bug

不加 key 也不一定就會複用,關于 diff 和 key 的使用,建議大家還是找一些非造玩具的文章真正深入的看一下原理。

為什麼 Vue 中不要用 index 作為 key?(diff 算法詳解)

元件中的data為什麼是函數

因為元件是用來複用的,JS裡對象是引用關系,這樣作用域沒有隔離,而new Vue的執行個體,是不會被複用的,是以不存在引用對象問題

這句話反正我壓根沒聽懂,事實上如果元件裡 data 直接寫了一個對象的話,那麼如果你在模闆中多次聲明這個元件,元件中的 data 會指向同一個引用。

此時如果在某個元件中對 data 進行修改,會導緻其他元件裡的 data 也被污染。 而如果使用函數的話,每個元件裡的 data 會有單獨的引用,這個問題就可以避免了。

這個問題我同樣舉個例子來友善了解,假設我們有這樣的一個元件,其中的 data 直接使用了對象而不是函數:

var Counter = {
    template: `<span @click="count++"></span>`
    data: {
        count: 0
    }
}
           

複制

注意,這裡的

Counter.data

僅僅是一個對象而已,它 是一個引用,也就是它是在目前的運作環境下

全局唯一

的,它真正的值在堆記憶體中占用了一部分空間。

也就是說,不管利用這份

data

資料建立了多少個元件執行個體,這個元件執行個體内部的

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,它也會感覺到變化而更新元件,這就造成了多個元件之間的資料混亂了。

那麼如果換成函數的情況呢?每建立一次元件執行個體就執行一次

data()

函數:

function data() {
    return { count: 0 }
}

// 元件a建立一份data
const a = data()
// 元件b建立一份data
const b = data()

a === b // false
複制代碼           

複制

是不是一目了然,每個元件擁有了自己的一份全新的

data

,再也不會互相污染資料了。

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,等待下次的開啟……

後續我會考慮單獨出一篇文章進行詳細講解。

Watch中的deep:true是如何實作的

當使用者指定了watch中的deep屬性為true時,如果目前監控的值是數組類型,會對對象中的每一項進行求值,此時會将目前watcher存入到對應屬性的依賴中,這樣數組中的對象發生變化時也會通知資料更新。

不光是數組類型,對象類型也會對深層屬性進行

依賴收集

,比如監聽了

obj

,假如設定了

deep: true

,那麼對

obj.a.b.c = 5

這樣深層次的修改也一樣會觸發 watch 的回調函數。本質上是因為 Vue 内部對設定了

deep

的 watch,會進行

遞歸的通路

(隻要此屬性也是

響應式屬性

),而在此過程中也會不斷發生依賴收集。

在回答這道題的時候,同樣也要考慮到

遞歸收集依賴

對性能上的損耗和權衡,才是一份合格的回答。

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;
};
複制代碼           

複制

關于重複發文章

此外 @小明同學喲 這個作者和 @小夢喲 這兩個作者之間有說不清道不明的關系(之前看好像是情侶頭像,然後經常互動,并且兩個人分别著有《一個湖北女生的總結》、《一個湖北男生的總結》)。

兩個作者之間把同一篇低品質文章來回發,都是那種評論區能指出特别多錯誤的水文。

來波 diff 算法

這是 @小明同學喲 的 《前端面試大廠手寫源碼系列(上)》:

《前端面試大廠手寫源碼系列(上)》

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

這是 @小夢喲 的 《面試時,你被要求手寫常見原理了嗎?》

面試時,你被要求手寫常見原理了嗎?

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

基本上就是順序調換一下,内容完全重複的文章,閱讀量還不低。

關于發課程文章不注明出處

最開始接觸到這個作者,是因為她寫了一篇 《Vue仿餓了麼app項目總結》,我正好在這個項目的作者黃轶老師的群裡,群友非常憤慨的來評論區讨公道後她才在評論區裡聲明這是和慕課網的黃轶老師學習課程後進行的總結。

我可以了解為如果沒人說的話,她就想瞞混過去作為自己的項目了,可惜她不了解行情,這門課早就在幾年前就家喻戶曉,成為 Vue 面試必備的實戰項目了。

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

申請水軍号

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

他們的文章其實挺難獲得好評的,畢竟真的挺水的。但是這個使用者卻時常在他們的文章下搶沙發。點進去仔細一看,隻關注了這倆人,點贊的也全是這倆人……

關于知識變現

我一直覺得知識變現不可恥,這是一個「自媒體」流行的時代,認真輸出自己觀點并且影響他人的人理應獲得自己的收益,我并不覺得這有什麼丢人的,

我在 寫給國中級前端的進階進階指南 這篇文章裡放了幾個掘金小冊的推廣碼,是我認真讀過以後真心想推薦給大家的,這也是掘金官方提供的一種變現機制。我真心不覺得這有什麼不對。知識是有價值的,前提是你不要輸出二手錯誤百出的知識。甚至在大家的公衆号上看到廣告的時候,我也是會心一笑,因為這個作者曾經或「原創」或「轉載」的優質文章給我帶來了很大的收益……

而原作者 @小明同學喲 的水準明顯還不足以給社群的新人一些啟發,甚至我感覺大概相當于某c字開頭的論壇上面充斥着的新手學習筆記,這樣子為了變現而影響社群環境的吃相我就接受不了了。

再不濟,你還可以學習某不願提及姓名的「閏土大叔」,寫些心靈雞湯做一個教父,也一樣可以賺的盆滿缽滿,畢竟人家沒誤導人。隻是人家是真的不會技術,那就曲線救國而已。

總結

總而言之,我關注了這個作者和她的搭檔 @小夢喲 挺久了,不知道這些作者為什麼這麼拼命的想火起來,不惜重複發文章,不惜借用别人的課程成果而不聲明,這對社群的進步來說沒有任何好處。

關于面經,面經其實是一個挺不錯的文章形式,它可以讓你在不去參與面試的情況下也可以得知目前國内的大廠主要在技術上關注哪些重點。但是如果你用面經下面的簡略的答案去作為你的學習材料,那我覺得就本末倒置了。正确的方式是去針對每一個重難點,結合你自己目前的技術水準和方向去深入學習和研究。

比如面試官問你 Vue 的原理,其實是想考察你對平常使用的架構是否有探索底層原理的興趣和熱情,相信有這份熱情的人他的技術積累和潛力一定也不會差。但是很多人現在為了應付面試,就直接按照本文所說的《前端常見的Vue面試題目彙總》這種文章一個個

簡略版答案

去背(何況大廠面試官一定會針對每一個點深入挖掘,挖到你說不出來為止),這樣真的是很不推薦的一種行為。

如果你真的想掌握好 Vue 的原理,并且作為你履歷中的一個亮點,那麼你就自己打開源碼一點點花時間去研究,如果你目前的基礎不夠,那也可以輔助以一些優秀的的視訊教程或者文章。但是我始終覺得,紙上得來終覺淺,如果你不能去深入源碼一點點調試,你對它的認知總歸是比較淺層的。

我堅持在掘金發文章其實有一個原因,就是我也希望中文社群能慢慢發展出類似

medium

那樣高品質的前端交流社群(雖然它是付費制的,有難度),而掘金是我前端最開始就接觸到的社群,心裡也很有感情,看着首頁混雜着這種錯誤百出的低品質文章,我心裡真的是百感交集,為什麼明明是很多未經考證,甚至連自己都說服不了的觀點,也要整理成文章也要急着發出來吸引流量呢?

總之,真心希望掘金能少一些不負責任的水文,一些摘抄搬運官方文檔的東西。大家都認真的輸出自己去證明過,或者真正了解的總結,慢慢的讓掘金、甚至國内的前端氛圍能夠形成一個良性氛圍,前端的明天越來越美好。