20+個Vue經典面試題(附原始碼級詳解)

2022-05-18 13:00:24
本篇文章給大家總結分享20+個經典面試題(附原始碼級詳解),帶你梳理基礎知識,增強Vue知識儲備,值得收藏,快來看看吧!

01-Vue元件之間通訊方式有哪些

vue是元件化開發框架,所以對於vue應用來說元件間的資料通訊非常重要。 此題主要考查大家vue基本功,對於vue基礎api運用熟練度。 另外一些邊界知識如provide/inject/$attrs則提現了面試者的知識廣度。


元件傳參的各種方式

1.png


思路分析:

  • 總述知道的所有方式

  • 按元件關係闡述使用場景


回答範例:

1、元件通訊常用方式有以下8種:

  • props
  • $emit/$on
  • $children/$parent
  • $attrs/$listeners
  • ref
  • $root
  • eventbus
  • vuex

注意vue3中廢棄的幾個API

https://v3-migration.vuejs.org/breaking-changes/children.html

https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html

https://v3-migration.vuejs.org/breaking-changes/events-api.html#overview


2、根據元件之間關係討論元件通訊最為清晰有效

  • 父子元件

    • props/$emit/$parent/ref/$attrs
  • 兄弟元件

    • $parent/$root/eventbus/vuex
  • 跨層級關係

    • eventbus/vuex/provide+inject

02-v-if和v-for哪個優先順序更高?

分析:

此題考查常識,檔案中曾有詳細說明v2|v3;也是一個很好的實踐題目,專案中經常會遇到,能夠看出面試者api熟悉程度和應用能力。


思路分析:

  • 先給出結論

  • 為什麼是這樣的,說出細節

  • 哪些場景可能導致我們這樣做,該怎麼處理

  • 總結,拔高


回答範例:

  • 實踐中不應該把v-for和v-if放一起

  • vue2中v-for的優先順序是高於v-if,把它們放在一起,輸出的渲染函數中可以看出會先執行迴圈再判斷條件,哪怕我們只渲染列表中一小部分元素,也得在每次重渲染的時候遍歷整個列表,這會比較浪費;另外需要注意的是在vue3中則完全相反,v-if的優先順序高於v-for,所以v-if執行時,它呼叫的變數還不存在,就會導致異常

  • 通常有兩種情況下導致我們這樣做:

    • 為了過濾列表中的專案 (比如 v-for="user in users" v-if="user.isActive")。此時定義一個計算屬性 (比如 activeUsers),讓其返回過濾後的列表即可(比如users.filter(u=>u.isActive))。

    • 為了避免渲染本應該被隱藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers")。此時把 v-if 移動至容器元素上 (比如 ulol)或者外面包一層template即可。

  • 檔案中明確指出永遠不要把 v-ifv-for 同時用在同一個元素上,顯然這是一個重要的注意事項。

  • 原始碼裡面關於程式碼生成的部分,能夠清晰的看到是先處理v-if還是v-for,順序上vue2和vue3正好相反,因此產生了一些症狀的不同,但是不管怎樣都是不能把它們寫在一起的。


知其所以然:

做個測試,test.html兩者同級時,渲染函數如下:

ƒ anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},_l((items),function(item){return (item.isActive)?_c('div',{key:item.id},[_v("\n      "+_s(item.name)+"\n    ")]):_e()}),0)}
}

做個測試,test-v3.html

2.png


原始碼中找答案

v2:https://github1s.com/vuejs/vue/blob/HEAD/src/compiler/codegen/index.js#L65-L66

v3:https://github1s.com/vuejs/core/blob/HEAD/packages/compiler-core/src/codegen.ts#L586-L587


03-簡述 Vue 的生命週期以及每個階段做的事

必問題目,考查vue基礎知識。

思路

  • 給出概念

  • 列舉生命週期各階段

  • 闡述整體流程

  • 結合實踐

  • 擴充套件:vue3變化


回答範例

1、每個Vue元件範例被建立後都會經過一系列初始化步驟,比如,它需要資料觀測,模板編譯,掛載範例到dom上,以及資料變化時更新dom。這個過程中會執行叫做生命週期勾點的函數,以便使用者在特定階段有機會新增他們自己的程式碼。

2、Vue生命週期總共可以分為8個階段:建立前後, 載入前後, 更新前後, 銷燬前後,以及一些特殊場景的生命週期。vue3中新增了三個用於偵錯和伺服器端渲染場景。

生命週期v2生命週期v3描述
beforeCreatebeforeCreate元件範例被建立之初
createdcreated元件範例已經完全建立
beforeMountbeforeMount元件掛載之前
mountedmounted元件掛載到範例上去之後
beforeUpdatebeforeUpdate元件資料發生變化,更新之前
updatedupdated資料資料更新之後
beforeDestroybeforeUnmount元件範例銷燬之前
destroyedunmounted元件範例銷燬之後
生命週期v2生命週期v3描述
activatedactivatedkeep-alive 快取的元件啟用時
deactivateddeactivatedkeep-alive 快取的元件停用時呼叫
errorCapturederrorCaptured捕獲一個來自子孫元件的錯誤時被呼叫
-renderTracked偵錯勾點,響應式依賴被收集時呼叫
-renderTriggered偵錯勾點,響應式依賴被觸發時呼叫
-serverPrefetchssr only,元件範例在伺服器上被渲染前呼叫

3、Vue生命週期流程圖:

3.png

4、結合實踐:

beforeCreate:通常用於外掛開發中執行一些初始化任務

created:元件初始化完畢,可以存取各種資料,獲取介面資料等

mounted:dom已建立,可用於獲取存取資料和dom元素;存取子元件等。

beforeUpdate:此時view層還未更新,可用於獲取更新前各種狀態

updated:完成view層的更新,更新後,所有狀態已是最新

beforeunmount:範例被銷燬前呼叫,可用於一些定時器或訂閱的取消

unmounted:銷燬一個範例。可清理它與其它範例的連線,解綁它的全部指令及事件監聽器


可能的追問

  • setup和created誰先執行?

  • setup中為什麼沒有beforeCreate和created?


知其所以然

vue3中生命週期的派發時刻:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentOptions.ts#L554-L555

vue2中宣告週期的派發時刻:

https://github1s.com/vuejs/vue/blob/HEAD/src/core/instance/init.js#L55-L56


04-能說一說雙向繫結使用和原理嗎?

題目分析:

雙向繫結是vue的特色之一,開發中必然會用到的知識點,然而此題還問了實現原理,升級為深度考查。


思路分析:

  • 給出雙繫結義

  • 雙綁帶來的好處

  • 在哪使用雙綁

  • 使用方式、使用細節、vue3變化

  • 原理實現描述


回答範例:

  • vue中雙向繫結是一個指令v-model,可以繫結一個響應式資料到檢視,同時檢視中變化能改變該值。

  • v-model是語法糖,預設情況下相當於:value@input。使用v-model可以減少大量繁瑣的事件處理程式碼,提高開發效率。

  • 通常在表單項上使用v-model,還可以在自定義元件上使用,表示某個值的輸入和輸出控制。

  • 通過<input v-model="xxx">的方式將xxx的值繫結到表單元素value上;對於checkbox,可以使用true-value和false-value指定特殊的值,對於radio可以使用value指定特殊的值;對於select可以通過options元素的value設定特殊的值;還可以結合.lazy,.number,.trim對v-mode的行為做進一步限定;v-model用在自定義元件上時又會有很大不同,vue3中它類似於sync修飾符,最終展開的結果是modelValue屬性和update:modelValue事件;vue3中我們甚至可以用引數形式指定多個不同的繫結,例如v-model:foo和v-model:bar,非常強大!

  • v-model是一個指令,它的神奇魔法實際上是vue的編譯器完成的。我做過測試,包含v-model的模板,轉換為渲染函數之後,實際上還是是value屬性的繫結以及input事件監聽,事件回撥函數中會做相應變數更新操作。編譯器根據表單元素的不同會展開不同的DOM屬性和事件對,比如text型別的input和textarea會展開為value和input事件;checkbox和radio型別的input會展開為checked和change事件;select用value作為屬性,用change作為事件。


可能的追問:

  • v-modelsync修飾符有什麼區別

  • 自定義元件使用v-model如果想要改變事件名或者屬性名應該怎麼做


知其所以然:

測試程式碼,test.html

觀察輸出的渲染函數:

// <input type="text" v-model="foo">
_c('input', { 
  directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }], 
  attrs: { "type": "text" }, 
  domProps: { "value": (foo) }, 
  on: { 
    "input": function ($event) { 
      if ($event.target.composing) return; 
      foo = $event.target.value 
    } 
  } 
})
// <input type="checkbox" v-model="bar">
_c('input', { 
  directives: [{ name: "model", rawName: "v-model", value: (bar), expression: "bar" }], 
  attrs: { "type": "checkbox" }, 
  domProps: { 
    "checked": Array.isArray(bar) ? _i(bar, null) > -1 : (bar) 
  }, 
  on: { 
    "change": function ($event) { 
      var $$a = bar, $$el = $event.target, $$c = $$el.checked ? (true) : (false); 
      if (Array.isArray($$a)) { 
        var $$v = null, $$i = _i($$a, $$v); 
        if ($$el.checked) { $$i < 0 && (bar = $$a.concat([$$v])) } 
        else { 
          $$i > -1 && (bar = $$a.slice(0, $$i).concat($$a.slice($$i + 1))) } 
      } else { 
        bar = $$c 
      } 
    } 
  } 
})
// <select v-model="baz">
//     <option value="vue">vue</option>
//     <option value="react">react</option>
// </select>
_c('select', { 
  directives: [{ name: "model", rawName: "v-model", value: (baz), expression: "baz" }], 
  on: { 
    "change": function ($event) { 
      var $$selectedVal = Array.prototype.filter.call(
        $event.target.options, 
        function (o) { return o.selected }
      ).map(
        function (o) { 
          var val = "_value" in o ? o._value : o.value; 
          return val 
        }
      ); 
      baz = $event.target.multiple ? $$selectedVal : $$selectedVal[0] 
    } 
  } 
}, [
  _c('option', { attrs: { "value": "vue" } }, [_v("vue")]), _v(" "), 
  _c('option', { attrs: { "value": "react" } }, [_v("react")])
])

05-Vue中如何擴充套件一個元件

此題屬於實踐題,考察大家對vue常用api使用熟練度,答題時不僅要列出這些解決方案,同時最好說出他們異同。

答題思路:

  • 按照邏輯擴充套件和內容擴充套件來列舉,

    • 邏輯擴充套件有:mixins、extends、composition api;

    • 內容擴充套件有slots;

  • 分別說出他們使用方法、場景差異和問題。

  • 作為擴充套件,還可以說說vue3中新引入的composition api帶來的變化


回答範例:

  • 常見的元件擴充套件方法有:mixins,slots,extends等

  • 混入mixins是分發 Vue 元件中可複用功能的非常靈活的方式。混入物件可以包含任意元件選項。當元件使用混入物件時,所有混入物件的選項將被混入該元件本身的選項。

    // 複用程式碼:它是一個設定物件,選項和元件裡面一樣
    const mymixin = {
       methods: {
          dosomething(){}
       }
    }
    // 全域性混入:將混入物件傳入
    Vue.mixin(mymixin)
    
    // 區域性混入:做陣列項設定到mixins選項,僅作用於當前元件
    const Comp = {
       mixins: [mymixin]
    }
  • 插槽主要用於vue元件中的內容分發,也可以用於元件擴充套件。

    子元件Child

    <div>
      <slot>這個內容會被父元件傳遞的內容替換</slot>
    </div>

    父元件Parent

    <div>
       <Child>來自老爹的內容</Child>
    </div>

    如果要精確分發到不同位置可以使用具名插槽,如果要使用子元件中的資料可以使用作用域插槽。

  • 元件選項中還有一個不太常用的選項extends,也可以起到擴充套件元件的目的

    // 擴充套件物件
    const myextends = {
       methods: {
          dosomething(){}
       }
    }
    // 元件擴充套件:做陣列項設定到extends選項,僅作用於當前元件
    // 跟混入的不同是它只能擴充套件單個物件
    // 另外如果和混入發生衝突,該選項優先順序較高,優先起作用
    const Comp = {
       extends: myextends
    }
  • 混入的資料和方法不能明確判斷來源且可能和當前元件內變數產生命名衝突,vue3中引入的composition api,可以很好解決這些問題,利用獨立出來的響應式模組可以很方便的編寫獨立邏輯並提供響應式的資料,然後在setup選項中組合使用,增強程式碼的可讀性和維護性。例如:

    // 複用邏輯1
    function useXX() {}
    // 複用邏輯2
    function useYY() {}
    // 邏輯組合
    const Comp = {
       setup() {
          const {xx} = useXX()
          const {yy} = useYY()
          return {xx, yy}
       }
    }

可能的追問

Vue.extend方法你用過嗎?它能用來做元件擴充套件嗎?


知其所以然

mixins原理:

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiCreateApp.ts#L232-L233

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentOptions.ts#L545

slots原理:

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentSlots.ts#L129-L130

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1373-L1374

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/renderSlot.ts#L23-L24


06-子元件可以直接改變父元件的資料麼,說明原因

分析

這是一個實踐知識點,元件化開發過程中有個單項資料流原則,不在子元件中修改父元件是個常識問題。

參考檔案:https://staging.vuejs.org/guide/components/props.html#one-way-data-flow


思路

  • 講講單項資料流原則,表明為何不能這麼做

  • 舉幾個常見場景的例子說說解決方案

  • 結合實踐講講如果需要修改父元件狀態應該如何做


回答範例

  • 所有的 prop 都使得其父子之間形成了一個單向下行繫結:父級 prop 的更新會向下流動到子元件中,但是反過來則不行。這樣會防止從子元件意外變更父級元件的狀態,從而導致你的應用的資料流向難以理解。另外,每次父級元件發生變更時,子元件中所有的 prop 都將會重新整理為最新的值。這意味著你應該在一個子元件內部改變 prop。如果你這樣做了,Vue 會在瀏覽器控制檯中發出警告。

    const props = defineProps(['foo'])
    // ❌ 下面行為會被警告, props是唯讀的!
    props.foo = 'bar'
  • 實際開發過程中有兩個場景會想要修改一個屬性:

    • **這個 prop 用來傳遞一個初始值;這個子元件接下來希望將其作為一個原生的 prop 資料來使用。**在這種情況下,最好定義一個原生的 data,並將這個 prop 用作其初始值:

      const props = defineProps(['initialCounter'])
      const counter = ref(props.initialCounter)
    • **這個 prop 以一種原始的值傳入且需要進行轉換。**在這種情況下,最好使用這個 prop 的值來定義一個計算屬性:

      const props = defineProps(['size'])
      // prop變化,計算屬性自動更新
      const normalizedSize = computed(() => props.size.trim().toLowerCase())
  • 實踐中如果確實想要改變父元件屬性應該emit一個事件讓父元件去做這個變更。注意雖然我們不能直接修改一個傳入的物件或者陣列型別的prop,但是我們還是能夠直接改內嵌的物件或屬性。


07-Vue要做許可權管理該怎麼做?控制到按鈕級別的許可權怎麼做?

分析

綜合實踐題目,實際開發中經常需要面臨許可權管理的需求,考查實際應用能力。

許可權管理一般需求是兩個:頁面許可權和按鈕許可權,從這兩個方面論述即可。

4.png


思路

  • 許可權管理需求分析:頁面和按鈕許可權

  • 許可權管理的實現方案:分後端方案和前端方案闡述

  • 說說各自的優缺點


回答範例

  • 許可權管理一般需求是頁面許可權按鈕許可權的管理

  • 具體實現的時候分後端和前端兩種方案:

    前端方案會把所有路由資訊在前端設定,通過路由守衛要求使用者登入,使用者登入後根據角色過濾出路由表。比如我會設定一個asyncRoutes陣列,需要認證的頁面在其路由的meta中新增一個roles欄位,等獲取使用者角色之後取兩者的交集,若結果不為空則說明可以存取。此過濾過程結束,剩下的路由就是該使用者能存取的頁面,最後通過router.addRoutes(accessRoutes)方式動態新增路由即可。

    後端方案會把所有頁面路由資訊存在資料庫中,使用者登入的時候根據其角色查詢得到其能存取的所有頁面路由資訊返回給前端,前端再通過addRoutes動態新增路由資訊

    按鈕許可權的控制通常會實現一個指令,例如v-permission將按鈕要求角色通過值傳給v-permission指令,在指令的moutned勾點中可以判斷當前使用者角色和按鈕是否存在交集,有則保留按鈕,無則移除按鈕。

  • 純前端方案的優點是實現簡單,不需要額外許可權管理頁面,但是維護起來問題比較大,有新的頁面和角色需求就要修改前端程式碼重新打包部署;伺服器端方案就不存在這個問題,通過專門的角色和許可權管理頁面,設定頁面和按鈕許可權資訊到資料庫,應用每次登陸時獲取的都是最新的路由資訊,可謂一勞永逸!


知其所以然

路由守衛

https://github1s.com/PanJiaChen/vue-element-admin/blob/HEAD/src/permission.js#L13-L14

路由生成

https://github1s.com/PanJiaChen/vue-element-admin/blob/HEAD/src/store/modules/permission.js#L50-L51

動態追加路由

https://github1s.com/PanJiaChen/vue-element-admin/blob/HEAD/src/permission.js#L43-L44


可能的追問

  • 類似Tabs這類元件能不能使用v-permission指令實現按鈕許可權控制?

    <el-tabs> 
      <el-tab-pane label="⽤戶管理" name="first">⽤戶管理</el-tab-pane> 
    	<el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane>
    </el-tabs>
  • 伺服器端返回的路由資訊如何新增到路由器中?

    // 前端元件名和元件對映表
    const map = {
      //xx: require('@/views/xx.vue').default // 同步的⽅式
      xx: () => import('@/views/xx.vue') // 非同步的⽅式
    }
    // 伺服器端返回的asyncRoutes
    const asyncRoutes = [
      { path: '/xx', component: 'xx',... }
    ]
    // 遍歷asyncRoutes,將component替換為map[component]
    function mapComponent(asyncRoutes) {
      asyncRoutes.forEach(route => {
        route.component = map[route.component];
        if(route.children) {
          route.children.map(child => mapComponent(child))
        }
    	})
    }
    mapComponent(asyncRoutes)

08 - 說一說你對vue響應式理解?

分析

這是一道必問題目,但能回答到位的比較少。如果只是看看一些網文,通常沒什麼底氣,經不住面試官推敲,但像我們這樣即看過原始碼還造過輪子的,回答這個問題就會比較有底氣啦。

答題思路:

  • 啥是響應式?

  • 為什麼vue需要響應式?

  • 它能給我們帶來什麼好處?

  • vue的響應式是怎麼實現的?有哪些優缺點?

  • vue3中的響應式的新變化


回答範例:

  • 所謂資料響應式就是能夠使資料變化可以被檢測並對這種變化做出響應的機制

  • MVVM框架中要解決的一個核心問題是連線資料層和檢視層,通過資料驅動應用,資料變化,檢視更新,要做到這點的就需要對資料做響應式處理,這樣一旦資料發生變化就可以立即做出更新處理。

  • 以vue為例說明,通過資料響應式加上虛擬DOM和patch演演算法,開發人員只需要運算元據,關心業務,完全不用接觸繁瑣的DOM操作,從而大大提升開發效率,降低開發難度。

  • vue2中的資料響應式會根據資料型別來做不同處理,如果是物件則採用Object.defineProperty()的方式定義資料攔截,當資料被存取或發生變化時,我們感知並作出響應;如果是陣列則通過覆蓋陣列物件原型的7個變更方法,使這些方法可以額外的做更新通知,從而作出響應。這種機制很好的解決了資料響應化的問題,但在實際使用中也存在一些缺點:比如初始化時的遞迴遍歷會造成效能損失;新增或刪除屬性時需要使用者使用Vue.set/delete這樣特殊的api才能生效;對於es6中新產生的Map、Set這些資料結構不支援等問題。

  • 為了解決這些問題,vue3重新編寫了這一部分的實現:利用ES6的Proxy代理要響應化的資料,它有很多好處,程式設計體驗是一致的,不需要使用特殊api,初始化效能和記憶體消耗都得到了大幅改善;另外由於響應化的實現程式碼抽取為獨立的reactivity包,使得我們可以更靈活的使用它,第三方的擴充套件開發起來更加靈活了。


知其所以然

vue2響應式:

  • https://github1s.com/vuejs/vue/blob/HEAD/src/core/observer/index.js#L135-L136

vue3響應式:

  • https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/reactive.ts#L89-L90

  • https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/ref.ts#L67-L68


09 - 說說你對虛擬 DOM 的理解?

分析

現有框架幾乎都引入了虛擬 DOM 來對真實 DOM 進行抽象,也就是現在大家所熟知的 VNode 和 VDOM,那麼為什麼需要引入虛擬 DOM 呢?圍繞這個疑問來解答即可!

思路

  • vdom是什麼

  • 引入vdom的好處

  • vdom如何生成,又如何成為dom

  • 在後續的diff中的作用


回答範例

  • 虛擬dom顧名思義就是虛擬的dom物件,它本身就是一個 JavaScript 物件,只不過它是通過不同的屬性去描述一個檢視結構。

  • 通過引入vdom我們可以獲得如下好處:

    將真實元素節點抽象成 VNode,有效減少直接操作 dom 次數,從而提高程式效能

    • 直接操作 dom 是有限制的,比如:diff、clone 等操作,一個真實元素上有許多的內容,如果直接對其進行 diff 操作,會去額外 diff 一些沒有必要的內容;同樣的,如果需要進行 clone 那麼需要將其全部內容進行復制,這也是沒必要的。但是,如果將這些操作轉移到 JavaScript 物件上,那麼就會變得簡單了。
    • 操作 dom 是比較昂貴的操作,頻繁的dom操作容易引起頁面的重繪和迴流,但是通過抽象 VNode 進行中間處理,可以有效減少直接操作dom的次數,從而減少頁面重繪和迴流。

    方便實現跨平臺

    • 同一 VNode 節點可以渲染成不同平臺上的對應的內容,比如:渲染在瀏覽器是 dom 元素節點,渲染在 Native( iOS、Android) 變為對應的控制元件、可以實現 SSR 、渲染到 WebGL 中等等
    • Vue3 中允許開發者基於 VNode 實現自定義渲染器(renderer),以便於針對不同平臺進行渲染。
  • vdom如何生成?在vue中我們常常會為元件編寫模板 - template, 這個模板會被編譯器 - compiler編譯為渲染函數,在接下來的掛載(mount)過程中會呼叫render函數,返回的物件就是虛擬dom。但它們還不是真正的dom,所以會在後續的patch過程中進一步轉化為dom。

5.png

  • 掛載過程結束後,vue程式進入更新流程。如果某些響應式資料發生變化,將會引起元件重新render,此時就會生成新的vdom,和上一次的渲染結果diff就能得到變化的地方,從而轉換為最小量的dom操作,高效更新檢視。


知其所以然

vnode定義:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L127-L128

觀察渲染函數:21-vdom/test-render-v3.html

建立vnode:

  • createElementBlock:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L291-L292

  • createVnode:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L486-L487

  • 首次呼叫時刻:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiCreateApp.ts#L283-L284


mount:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1171-L1172

偵錯mount過程:mountComponent

21-vdom/test-render-v3.html


10 - 你瞭解diff演演算法嗎?

分析

必問題目,涉及vue更新原理,比較考查理解深度。

6.png


思路

  • diff演演算法是幹什麼的

  • 它的必要性

  • 它何時執行

  • 具體執行方式

  • 拔高:說一下vue3中的優化


回答範例

1、Vue中的diff演演算法稱為patching演演算法,它由Snabbdom修改而來,虛擬DOM要想轉化為真實DOM就需要通過patch方法轉換。

2、最初Vue1.x檢視中每個依賴均有更新函數對應,可以做到精準更新,因此並不需要虛擬DOM和patching演演算法支援,但是這樣粒度過細導致Vue1.x無法承載較大應用;Vue 2.x中為了降低Watcher粒度,每個元件只有一個Watcher與之對應,此時就需要引入patching演演算法才能精確找到發生變化的地方並高效更新。

3、vue中diff執行的時刻是元件內響應式資料變更觸發範例執行其更新函數時,更新函數會再次執行render函數獲得最新的虛擬DOM,然後執行patch函數,並傳入新舊兩次虛擬DOM,通過比對兩者找到變化的地方,最後將其轉化為對應的DOM操作。

4、patch過程是一個遞迴過程,遵循深度優先、同層比較的策略;以vue3的patch為例:

  • 首先判斷兩個節點是否為相同同類節點,不同則刪除重新建立
  • 如果雙方都是文字則更新文字內容
  • 如果雙方都是元素節點則遞迴更新子元素,同時更新元素屬性
  • 更新子節點時又分了幾種情況:
    • 新的子節點是文字,老的子節點是陣列則清空,並設定文字;
    • 新的子節點是文字,老的子節點是文字則直接更新文字;
    • 新的子節點是陣列,老的子節點是文字則清空文字,並建立新子節點陣列中的子元素;
    • 新的子節點是陣列,老的子節點也是陣列,那麼比較兩組子節點,更新細節blabla

5、vue3中引入的更新策略:編譯期優化patchFlags、block等


知其所以然

patch關鍵程式碼

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L354-L355

偵錯 test-v3.html


11 - 你知道哪些vue3新特性

分析

官網列舉的最值得注意的新特性:https://v3-migration.vuejs.org/

7.png

也就是下面這些:

  • Composition API
  • SFC Composition API語法糖
  • Teleport傳送門
  • Fragments片段
  • Emits選項
  • 自定義渲染器
  • SFC CSS變數
  • Suspense

以上這些是api相關,另外還有很多框架特性也不能落掉。


回答範例

1、api層面Vue3新特性主要包括:Composition API、SFC Composition API語法糖、Teleport傳送門、Fragments 片段、Emits選項、自定義渲染器、SFC CSS變數、Suspense

2、另外,Vue3.0在框架層面也有很多亮眼的改進:

  • 更快
    • 虛擬DOM重寫
    • 編譯器優化:靜態提升、patchFlags、block等
    • 基於Proxy的響應式系統
  • 更小:更好的搖樹優化
  • 更容易維護:TypeScript + 模組化
  • 更容易擴充套件
    • 獨立的響應化模組
    • 自定義渲染器

知其所以然

體驗編譯器優化

  • https://sfc.vuejs.org/

reactive實現

  • https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/reactive.ts#L90-L91


12 - 怎麼定義動態路由?怎麼獲取傳過來的動態引數?

分析

API題目,考查基礎能力,不容有失,儘可能說的詳細。

思路

  • 什麼是動態路由

  • 什麼時候使用動態路由,怎麼定義動態路由

  • 引數如何獲取

  • 細節、注意事項


回答範例

  • 很多時候,我們需要將給定匹配模式的路由對映到同一個元件,這種情況就需要定義動態路由。

  • 例如,我們可能有一個 User 元件,它應該對所有使用者進行渲染,但使用者 ID 不同。在 Vue Router 中,我們可以在路徑中使用一個動態欄位來實現,例如:{ path: '/users/:id', component: User },其中:id就是路徑引數

  • 路徑引數 用冒號 : 表示。當一個路由被匹配時,它的 params 的值將在每個元件中以 this.$route.params 的形式暴露出來。

  • 引數還可以有多個,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 物件還公開了其他有用的資訊,如 $route.query$route.hash 等。


可能的追問

  • 如何響應動態路由引數的變化

https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#響應路由引數的變化

  • 我們如何處理404 Not Found路由

https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html#捕獲所有路由或-404-not-found-路由


13-如果讓你從零開始寫一個vue路由,說說你的思路

思路分析:

首先思考vue路由要解決的問題:使用者點選跳轉連結內容切換,頁面不重新整理。

  • 藉助hash或者history api實現url跳轉頁面不重新整理
  • 同時監聽hashchange事件或者popstate事件處理跳轉
  • 根據hash值或者state值從routes表中匹配對應component並渲染之

回答範例:

一個SPA應用的路由需要解決的問題是頁面跳轉內容改變同時不重新整理,同時路由還需要以外掛形式存在,所以:

  • 首先我會定義一個createRouter函數,返回路由器範例,範例內部做幾件事:

    • 儲存使用者傳入的設定項
    • 監聽hash或者popstate事件
    • 回撥里根據path匹配對應路由
  • 將router定義成一個Vue外掛,即實現install方法,內部做兩件事:

    • 實現兩個全域性元件:router-link和router-view,分別實現頁面跳轉和內容顯示
    • 定義兩個全域性變數:$route和$router,元件內可以存取當前路由和路由器範例

知其所以然:

  • createRouter如何建立範例

https://github1s.com/vuejs/router/blob/HEAD/src/router.ts#L355-L356

  • 事件監聽

https://github1s.com/vuejs/router/blob/HEAD/src/history/html5.ts#L314-L315

RouterView

  • 頁面跳轉RouterLink

https://github1s.com/vuejs/router/blob/HEAD/src/RouterLink.ts#L184-L185

  • 內容顯示RouterView

https://github1s.com/vuejs/router/blob/HEAD/src/RouterView.ts#L43-L44


14-能說說key的作用嗎?

分析:

這是一道特別常見的問題,主要考查大家對虛擬DOM和patch細節的掌握程度,能夠反映面試者理解層次。

思路分析:

  • 給出結論,key的作用是用於優化patch效能

  • key的必要性

  • 實際使用方式

  • 總結:可從原始碼層面描述一下vue如何判斷兩個節點是否相同


回答範例:

  • key的作用主要是為了更高效的更新虛擬DOM。

  • vue在patch過程中判斷兩個節點是否是相同節點是key是一個必要條件,渲染一組列表時,key往往是唯一標識,所以如果不定義key的話,vue只能認為比較的兩個節點是同一個,哪怕它們實際上不是,這導致了頻繁更新元素,使得整個patch過程比較低效,影響效能。

  • 實際使用中在渲染一組列表時key必須設定,而且必須是唯一標識,應該避免使用陣列索引作為key,這可能導致一些隱蔽的bug;vue中在使用相同標籤元素過渡切換時,也會使用key屬性,其目的也是為了讓vue可以區分它們,否則vue只會替換其內部屬性而不會觸發過渡效果。

  • 從原始碼中可以知道,vue判斷兩個節點是否相同時主要判斷兩者的key和元素型別等,因此如果不設定key,它的值就是undefined,則可能永遠認為這是兩個相同節點,只能去做更新操作,這造成了大量的dom更新操作,明顯是不可取的。


知其所以然

測試程式碼,test-v3.html

上面案例重現的是以下過程

8.png

不使用key

9.png


如果使用key

// 首次迴圈patch A
A B C D E
A B F C D E

// 第2次迴圈patch B
B C D E
B F C D E

// 第3次迴圈patch E
C D E
F C D E

// 第4次迴圈patch D
C D
F C D

// 第5次迴圈patch C
C 
F C

// oldCh全部處理結束,newCh中剩下的F,建立F並插入到C前面

原始碼中找答案:

判斷是否為相同節點

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L342-L343

更新時的處理

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1752-L1753


不使用key

10.png

如果使用key

// 首次迴圈patch A
A B C D E
A B F C D E

// 第2次迴圈patch B
B C D E
B F C D E

// 第3次迴圈patch E
C D E
F C D E

// 第4次迴圈patch D
C D
F C D

// 第5次迴圈patch C
C 
F C

// oldCh全部處理結束,newCh中剩下的F,建立F並插入到C前面

原始碼中找答案:

判斷是否為相同節點

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L342-L343

更新時的處理

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1752-L1753


15-說說nextTick的使用和原理?

分析

這道題及考察使用,有考察原理,nextTick在開發過程中應用的也較少,原理上和vue非同步更新有密切關係,對於面試者考查很有區分度,如果能夠很好回答此題,對面試效果有極大幫助。

答題思路

  • nextTick是做什麼的?

  • 為什麼需要它呢?

  • 開發時何時使用它?抓抓頭,想想你在平時開發中使用它的地方

  • 下面介紹一下如何使用nextTick

  • 原理解讀,結合非同步更新和nextTick生效方式,會顯得你格外優秀


回答範例:

1、nextTick是等待下一次 DOM 更新重新整理的工具方法。

2、Vue有個非同步更新策略,意思是如果資料變化,Vue不會立刻更新DOM,而是開啟一個佇列,把元件更新函數儲存在佇列中,在同一事件迴圈中發生的所有資料變更會非同步的批次更新。這一策略導致我們對資料的修改不會立刻體現在DOM上,此時如果想要獲取更新後的DOM狀態,就需要使用nextTick。

3、開發時,有兩個場景我們會用到nextTick:

  • created中想要獲取DOM時;

  • 響應式資料變化後獲取DOM更新後的狀態,比如希望獲取列表更新後的高度。

4、nextTick簽名如下:function nextTick(callback?: () => void): Promise<void>

所以我們只需要在傳入的回撥函數中存取最新DOM狀態即可,或者我們可以await nextTick()方法返回的Promise之後做這件事。

5、在Vue內部,nextTick之所以能夠讓我們看到DOM更新後的結果,是因為我們傳入的callback會被新增到佇列重新整理函數(flushSchedulerQueue)的後面,這樣等佇列內部的更新函數都執行完畢,所有DOM操作也就結束了,callback自然能夠獲取到最新的DOM值。


知其所以然:

  • 原始碼解讀:

元件更新函數入隊:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1547-L1548

入隊函數:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/scheduler.ts#L84-L85

nextTick定義:

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/scheduler.ts#L58-L59

  • 測試案例,test-v3.html


16-watch和computed的區別以及選擇?

兩個重要API,反應應聘者熟練程度。

思路分析

  • 先看computed, watch兩者定義,列舉使用上的差異

  • 列舉使用場景上的差異,如何選擇

  • 使用細節、注意事項

  • vue3變化


computed特點:具有響應式的返回值

const count = ref(1)
const plusOne = computed(() => count.value + 1)

watch特點:偵測變化,執行回撥

const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

回答範例

  • 計算屬性可以從元件資料派生出新資料,最常見的使用方式是設定一個函數,返回計算之後的結果,computed和methods的差異是它具備快取性,如果依賴項不變時不會重新計算。偵聽器可以偵測某個響應式資料的變化並執行副作用,常見用法是傳遞一個函數,執行副作用,watch沒有返回值,但可以執行非同步操作等複雜邏輯。

  • 計算屬性常用場景是簡化行內模板中的複雜表示式,模板中出現太多邏輯會是模板變得臃腫不易維護。偵聽器常用場景是狀態變化之後做一些額外的DOM操作或者非同步操作。選擇採用何用方案時首先看是否需要派生出新值,基本能用計算屬性實現的方式首選計算屬性。

  • 使用過程中有一些細節,比如計算屬性也是可以傳遞物件,成為既可讀又可寫的計算屬性。watch可以傳遞物件,設定deep、immediate等選項。

  • vue3中watch選項發生了一些變化,例如不再能偵測一個點操作符之外的字串形式的表示式; reactivity API中新出現了watch、watchEffect可以完全替代目前的watch選項,且功能更加強大。


可能追問

  • watch會不會立即執行?

  • watch 和 watchEffect有什麼差異


知其所以然

computed的實現

  • https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/computed.ts#L79-L80

ComputedRefImpl

  • https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/computed.ts#L26-L27

快取性

  • https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/computed.ts#L59-L60

  • https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/computed.ts#L45-L46

watch的實現

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiWatch.ts#L158-L159


17-說一下 Vue 子元件和父元件建立和掛載順序

這題考查大家對建立過程的理解程度。

思路分析

  • 給結論

  • 闡述理由


回答範例

  • 建立過程自上而下,掛載過程自下而上;即:

    • parent created
    • child created
    • child mounted
    • parent mounted
  • 之所以會這樣是因為Vue建立過程是一個遞迴過程,先建立父元件,有子元件就會建立子元件,因此建立時先有父元件再有子元件;子元件首次建立時會新增mounted勾點到佇列,等到patch結束再執行它們,可見子元件的mounted勾點是先進入到佇列中的,因此等到patch結束執行這些勾點時也先執行。


知其所以然

觀察beforeCreated和created勾點的處理

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentOptions.ts#L554-L555

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentOptions.ts#L741-L742

觀察beforeMount和mounted勾點的處理

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1310-L1311

測試程式碼,test-v3.html


18-怎麼快取當前的元件?快取後怎麼更新?

快取元件使用keep-alive元件,這是一個非常常見且有用的優化手段,vue3中keep-alive有比較大的更新,能說的點比較多。

思路

  • 快取用keep-alive,它的作用與用法

  • 使用細節,例如快取指定/排除、結合router和transition

  • 元件快取後更新可以利用activated或者beforeRouteEnter

  • 原理闡述


回答範例

  • 開發中快取元件使用keep-alive元件,keep-alive是vue內建元件,keep-alive包裹動態元件component時,會快取不活動的元件範例,而不是銷燬它們,這樣在元件切換過程中將狀態保留在記憶體中,防止重複渲染DOM。

    <keep-alive>
      <component :is="view"></component>
    </keep-alive>
  • 結合屬性include和exclude可以明確指定快取哪些元件或排除快取指定元件。vue3中結合vue-router時變化較大,之前是keep-alive包裹router-view,現在需要反過來用router-view包裹keep-alive

    <router-view v-slot="{ Component }">
      <keep-alive>
        <component :is="Component"></component>
      </keep-alive>
    </router-view>
  • 快取後如果要獲取資料,解決方案可以有以下兩種:

    • beforeRouteEnter:在有vue-router的專案,每次進入路由的時候,都會執行beforeRouteEnter

      beforeRouteEnter(to, from, next){
        next(vm=>{
          console.log(vm)
          // 每次進入路由執行
          vm.getData()  // 獲取資料
        })
      },
    • actived:在keep-alive快取的元件被啟用的時候,都會執行actived勾點

      activated(){
      	  this.getData() // 獲取資料
      },
  • keep-alive是一個通用元件,它內部定義了一個map,快取建立過的元件範例,它返回的渲染函數內部會查詢內嵌的component元件對應元件的vnode,如果該元件在map中存在就直接返回它。由於component的is屬性是個響應式資料,因此只要它變化,keep-alive的render函數就會重新執行。


知其所以然

KeepAlive定義

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/components/KeepAlive.ts#L73-L74

快取定義

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/components/KeepAlive.ts#L102-L103

快取元件

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/components/KeepAlive.ts#L215-L216

獲取快取元件

  • https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/components/KeepAlive.ts#L241-L242

測試快取特性,test-v3.html


19-從0到1自己構架一個vue專案,說說有哪些步驟、哪些重要外掛、目錄結構你會怎麼組織

綜合實踐類題目,考查實戰能力。沒有什麼絕對的正確答案,把平時工作的重點有條理的描述一下即可。

思路

  • 構建專案,建立專案基本結構

  • 引入必要的外掛:

  • 程式碼規範:prettier,eslint

  • 提交規範:husky,lint-staged

  • 其他常用:svg-loader,vueuse,nprogress

  • 常見目錄結構


回答範例

  • 從0建立一個專案我大致會做以下事情:專案構建、引入必要外掛、程式碼規範、提交規範、常用庫和元件

  • 目前vue3專案我會用vite或者create-vue建立專案

  • 接下來引入必要外掛:路由外掛vue-router、狀態管理vuex/pinia、ui庫我比較喜歡element-plus和antd-vue、http工具我會選axios

  • 其他比較常用的庫有vueuse,nprogress,圖示可以使用vite-svg-loader

  • 下面是程式碼規範:結合prettier和eslint即可

  • 最後是提交規範,可以使用husky,lint-staged,commitlint


  • 目錄結構我有如下習慣:.vscode:用來放專案中的 vscode 設定

    plugins:用來放 vite 外掛的 plugin 設定

    public:用來放一些諸如 頁頭icon 之類的公共檔案,會被打包到dist根目錄下

    src:用來放專案程式碼檔案

    api:用來放http的一些介面設定

    assets:用來放一些 CSS 之類的靜態資源

    components:用來放專案通用元件

    layout:用來放專案的佈局

    router:用來放專案的路由設定

    store:用來放狀態管理Pinia的設定

    utils:用來放專案中的工具方法類

    views:用來放專案的頁面檔案


20-實際工作中,你總結的vue最佳實踐有哪些?

看到這樣的題目,可以用以下圖片來回答:

11.png


思路

檢視vue官方檔案:

風格指南:https://vuejs.org/style-guide/

效能:https://vuejs.org/guide/best-practices/performance.html#overview

安全:https://vuejs.org/guide/best-practices/security.html

存取性:https://vuejs.org/guide/best-practices/accessibility.html

釋出:https://vuejs.org/guide/best-practices/production-deployment.html


回答範例

我從編碼風格、效能、安全等方面說幾條:

  • 編碼風格方面:

    • 命名元件時使用「多詞」風格避免和HTML元素衝突
    • 使用「細節化」方式定義屬性而不是隻有一個屬性名
    • 屬性名宣告時使用「駝峰命名」,模板或jsx中使用「肉串命名」
    • 使用v-for時務必加上key,且不要跟v-if寫在一起
  • 效能方面:

    • 路由懶載入減少應用尺寸
    • 利用SSR減少首屏載入時間
    • 利用v-once渲染那些不需要更新的內容
    • 一些長列表可以利用虛擬捲動技術避免記憶體過度佔用
    • 對於深層巢狀物件的大陣列可以使用shallowRef或shallowReactive降低開銷
    • 避免不必要的元件抽象
  • 安全:

    • 不使用不可信模板,例如使用使用者輸入拼接模板:template: <div> + userProvidedString + </div>
    • 小心使用v-html,:url,:style等,避免html、url、樣式等注入
  • 等等......


21 - 簡單說一說你對vuex理解?

12.png

思路

  • 給定義

  • 必要性闡述

  • 何時使用

  • 拓展:一些個人思考、實踐經驗等


範例

  • Vuex 是一個專為 Vue.js 應用開發的狀態管理模式 + 庫。它採用集中式儲存,管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。

  • 我們期待以一種簡單的「單向資料流」的方式管理應用,即狀態 -> 檢視 -> 操作單向迴圈的方式。但當我們的應用遇到多個元件共用狀態時,比如:多個檢視依賴於同一狀態或者來自不同檢視的行為需要變更同一狀態。此時單向資料流的簡潔性很容易被破壞。因此,我們有必要把元件的共用狀態抽取出來,以一個全域性單例模式管理。通過定義和隔離狀態管理中的各種概念並通過強制規則維持檢視和狀態間的獨立性,我們的程式碼將會變得更結構化且易維護。這是vuex存在的必要性,它和react生態中的redux之類是一個概念。

  • Vuex 解決狀態管理的同時引入了不少概念:例如state、mutation、action等,是否需要引入還需要根據應用的實際情況衡量一下:如果不打算開發大型單頁應用,使用 Vuex 反而是繁瑣冗餘的,一個簡單的 store 模式就足夠了。但是,如果要構建一箇中大型單頁應用,Vuex 基本是標配。

  • 我在使用vuex過程中感受到一些blabla


可能的追問

  • vuex有什麼缺點嗎?你在開發過程中有遇到什麼問題嗎?

  • action和mutation的區別是什麼?為什麼要區分它們?


22-說說從 template 到 render 處理過程

分析

問我們template到render過程,其實是問vue編譯器工作原理。

思路

  • 引入vue編譯器概念

  • 說明編譯器的必要性

  • 闡述編譯器工作流程

回答範例

  • Vue中有個獨特的編譯器模組,稱為「compiler」,它的主要作用是將使用者編寫的template編譯為js中可執行的render函數。

  • 之所以需要這個編譯過程是為了便於前端程式設計師能高效的編寫檢視模板。相比而言,我們還是更願意用HTML來編寫檢視,直觀且高效。手寫render函數不僅效率底下,而且失去了編譯期的優化能力。

  • 在Vue中編譯器會先對template進行解析,這一步稱為parse,結束之後會得到一個JS物件,我們成為抽象語法樹AST,然後是對AST進行深加工的轉換過程,這一步成為transform,最後將前面得到的AST生成為JS程式碼,也就是render函數。

知其所以然

vue3編譯過程窺探:

  • https://github1s.com/vuejs/core/blob/HEAD/packages/compiler-core/src/compile.ts#L61-L62

測試,test-v3.html

可能的追問

  • Vue中編譯器何時執行?

  • react有沒有編譯器?


23-Vue範例掛載的過程中發生了什麼?

分析

掛載過程完成了最重要的兩件事:

  • 初始化

  • 建立更新機制

把這兩件事說清楚即可!

回答範例

  • 掛載過程指的是app.mount()過程,這個過程中整體上做了兩件事:初始化建立更新機制

  • 初始化會建立元件範例、初始化元件狀態,建立各種響應式資料

  • 建立更新機制這一步會立即執行一次元件更新函數,這會首次執行元件渲染函數並執行patch將前面獲得vnode轉換為dom;同時首次執行渲染函數會建立它內部響應式資料之間和元件更新函數之間的依賴關係,這使得以後資料變化時會執行對應的更新函數。

知其所以然

  • 測試程式碼,test-v3.html mount函數定義

    https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiCreateApp.ts#L277-L278

  • 首次render過程

    https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L2303-L2304

可能的追問

  • 響應式資料怎麼建立

  • 依賴關係如何建立

原文地址:https://juejin.cn/post/7097067108663558151

作者:楊村長

(學習視訊分享:)