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

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

01-Vue 3.0的設計目標是什麼?做了哪些優化?

分析

還是問新特性,陳述典型新特性,分析其給你帶來的變化即可。(學習視訊分享:)

思路

從以下幾方面分門別類闡述:易用性、效能、擴充套件性、可維護性、開發體驗等

範例

  • Vue3的最大設計目標是替代Vue2(皮一下),為了實現這一點,Vue3在以下幾個方面做了很大改進,如:易用性、框架效能、擴充套件性、可維護性、開發體驗等

  • 易用性方面主要是API簡化,比如v-model在Vue3中變成了Vue2中v-modelsync修飾符的結合體,使用者不用區分兩者不同,也不用選擇困難。類似的簡化還有用於渲染函數內部生成VNode的h(type, props, children),其中props不用考慮區分屬性、特性、事件等,框架替我們判斷,易用性大增。

  • 開發體驗方面,新元件Teleport傳送門、FragmentsSuspense等都會簡化特定場景的程式碼編寫,SFC Composition API語法糖更是極大提升我們開發體驗。

  • 擴充套件性方面提升如獨立的reactivity模組,custom renderer API等

  • 可維護性方面主要是Composition API,更容易編寫高複用性的業務邏輯。還有對TypeScript支援的提升。

  • 效能方面的改進也很顯著,例如編譯期優化、基於Proxy的響應式系統

  • 。。。

可能的追問

  1. Vue3做了哪些編譯優化?
  2. ProxydefineProperty有什麼不同?

02-你瞭解哪些Vue效能優化方法?

分析

這是一道綜合實踐題目,寫過一定數量的程式碼之後小夥伴們自然會開始關注一些優化方法,答得越多肯定實踐經驗也越豐富,是很好的題目。

答題思路:

根據題目描述,這裡主要探討Vue程式碼層面的優化

回答範例

  • 我這裡主要從Vue程式碼編寫層面說一些優化手段,例如:程式碼分割、伺服器端渲染、元件快取、長列表優化等

  • 最常見的路由懶載入:有效拆分App尺寸,存取時才非同步載入

    const router = createRouter({
      routes: [
        // 藉助webpack的import()實現非同步元件
        { path: '/foo', component: () => import('./Foo.vue') }
      ]
    })
  • keep-alive快取頁面:避免重複建立元件範例,且能保留快取元件狀態

    <router-view v-slot="{ Component }">
        <keep-alive>
        <component :is="Component"></component>
      </keep-alive>
    </router-view>
  • 使用v-show複用DOM:避免重複建立元件

    <template>
      <div class="cell">
        <!-- 這種情況用v-show複用DOM,比v-if效果好 -->
        <div v-show="value" class="on">
          <Heavy :n="10000"/>
        </div>
        <section v-show="!value" class="off">
          <Heavy :n="10000"/>
        </section>
      </div>
    </template>
  • v-for 遍歷避免同時使用 v-if:實際上在Vue3中已經是個錯誤寫法

    <template>
        <ul>
          <li
            v-for="user in activeUsers"
            <!-- 避免同時使用,vue3中會報錯 -->
            <!-- v-if="user.isActive" -->
            :key="user.id">
            {{ user.name }}
          </li>
        </ul>
    </template>
    <script>
      export default {
        computed: {
          activeUsers: function () {
            return this.users.filter(user => user.isActive)
          }
        }
      }
    </script>
  • v-once和v-memo:不再變化的資料使用v-once

    <!-- single element -->
    <span v-once>This will never change: {{msg}}</span>
    <!-- the element have children -->
    <div v-once>
      <h1>comment</h1>
      <p>{{msg}}</p>
    </div>
    <!-- component -->
    <my-component v-once :comment="msg"></my-component>
    <!-- `v-for` directive -->
    <ul>
      <li v-for="i in list" v-once>{{i}}</li>
    </ul>

    按條件跳過更新時使用v-momo:下面這個列表只會更新選中狀態變化項

    <div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
      <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
      <p>...more child nodes</p>
    </div>

    https://vuejs.org/api/built-in-directives.html

  • 長列表效能優化:如果是巨量資料長列表,可採用虛擬捲動,只渲染少部分割區域的內容

    <recycle-scroller
      class="items"
      :items="items"
      :item-size="24"
    >
      <template v-slot="{ item }">
        <FetchItemView
          :item="item"
          @vote="voteItem(item)"
        />
      </template>
    </recycle-scroller>

    一些開源庫:

    • vue-virtual-scroller:https://github.com/Akryum/vue-virtual-scroller
    • vue-virtual-scroll-grid:https://github.com/rocwang/vue-virtual-scroll-grid
  • 事件的銷燬:Vue 元件銷燬時,會自動解綁它的全部指令及事件監聽器,但是僅限於元件本身的事件。

    export default {
      created() {
        this.timer = setInterval(this.refresh, 2000)
      },
      beforeUnmount() {
        clearInterval(this.timer)
      }
    }
  • 圖片懶載入

    對於圖片過多的頁面,為了加速頁面載入速度,所以很多時候我們需要將頁面內未出現在可視區域內的圖片先不做載入, 等到捲動到可視區域後再去載入。

    <img v-lazy="/static/img/1.png">

    參考專案:https://github.com/hilongjw/vue-lazyload

  • 第三方外掛按需引入

    element-plus這樣的第三方元件庫可以按需引入避免體積太大。

    import { createApp } from 'vue';
    import { Button, Select } from 'element-plus';
    
    const app = createApp()
    app.use(Button)
    app.use(Select)
  • 子元件分割策略:較重的狀態元件適合拆分

    <template>
      <div>
        <ChildComp/>
      </div>
    </template>
    
    <script>
    export default {
      components: {
        ChildComp: {
          methods: {
            heavy () { /* 耗時任務 */ }
          },
          render (h) {
            return h('div', this.heavy())
          }
        }
      }
    }
    </script>

    但同時也不宜過度拆分元件,尤其是為了所謂元件抽象將一些不需要渲染的元件特意抽出來,元件範例消耗遠大於純dom節點。參考:https://vuejs.org/guide/best-practices/performance.html#avoid-unnecessary-component-abstractions

  • 伺服器端渲染/靜態網站生成:SSR/SSG

    如果SPA應用有首屏渲染慢的問題,可以考慮SSR、SSG方案優化。參考:https://vuejs.org/guide/scaling-up/ssr.html


03-Vue元件為什麼只能有一個根元素?

這題現在有些落伍,vue3已經不用一個根了。因此這題目很有說頭!

體驗一下

vue2直接報錯,test-v2.html

new Vue({
  components: {
    comp: {
      template: `
        <div>root1</div>
        <div>root2</div>
      `
    }
  }
}).$mount('#app')

1.png

vue3中沒有問題,test-v3.html

Vue.createApp({
  components: {
    comp: {
      template: `
        <div>root1</div>
        <div>root2</div>
      `
    }
  }
}).mount('#app')

2.png

回答思路

  • 給一條自己的結論
  • 解釋為什麼會這樣
  • vue3解決方法原理

範例

  • vue2中元件確實只能有一個根,但vue3中元件已經可以多根節點了。
  • 之所以需要這樣是因為vdom是一顆單根樹形結構,patch方法在遍歷的時候從根節點開始遍歷,它要求只有一個根節點。元件也會轉換為一個vdom,自然應該滿足這個要求。
  • vue3中之所以可以寫多個根節點,是因為引入了Fragment的概念,這是一個抽象的節點,如果發現元件是多根的,就建立一個Fragment節點,把多個根節點作為它的children。將來patch的時候,如果發現是一個Fragment節點,則直接遍歷children建立或更新。

知其所以然

  • patch方法接收單根vdom:

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

    // 直接獲取type等,沒有考慮陣列的可能性
    const { type, ref, shapeFlag } = n2
  • patch方法對Fragment的處理:

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

    // a fragment can only have array children
    // since they are either generated by the compiler, or implicitly created
    // from arrays.
    mountChildren(n2.children as VNodeArrayChildren, container, ...)

04-這是基本應用能力考察,稍微上點規模的專案都要拆分vuex模組便於維護。

體驗

https://vuex.vuejs.org/zh/guide/modules.html

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態
store.getters.c // -> moduleA裡的getters
store.commit('d') // -> 能同時觸發子模組中同名mutation
store.dispatch('e') // -> 能同時觸發子模組中同名action

思路

  • 概念和必要性

  • 怎麼拆

  • 使用細節

  • 優缺點

範例

  • 用過module,專案規模變大之後,單獨一個store物件會過於龐大臃腫,通過模組方式可以拆分開來便於維護

  • 可以按之前規則單獨編寫子模組程式碼,然後在主檔案中通過modules選項組織起來:createStore({modules:{...}})

  • 不過使用時要注意存取子模組狀態時需要加上註冊時模組名:store.state.a.xxx,但同時gettersmutationsactions又在全域性空間中,使用方式和之前一樣。如果要做到完全拆分,需要在子模組加上namespace選項,此時再存取它們就要加上名稱空間字首。

  • 很顯然,模組的方式可以拆分程式碼,但是缺點也很明顯,就是使用起來比較繁瑣複雜,容易出錯。而且型別系統支援很差,不能給我們帶來幫助。pinia顯然在這方面有了很大改進,是時候切換過去了。

可能的追問

  • 用過pinia嗎?都做了哪些改善?


05-怎麼實現路由懶載入呢?

分析

這是一道應用題。當打包應用時,JavaScript 包會變得非常大,影響頁面載入。如果我們能把不同路由對應的元件分割成不同的程式碼塊,然後當路由被存取時才載入對應元件,這樣就會更加高效。

// 將
// import UserDetails from './views/UserDetails'
// 替換為
const UserDetails = () => import('./views/UserDetails')

const router = createRouter({
  // ...
  routes: [{ path: '/users/:id', component: UserDetails }],
})

參考:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html

思路

  • 必要性

  • 何時用

  • 怎麼用

  • 使用細節

回答範例

  • 當打包構建應用時,JavaScript 包會變得非常大,影響頁面載入。利用路由懶載入我們能把不同路由對應的元件分割成不同的程式碼塊,然後當路由被存取的時候才載入對應元件,這樣會更加高效,是一種優化手段。

  • 一般來說,對所有的路由都使用動態匯入是個好主意。

  • component選項設定一個返回 Promise 元件的函數就可以定義懶載入路由。例如:

    { path: '/users/:id', component: () => import('./views/UserDetails') }

  • 結合註釋() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')可以做webpack程式碼分塊

    vite中結合rollupOptions定義分塊

  • 路由中不能使用非同步元件

知其所以然

component (和 components) 設定如果接收一個返回 Promise 元件的函數,Vue Router 只會在第一次進入頁面時才會獲取這個函數,然後使用快取資料。

https://github1s.com/vuejs/router/blob/HEAD/src/navigationGuards.ts#L292-L293


06-ref和reactive異同

這是Vue3資料響應式中非常重要的兩個概念,自然的,跟我們寫程式碼關係也很大。

體驗

ref:https://vuejs.org/api/reactivity-core.html#ref

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

reactive:https://vuejs.org/api/reactivity-core.html#reactive

const obj = reactive({ count: 0 })
obj.count++

回答思路

  • 兩者概念

  • 兩者使用場景

  • 兩者異同

  • 使用細節

  • 原理

回答範例

  • ref接收內部值(inner value)返回響應式Ref物件,reactive返回響應式代理物件

  • 從定義上看ref通常用於處理單值的響應式,reactive用於處理物件型別的資料響應式

  • 兩者均是用於構造響應式資料,但是ref主要解決原始值的響應式問題

  • ref返回的響應式資料在JS中使用需要加上.value才能存取其值,在檢視中使用會自動脫ref,不需要.value;ref可以接收物件或陣列等非原始值,但內部依然是reactive實現響應式;reactive內部如果接收Ref物件會自動脫ref;使用展開運運算元(...)展開reactive返回的響應式物件會使其失去響應性,可以結合toRefs()將值轉換為Ref物件之後再展開。

  • reactive內部使用Proxy代理傳入物件並攔截該物件各種操作(trap),從而實現響應式。ref內部封裝一個RefImpl類,並設定get value/set value,攔截使用者對值的存取,從而實現響應式。

知其所以然

  • reactive實現響應式:

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

  • ref實現響應式:

    https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/ref.ts#L73-L74


07-watch和watchEffect異同

我們經常性需要偵測響應式資料的變化,vue3中除了watch之外又出現了watchEffect,不少同學會混淆這兩個api。

體驗

watchEffect立即執行一個函數,然後被動地追蹤它的依賴,當這些依賴改變時重新執行該函數。

Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

count.value++
// -> logs 1

watch偵測一個或多個響應式資料來源並在資料來源變化時呼叫一個回撥函數。

Watches one or more reactive data sources and invokes a callback function when the sources change.

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

思路

  • 給出兩者定義

  • 給出場景上的不同

  • 給出使用方式和細節

  • 原理闡述

範例

  • watchEffect立即執行一個函數,然後被動地追蹤它的依賴,當這些依賴改變時重新執行該函數。watch偵測一個或多個響應式資料來源並在資料來源變化時呼叫一個回撥函數。

  • watchEffect(effect)是一種特殊watch,傳入的函數既是依賴收集的資料來源,也是回撥函數。如果我們不關心響應式資料變化前後的值,只是想拿這些資料做些事情,那麼watchEffect就是我們需要的。watch更底層,可以接收多種資料來源,包括用於依賴收集的getter函數,因此它完全可以實現watchEffect的功能,同時由於可以指定getter函數,依賴可以控制的更精確,還能獲取資料變化前後的值,因此如果需要這些時我們會使用watch。

  • watchEffect在使用時,傳入的函數會立刻執行一次。watch預設情況下並不會執行回撥函數,除非我們手動設定immediate選項。

  • 從實現上來說,watchEffect(fn)相當於watch(fn,fn,{immediate:true})

知其所以然

watchEffect定義:https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiWatch.ts#L80-L81

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}

watch定義如下:https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiWatch.ts#L158-L159

export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  return doWatch(source as any, cb, options)
}

很明顯watchEffect就是一種特殊的watch實現。


08-SPA、SSR的區別是什麼

我們現在編寫的Vue、React和Angular應用大多數情況下都會在一個頁面中,點選連結跳轉頁面通常是內容切換而非頁面跳轉,由於良好的使用者體驗逐漸成為主流的開發模式。但同時也會有首屏載入時間長,SEO不友好的問題,因此有了SSR,這也是為什麼面試中會問到兩者的區別。

思路分析

  • 兩者概念

  • 兩者優缺點分析

  • 使用場景差異

  • 其他選擇

回答範例

  • SPA(Single Page Application)即單頁面應用。一般也稱為 使用者端渲染(Client Side Render), 簡稱 CSR。SSR(Server Side Render)即 伺服器端渲染。一般也稱為 多頁面應用(Mulpile Page Application),簡稱 MPA。

  • SPA應用只會首次請求html檔案,後續只需要請求JSON資料即可,因此使用者體驗更好,節約流量,伺服器端壓力也較小。但是首屏載入的時間會變長,而且SEO不友好。為了解決以上缺點,就有了SSR方案,由於HTML內容在伺服器一次性生成出來,首屏載入快,搜尋引擎也可以很方便的抓取頁面資訊。但同時SSR方案也會有效能,開發受限等問題。

  • 在選擇上,如果我們的應用存在首屏載入優化需求,SEO需求時,就可以考慮SSR。

  • 但並不是只有這一種替代方案,比如對一些不常變化的靜態網站,SSR反而浪費資源,我們可以考慮預渲染(prerender)方案。另外nuxt.js/next.js中給我們提供了SSG(Static Site Generate)靜態網站生成方案也是很好的靜態站點解決方案,結合一些CI手段,可以起到很好的優化效果,且能節約伺服器資源。

知其所以然

內容生成上的區別:

SSR

3.png

SPA

4.png

部署上的區別

5.png


09-vue-loader是什麼?它有什麼作用?

分析

這是一道工具類的原理題目,相當有深度,具有不錯的人才區分度。

體驗

使用官方提供的SFC playground可以很好的體驗vue-loader

sfc.vuejs.org

有了vue-loader加持,我們才可以以SFC的方式快速編寫程式碼。

<template>
  <div class="example">{{ msg }}</div>
</template>

<script>
export default {
  data() {
    return {
      msg: 'Hello world!',
    }
  },
}
</script>

<style>
.example {
  color: red;
}
</style>

思路

  • vue-loader是什麼東東
  • vue-loader是做什麼用的
  • vue-loader何時生效
  • vue-loader如何工作

回答範例

  • vue-loader是用於處理單檔案元件(SFC,Single-File Component)的webpack loader

  • 因為有了vue-loader,我們就可以在專案中編寫SFC格式的Vue元件,我們可以把程式碼分割為<template>、<script>和<style>,程式碼會異常清晰。結合其他loader我們還可以用Pug編寫<template>,用SASS編寫<style>,用TS編寫<script>。我們的<style>還可以單獨作用當前元件。

  • webpack打包時,會以loader的方式呼叫vue-loader

  • vue-loader被執行時,它會對SFC中的每個語言塊用單獨的loader鏈處理。最後將這些單獨的塊裝配成最終的元件模組。

知其所以然

1、vue-loader會呼叫@vue/compiler-sfc模組解析SFC原始碼為一個描述符(Descriptor),然後為每個語言塊生成import程式碼,返回的程式碼類似下面:

// source.vue被vue-loader處理之後返回的程式碼

// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'

script.render = render
export default script

2、我們想要script塊中的內容被作為js處理(當然如果是<script lang="ts">被作為ts處理),這樣我們想要webpack把設定中跟.js匹配的規則都應用到形如source.vue?vue&type=script的這個請求上。例如我們對所有*.js設定了babel-loader,這個規則將被克隆並應用到所在Vue SFC的

import script from 'source.vue?vue&type=script'

將被展開為:

import script from 'babel-loader!vue-loader!source.vue?vue&type=script'

類似的,如果我們對.sass檔案設定了style-loader + css-loader + sass-loader,對下面的程式碼:

<style scoped lang="scss">

vue-loader將會返回給我們下面結果:

import 'source.vue?vue&type=style&index=1&scoped&lang=scss'

然後webpack會展開如下:

import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'

1)當處理展開請求時,vue-loader將被再次呼叫。這次,loader將會關注那些有查詢串的請求,且僅針對特定塊,它會選中特定塊內部的內容並傳遞給後面匹配的loader。

2)對於<script>塊,處理到這就可以了,但是<template><style>還有一些額外任務要做,比如:

  • 需要用Vue 模板編譯器編譯template,從而得到render函數
  • 需要對<style scoped>中的CSS做後處理(post-process),該操作在css-loader之後但在style-loader之前

實現上這些附加的loader需要被注入到已經展開的loader鏈上,最終的請求會像下面這樣:

// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'

// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'

10-你寫過自定義指令嗎?使用場景有哪些?

分析

這是一道API題,我們可能寫的自定義指令少,但是我們用的多呀,多舉幾個例子就行。

體驗

定義一個包含類似元件生命週期勾點的物件,勾點函數會接收指令掛鉤的dom元素:

const focus = {
  mounted: (el) => el.focus()
}

export default {
  directives: {
    // enables v-focus in template
    focus
  }
}
<input v-focus />
<input v-focus />

思路

  • 定義

  • 何時用

  • 如何用

  • 常用指令

  • vue3變化

回答範例

  • Vue有一組預設指令,比如v-model或v-for,同時Vue也允許使用者註冊自定義指令來擴充套件Vue能力

  • 自定義指令主要完成一些可複用低層級DOM操作

  • 使用自定義指令分為定義、註冊和使用三步:

    • 定義自定義指令有兩種方式:物件和函數形式,前者類似元件定義,有各種生命週期;後者只會在mounted和updated時執行
    • 註冊自定義指令類似元件,可以使用app.directive()全域性註冊,使用{directives:{xxx}}區域性註冊
    • 使用時在註冊名稱前加上v-即可,比如v-focus
  • 我在專案中常用到一些自定義指令,例如:

    • 複製貼上 v-copy
    • 長按 v-longpress
    • 防抖 v-debounce
    • 圖片懶載入 v-lazy
    • 按鈕許可權 v-premission
    • 頁面水印 v-waterMarker
    • 拖拽指令 v-draggable
  • vue3中指令定義發生了比較大的變化,主要是勾點的名稱保持和元件一致,這樣開發人員容易記憶,不易犯錯。另外在v3.2之後,可以在setup中以一個小寫v開頭方便的定義自定義指令,更簡單了!

知其所以然

編譯後的自定義指令會被withDirective函數裝飾,進一步處理生成的vnode,新增到特定屬性中。

https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcblxuY29uc3QgbXNnID0gcmVmKCdIZWxsbyBXb3JsZCEnKVxuXG5jb25zdCB2Rm9jdXMgPSB7XG4gIG1vdW50ZWQoZWwpIHtcbiAgICAvLyDojrflj5ZpbnB1dO+8jOW5tuiwg+eUqOWFtmZvY3VzKCnmlrnms5VcbiAgICBlbC5mb2N1cygpXG4gIH1cbn1cbjwvc2NyaXB0PlxuXG48dGVtcGxhdGU+XG4gIDxoMT57eyBtc2cgfX08L2gxPlxuICA8aW5wdXQgdi1tb2RlbD1cIm1zZ1wiIHYtZm9jdXM+XG48L3RlbXBsYXRlPiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0ifQ==

11-說下$attrs和$listeners的使用場景

分析

API考察,但$attrs和$listeners是比較少用的邊界知識,而且vue3有變化,$listeners已經移除,還是有細節可說的。

思路

  • 這兩個api的作用

  • 使用場景分析

  • 使用方式和細節

  • vue3變化

體驗

一個包含元件透傳屬性的物件。

An object that contains the component's fallthrough attributes.

<template>
    <child-component v-bind="$attrs">
        將非屬性特性透傳給內部的子元件
    </child-component>
</template>

範例

  • 我們可能會有一些屬性和事件沒有在props中定義,這類稱為非屬性特性,結合v-bind指令可以直接透傳給內部的子元件。

  • 這類「屬性透傳」常常用於包裝高階元件時往內部傳遞屬性,常用於爺孫元件之間傳參。比如我在擴充套件A元件時建立了元件B元件,然後在C元件中使用B,此時傳遞給C的屬性中只有props裡面宣告的屬性是給B使用的,其他的都是A需要的,此時就可以利用v-bind="$attrs"透傳下去。

  • 最常見用法是結合v-bind做展開;$attrs本身不是響應式的,除非存取的屬性本身是響應式物件。

  • vue2中使用listeners獲取事件,vue3中已移除,均合併到listeners獲取事件,vue3中已移除,均合併到attrs中,使用起來更簡單了。

原理

檢視透傳屬性foo和普通屬性bar,發現vnode結構完全相同,這說明vue3中將分辨兩者工作由框架完成而非使用者指定:

<template>
  <h1>{{ msg }}</h1>
  <comp foo="foo" bar="bar" />
</template>
<template>
  <div>
    {{$attrs.foo}} {{bar}}
  </div>
</template>
<script setup>
defineProps({
  bar: String
})
</script>
_createVNode(Comp, {
    foo: "foo",
    bar: "bar"
})
https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcbmltcG9ydCBDb21wIGZyb20gJy4vQ29tcC52dWUnXG5jb25zdCBtc2cgPSByZWYoJ0hlbGxvIFdvcmxkIScpXG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aDE+e3sgbXNnIH19PC9oMT5cbiAgPGNvbXAgZm9vPVwiZm9vXCIgYmFyPVwiYmFyXCIgLz5cbjwvdGVtcGxhdGU+IiwiaW1wb3J0LW1hcC5qc29uIjoie1xuICBcImltcG9ydHNcIjoge1xuICAgIFwidnVlXCI6IFwiaHR0cHM6Ly9zZmMudnVlanMub3JnL3Z1ZS5ydW50aW1lLmVzbS1icm93c2VyLmpzXCJcbiAgfVxufSIsIkNvbXAudnVlIjoiPHRlbXBsYXRlPlxuXHQ8ZGl2PlxuICAgIHt7JGF0dHJzLmZvb319IHt7YmFyfX1cbiAgPC9kaXY+XG48L3RlbXBsYXRlPlxuPHNjcmlwdCBzZXR1cD5cbmRlZmluZVByb3BzKHtcbiAgYmFyOiBTdHJpbmdcbn0pXG48L3NjcmlwdD4ifQ==

12-v-once的使用場景有哪些?

分析

v-once是Vue中內建指令,很有用的API,在優化方面經常會用到,不過小夥伴們平時可能容易忽略它。

體驗

僅渲染元素和元件一次,並且跳過未來更新

Render the element and component once only, and skip future updates.

<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

思路

  • v-once是什麼

  • 什麼時候使用

  • 如何使用

  • 擴充套件v-memo

  • 探索原理

回答範例

  • v-once是vue的內建指令,作用是僅渲染指定元件或元素一次,並跳過未來對其更新。

  • 如果我們有一些元素或者元件在初始化渲染之後不再需要變化,這種情況下適合使用v-once,這樣哪怕這些資料變化,vue也會跳過更新,是一種程式碼優化手段。

  • 我們只需要作用的元件或元素上加上v-once即可。

  • vue3.2之後,又增加了v-memo指令,可以有條件快取部分模板並控制它們的更新,可以說控制力更強了。

  • 編譯器發現元素上面有v-once時,會將首次計算結果存入快取物件,元件再次渲染時就會從快取獲取,從而避免再次計算。

知其所以然

下面例子使用了v-once:

<script setup>
import { ref } from 'vue'

const msg = ref('Hello World!')
</script>

<template>
  <h1 v-once>{{ msg }}</h1>
  <input v-model="msg">
</template>

我們發現v-once出現後,編譯器會快取作用元素或元件,從而避免以後更新時重新計算這一部分:

// ...
return (_ctx, _cache) => {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    // 從快取獲取vnode
    _cache[0] || (
      _setBlockTracking(-1),
      _cache[0] = _createElementVNode("h1", null, [
        _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
      ]),
      _setBlockTracking(1),
      _cache[0]
    ),
// ...

13-什麼是遞迴元件?舉個例子說明下?

分析

遞迴元件我們用的比較少,但是在Tree、Menu這類元件中會被用到。

體驗

元件通過元件名稱參照它自己,這種情況就是遞迴元件。

An SFC can implicitly refer to itself via its filename.

<template>
  <li>
    <div> {{ model.name }}</div>
    <ul v-show="isOpen" v-if="isFolder">
      <!-- 注意這裡:元件遞迴渲染了它自己 -->
      <TreeItem
        class="item"
        v-for="model in model.children"
        :model="model">
      </TreeItem>
    </ul>
  </li>
<script>
export default {
  name: 'TreeItem',
  // ...
}
</script>

思路

  • 下定義
  • 使用場景
  • 使用細節
  • 原理闡述

回答範例

  • 如果某個元件通過元件名稱參照它自己,這種情況就是遞迴元件。

  • 實際開發中類似Tree、Menu這類元件,它們的節點往往包含子節點,子節點結構和父節點往往是相同的。這類元件的資料往往也是樹形結構,這種都是使用遞迴元件的典型場景。

  • 使用遞迴元件時,由於我們並未也不能在元件內部匯入它自己,所以設定元件name屬性,用來查詢元件定義,如果使用SFC,則可以通過SFC檔名推斷。元件內部通常也要有遞迴結束條件,比如model.children這樣的判斷。

  • 檢視生成渲染函數可知,遞迴元件查詢時會傳遞一個布林值給resolveComponent,這樣實際獲取的元件就是當前元件本身。

知其所以然

遞迴元件編譯結果中,獲取元件時會傳遞一個識別符號 _resolveComponent("Comp", true)

const _component_Comp = _resolveComponent("Comp", true)

就是在傳遞maybeSelfReference

export function resolveComponent(
  name: string,
  maybeSelfReference?: boolean
): ConcreteComponent | string {
  return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}

resolveAsset中最終返回的是元件自身:

if (!res && maybeSelfReference) {
    // fallback to implicit self-reference
    return Component
}
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/resolveAssets.ts#L22-L23


https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/resolveAssets.ts#L110-L111


https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdCBzZXR1cD5cbmltcG9ydCB7IHJlZiB9IGZyb20gJ3Z1ZSdcbmltcG9ydCBjb21wIGZyb20gJy4vQ29tcC52dWUnXG5jb25zdCBtc2cgPSByZWYoJ+mAkuW9kue7hOS7ticpXG5jb25zdCBtb2RlbCA9IHtcbiAgbGFiZWw6ICdub2RlLTEnLFxuICBjaGlsZHJlbjogW1xuICAgIHtsYWJlbDogJ25vZGUtMS0xJ30sXG4gICAge2xhYmVsOiAnbm9kZS0xLTInfVxuICBdXG59XG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aDE+e3sgbXNnIH19PC9oMT5cbiAgPGNvbXAgOm1vZGVsPVwibW9kZWxcIj48L2NvbXA+XG48L3RlbXBsYXRlPiIsImltcG9ydC1tYXAuanNvbiI6IntcbiAgXCJpbXBvcnRzXCI6IHtcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0iLCJDb21wLnZ1ZSI6Ijx0ZW1wbGF0ZT5cbiAgPGRpdj5cbiAgICB7e21vZGVsLmxhYmVsfX1cbiAgPC9kaXY+XG4gIDxDb21wIHYtZm9yPVwiaXRlbSBpbiBtb2RlbC5jaGlsZHJlblwiIDptb2RlbD1cIml0ZW1cIj48L0NvbXA+XG4gIDxjb21wMj48L2NvbXAyPlxuPC90ZW1wbGF0ZT5cbjxzY3JpcHQ+XG5cdGV4cG9ydCBkZWZhdWx0IHtcbiAgICBuYW1lOiAnQ29tcCcsXG4gICAgcHJvcHM6IHtcbiAgICAgIG1vZGVsOiBPYmplY3RcbiAgICB9LFxuICAgIGNvbXBvbmVudHM6IHtcbiAgICAgIGNvbXAyOiB7XG4gICAgICAgIHJlbmRlcigpe31cbiAgICAgIH1cbiAgICB9XG4gIH1cbjwvc2NyaXB0PiJ9

14-非同步元件是什麼?使用場景有哪些?

分析

因為非同步路由的存在,我們使用非同步元件的次數比較少,因此還是有必要兩者的不同。

體驗

大型應用中,我們需要分割應用為更小的塊,並且在需要元件時再載入它們。

In large applications, we may need to divide the app into smaller chunks and only load a component from the server when it's needed.

import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定義非同步元件
const AsyncComp = defineAsyncComponent(() => {
  // 載入函數返回Promise
  return new Promise((resolve, reject) => {
    // ...可以從伺服器載入元件
    resolve(/* loaded component */)
  })
})
// 藉助打包工具實現ES模組動態匯入
const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

思路

  • 非同步元件作用

  • 何時使用非同步元件

  • 使用細節

  • 和路由懶載入的不同

範例

  • 在大型應用中,我們需要分割應用為更小的塊,並且在需要元件時再載入它們。

  • 我們不僅可以在路由切換時懶載入元件,還可以在頁面元件中繼續使用非同步元件,從而實現更細的分割粒度。

  • 使用非同步元件最簡單的方式是直接給defineAsyncComponent指定一個loader函數,結合ES模組動態匯入函數import可以快速實現。我們甚至可以指定loadingComponent和errorComponent選項從而給使用者一個很好的載入反饋。另外Vue3中還可以結合Suspense元件使用非同步元件。

  • 非同步元件容易和路由懶載入混淆,實際上不是一個東西。非同步元件不能被用於定義懶載入路由上,處理它的是vue框架,處理路由元件載入的是vue-router。但是可以在懶載入的路由元件中使用非同步元件。

知其所以然

defineAsyncComponent定義了一個高階元件,返回一個包裝元件。包裝元件根據載入器的狀態決定渲染什麼內容。

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiAsyncComponent.ts#L43-L44


15-你是怎麼處理vue專案中的錯誤的?

分析

這是一個綜合應用題目,在專案中我們常常需要將App的異常上報,此時錯誤處理就很重要了。

這裡要區分錯誤的型別,針對性做收集。

然後是將收集的的錯誤資訊上報伺服器。

思路

  • 首先區分錯誤型別

  • 根據錯誤不同型別做相應收集

  • 收集的錯誤是如何上報伺服器的

回答範例

  • 應用中的錯誤型別分為"介面異常"和「程式碼邏輯異常

  • 我們需要根據不同錯誤型別做相應處理:介面異常是我們請求後端介面過程中發生的異常,可能是請求失敗,也可能是請求獲得了伺服器響應,但是返回的是錯誤狀態。以Axios為例,這類異常我們可以通過封裝Axios,在攔截器中統一處理整個應用中請求的錯誤。程式碼邏輯異常是我們編寫的前端程式碼中存在邏輯上的錯誤造成的異常,vue應用中最常見的方式是使用全域性錯誤處理常式app.config.errorHandler收集錯誤。

  • 收集到錯誤之後,需要統一處理這些異常:分析錯誤,獲取需要錯誤資訊和資料。這裡應該有效區分錯誤型別,如果是請求錯誤,需要上報介面資訊,引數,狀態碼等;對於前端邏輯異常,獲取錯誤名稱和詳情即可。另外還可以收集應用名稱、環境、版本、使用者資訊,所在頁面等。這些資訊可以通過vuex儲存的全域性狀態和路由資訊獲取。

實踐

axios攔截器中處理捕獲異常:

// 響應攔截器
instance.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (error) => {
    // 存在response說明伺服器有響應
    if (error.response) {
      let response = error.response;
      if (response.status >= 400) {
        handleError(response);
      }
    } else {
      handleError(null);
    }
    return Promise.reject(error);
  },
);

vue中全域性捕獲異常:

import { createApp } from 'vue'

const app = createApp(...)

app.config.errorHandler = (err, instance, info) => {
  // report error to tracking services
}

處理介面請求錯誤:

function handleError(error, type) {
  if(type == 1) {
    // 介面錯誤,從config欄位中獲取請求資訊
    let { url, method, params, data } = error.config
    let err_data = {
       url, method,
       params: { query: params, body: data },
       error: error.data?.message || JSON.stringify(error.data),
    })
  }
}

處理前端邏輯錯誤:

function handleError(error, type) {
  if(type == 2) {
    let errData = null
    // 邏輯錯誤
    if(error instanceof Error) {
      let { name, message } = error
      errData = {
        type: name,
        error: message
      }
    } else {
      errData = {
        type: 'other',
        error: JSON.strigify(error)
      }
    }
  }
}

16-如果讓你從零開始寫一個vuex,說說你的思路

思路分析

這個題目很有難度,首先思考vuex解決的問題:儲存使用者全域性狀態並提供管理狀態API。

  • vuex需求分析
  • 如何實現這些需求

回答範例

  • 官方說vuex是一個狀態管理模式和庫,並確保這些狀態以可預期的方式變更。可見要實現一個vuex

    • 要實現一個Store儲存全域性狀態
    • 要提供修改狀態所需API:commit(type, payload), dispatch(type, payload)
  • 實現Store時,可以定義Store類,建構函式接收選項options,設定屬性state對外暴露狀態,提供commit和dispatch修改屬性state。這裡需要設定state為響應式物件,同時將Store定義為一個Vue外掛。

  • commit(type, payload)方法中可以獲取使用者傳入mutations並執行它,這樣可以按使用者提供的方法修改狀態。 dispatch(type, payload)類似,但需要注意它可能是非同步的,需要返回一個Promise給使用者以處理非同步結果。

實踐

Store的實現:

class Store {
    constructor(options) {
        this.state = reactive(options.state)
        this.options = options
    }
    commit(type, payload) {
        this.options.mutations[type].call(this, this.state, payload)
    }
}

知其所以然

Vuex中Store的實現:https://github1s.com/vuejs/vuex/blob/HEAD/src/store.js#L19-L20


17-vuex中actions和mutations有什麼區別?

題目分析

mutationsactionsvuex帶來的兩個獨特的概念。新手程式設計師容易混淆,所以面試官喜歡問。

我們只需記住修改狀態只能是mutationsactions只能通過提交mutation修改狀態即可。

體驗

看下面例子可知,Action 類似於 mutation,不同在於:

  • Action 提交的是 mutation,而不是直接變更狀態。
  • Action 可以包含任意非同步操作。
const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

答題思路

  • 給出兩者概念說明區別

  • 舉例說明應用場景

  • 使用細節不同

  • 簡單闡述實現上差異

回答範例

  • 官方檔案說:更改 Vuex 的 store 中的狀態的唯一方法是提交 mutationmutation 非常類似於事件:每個 mutation 都有一個字串的型別 (type)和一個 回撥函數 (handler)Action 類似於 mutation,不同在於:Action可以包含任意非同步操作,但它不能修改狀態, 需要提交mutation才能變更狀態。

  • 因此,開發時,包含非同步操作或者複雜業務組合時使用action;需要直接修改狀態則提交mutation。但由於dispatch和commit是兩個API,容易引起混淆,實踐中也會採用統一使用dispatch action的方式。

  • 呼叫dispatch和commit兩個API時幾乎完全一樣,但是定義兩者時卻不甚相同,mutation的回撥函數接收引數是state物件。action則是與Store範例具有相同方法和屬性的上下文context物件,因此一般會解構它為{commit, dispatch, state},從而方便編碼。另外dispatch會返回Promise範例便於處理內部非同步結果。

  • 實現上commit(type)方法相當於呼叫options.mutations[type](state)dispatch(type)方法相當於呼叫options.actions[type](store),這樣就很容易理解兩者使用上的不同了。

知其所以然

我們可以像下面這樣簡單實現commitdispatch,從而辨別兩者不同:

class Store {
    constructor(options) {
        this.state = reactive(options.state)
        this.options = options
    }
    commit(type, payload) {
        // 傳入上下文和引數1都是state物件
        this.options.mutations[type].call(this.state, this.state, payload)
    }
    dispatch(type, payload) {
        // 傳入上下文和引數1都是store本身
        this.options.actions[type].call(this, this, payload)
    }
}

18-使用vue渲染大量資料時應該怎麼優化?說下你的思路!

分析

企業級專案中渲染大量資料的情況比較常見,因此這是一道非常好的綜合實踐題目。

思路

  • 描述巨量資料量帶來的問題

  • 分不同情況做不同處理

  • 總結一下

回答

  • 在大型企業級專案中經常需要渲染大量資料,此時很容易出現卡頓的情況。比如巨量資料量的表格、樹。

  • 處理時要根據情況做不通處理:

    • 可以採取分頁的方式獲取,避免渲染大量資料
    • vue-virtual-scroller等虛擬捲動方案,只渲染視口範圍內的資料
    • 如果不需要更新,可以使用v-once方式只渲染一次
    • 通過v-memo可以快取結果,結合v-for使用,避免資料變化時不必要的VNode建立
    • 可以採用懶載入方式,在使用者需要的時候再載入資料,比如tree元件子樹的懶載入
  • 總之,還是要看具體需求,首先從設計上避免巨量資料獲取和渲染;實在需要這樣做可以採用虛表的方式優化渲染;最後優化更新,如果不需要更新可以v-once處理,需要更新可以v-memo進一步優化巨量資料更新效能。其他可以採用的是互動方式優化,無線捲動、懶載入等方案。


19-怎麼監聽vuex資料的變化?

分析

vuex資料狀態是響應式的,所以狀態變檢視跟著變,但是有時還是需要知道資料狀態變了從而做一些事情。

既然狀態都是響應式的,那自然可以watch,另外vuex也提供了訂閱的API:store.subscribe()

思路

  • 總述知道的方法
  • 分別闡述用法
  • 選擇和場景

回答範例

  • 我知道幾種方法:

    • 可以通過watch選項或者watch方法監聽狀態
    • 可以使用vuex提供的API:store.subscribe()
  • watch選項方式,可以以字串形式監聽$store.state.xx;subscribe方式,可以呼叫store.subscribe(cb),回撥函數接收mutation物件和state物件,這樣可以進一步判斷mutation.type是否是期待的那個,從而進一步做後續處理。

  • watch方式簡單好用,且能獲取變化前後值,首選;subscribe方法會被所有commit行為觸發,因此還需要判斷mutation.type,用起來略繁瑣,一般用於vuex外掛中。

實踐

watch方式

const app = createApp({
    watch: {
      '$store.state.counter'() {
        console.log('counter change!');
      }
    }
  })

subscribe方式:

  store.subscribe((mutation, state) => {
    if (mutation.type === 'add') {
      console.log('counter change in subscribe()!');
    }
  })

20-router-link和router-view是如何起作用的?

分析

vue-router中兩個重要元件router-linkrouter-view,分別起到導航作用和內容渲染作用,但是回答如何生效還真有一定難度哪!

思路

  • 兩者作用
  • 闡述使用方式
  • 原理說明

回答範例

  • vue-router中兩個重要元件router-linkrouter-view,分別起到路由導航作用和元件內容渲染作用
  • 使用中router-link預設生成一個a標籤,設定to屬性定義跳轉path。實際上也可以通過custom和插槽自定義最終的展現形式。router-view是要顯示元件的佔位元件,可以巢狀,對應路由設定的巢狀關係,配合name可以顯示具名元件,起到更強的佈局作用。
  • router-link元件內部根據custom屬性判斷如何渲染最終生成節點,內部提供導航方法navigate,使用者點選之後實際呼叫的是該方法,此方法最終會修改響應式的路由變數,然後重新去routes匹配出陣列結果,router-view則根據其所處深度deep在匹配陣列結果中找到對應的路由並獲取元件,最終將其渲染出來。

知其所以然

  • 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


21-Vue-router 除了 router-link 怎麼實現跳轉

分析

vue-router導航有兩種方式:宣告式導航程式設計方式導航

體驗

宣告式導航

<router-link to="/about">Go to About</router-link>

程式設計導航

// literal string path
router.push('/users/eduardo')

// object with path
router.push({ path: '/users/eduardo' })

// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'eduardo' } })

思路

  • 兩種方式
  • 分別闡述使用方式
  • 區別和選擇
  • 原理說明

回答範例

  • vue-router導航有兩種方式:宣告式導航程式設計方式導航
  • 宣告式導航方式使用router-link元件,新增to屬性導航;程式設計方式導航更加靈活,可傳遞呼叫router.push(),並傳遞path字串或者RouteLocationRaw物件,指定path、name、params等資訊
  • 如果頁面中簡單表示跳轉連結,使用router-link最快捷,會渲染一個a標籤;如果頁面是個複雜的內容,比如商品資訊,可以新增點選事件,使用程式設計式導航
  • 實際上內部兩者呼叫的導航函數是一樣的

知其所以然

https://github1s.com/vuejs/router/blob/HEAD/src/RouterLink.ts#L240-L241

routerlink點選跳轉,呼叫的是navigate方法

image-20220626173129790

navigate內部依然呼叫的push


22-Vue3.0 效能提升體現在哪些方面?

分析

vue3在設計時有幾個目標:更小、更快、更友好,這些多數適合效能相關,因此可以圍繞介紹。

思路

  • 總述和效能相關的新特性
  • 逐個說細節
  • 能說點原理更佳

回答範例

  • 我分別從程式碼、編譯、打包三方面介紹vue3效能方面的提升
  • 程式碼層面效能優化主要體現在全新響應式API,基於Proxy實現,初始化時間和記憶體佔用均大幅改進;
  • 編譯層面做了更多編譯優化處理,比如靜態提升、動態標記、事件快取,區塊等,可以有效跳過大量diff過程;
  • 打包時更好的支援tree-shaking,因此整體體積更小,載入更快

體驗

通過playground體驗編譯優化:sfc.vuejs.org

知其所以然

為什麼基於Proxy更快了:初始化時懶處理,使用者存取才做攔截處理,初始化更快:

https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/baseHandlers.ts#L136-L137

輕量的依賴關係儲存:利用WeakMap、Map和Set儲存響應式資料和副作用之間的依賴關係

https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/effect.ts#L19-L20


23-Vue3.0裡為什麼要用 Proxy 替代 defineProperty ?

分析

Vue3中最重大的更新之一就是響應式模組reactivity的重寫。主要的修改就是Proxy替換defineProperty實現響應式。

此變化主要是從效能方面考量。

思路

  • 屬性攔截的幾種方式
  • defineProperty的問題
  • Proxy的優點
  • 其他考量

回答範例

  • JS中做屬性攔截常見的方式有三:: definePropertygetter/settersProxies.
  • Vue2中使用defineProperty的原因是,2013年時只能用這種方式。由於該API存在一些侷限性,比如對於陣列的攔截有問題,為此vue需要專門為陣列響應式做一套實現。另外不能攔截那些新增、刪除屬性;最後defineProperty方案在初始化時需要深度遞迴遍歷待處理的物件才能對它進行完全攔截,明顯增加了初始化的時間。
  • 以上兩點在Proxy出現之後迎刃而解,不僅可以對陣列實現攔截,還能對Map、Set實現攔截;另外Proxy的攔截也是懶處理行為,如果使用者沒有存取巢狀物件,那麼也不會實施攔截,這就讓初始化的速度和記憶體佔用都改善了。
  • 當然Proxy是有相容性問題的,IE完全不支援,所以如果需要IE相容就不合適

知其所以然

Proxy屬性攔截的原理:利用get、set、deleteProperty這三個trap實現攔截

function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {},
        set(target, key, val) {},
        deleteProperty(target, key){}
    })
}

Object.defineProperty屬性攔截原理:利用get、set這兩個trap實現攔截

function defineReactive(obj, key, val) {
    Object.defineReactive(obj, key, {
        get(key) {},
        set(key, val) {}
    })
}

很容易看出兩者的區別!


24-History模式和Hash模式有何區別?

分析

vue-router有3個模式,其中兩個更為常用,那便是history和hash。

兩者差別主要在顯示形式和部署上。

體驗

vue-router4.x中設定模式已經變化:

const router = createRouter({
  history: createWebHashHistory(), // hash模式
  history: createWebHistory(),     // history模式
})

用起來一模一樣

<router-link to="/about">Go to About</router-link>

區別只在url形式

// hash
// 瀏覽器裡的形態:http://xx.com/#/about
// history
// 瀏覽器裡的形態:http://xx.com/about

思路

  • 區別
  • 詳細闡述
  • 實現

回答範例

  • vue-router有3個模式,其中history和hash更為常用。兩者差別主要在顯示形式、seo和部署上。
  • hash模式在位址列顯示的時候是已雜湊的形式:#/xxx,這種方式使用和部署簡單,但是不會被搜尋引擎處理,seo有問題;history模式則建議用在大部分web專案上,但是它要求應用在部署時做特殊設定,伺服器需要做回退處理,否則會出現重新整理頁面404的問題。
  • 底層實現上其實hash是一種特殊的history實現。

知其所以然

hash是一種特殊的history實現:

https://github1s.com/vuejs/router/blob/HEAD/src/history/hash.ts#L31-L32


25-在什麼場景下會用到巢狀路由?

分析

應用的有些介面是由多層級元件組合而來的,這種情況下,url各部分通常對應某個巢狀的元件,vue-router中就可以使用巢狀路由表示這種關係:router.vuejs.org/guide/essen…

6.png

體驗

定義巢狀路由,對應上圖巢狀關係:

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // UserProfile 會被渲染在 User 元件中的 <router-view> 裡
        path: 'profile',
        component: UserProfile,
      },
      {
        // UserPosts 會被渲染在 User 元件中的 <router-view> 裡
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
]

思路

  • 概念和使用場景
  • 使用方式
  • 實現原理

回答範例

  • 平時開發中,應用的有些介面是由多層級元件組合而來的,這種情況下,url各部分通常對應某個巢狀的元件,vue-router中可以使用巢狀路由表示這種關係
  • 表現形式是在兩個路由間切換時,它們有公用的檢視內容。此時通常提取一個父元件,內部放上,從而形成物理上的巢狀,和邏輯上的巢狀對應起來
  • 定義巢狀路由時使用children屬性組織巢狀關係
  • 原理上是在router-view元件內部判斷當前router-view處於巢狀層級的深度,講這個深度作為匹配元件陣列matched的索引,獲取對應渲染元件,渲染之

知其所以然

router-view獲取自己所在的深度:預設0,加1之後傳給後代,同時根據深度獲取匹配路由。

7.png


26-頁面重新整理後vuex的state資料丟失怎麼解決?

分析

這是一道應用題目,很容易想到使用localStorage或資料庫儲存並還原狀態。

但是如何優雅編寫程式碼還是能體現認知水平。

體驗

可以從localStorage中獲取作為狀態初始值:

const store = createStore({
  state () {
    return {
      count: localStorage.getItem('count')
    }
  }
})

業務程式碼中,提交修改狀態同時儲存最新值:雖說實現了,但是每次還要手動重新整理localStorage不太優雅

store.commit('increment')
localStorage.setItem('count', store.state.count)

思路

  • 問題描述
  • 解決方法
  • 談個人理解
  • 三方庫原理探討

回答範例

  • vuex只是在記憶體儲存狀態,重新整理之後就會丟失,如果要持久化就要存起來。
  • localStorage就很合適,提交mutation的時候同時存入localStorage,store中把值取出作為state的初始值即可。
  • 這裡有兩個問題,不是所有狀態都需要持久化;如果需要儲存的狀態很多,編寫的程式碼就不夠優雅,每個提交的地方都要單獨做儲存處理。這裡就可以利用vuex提供的subscribe方法做一個統一的處理。甚至可以封裝一個vuex外掛以便複用。
  • 類似的外掛有vuex-persist、vuex-persistedstate,內部的實現就是通過訂閱mutation變化做統一處理,通過外掛的選項控制哪些需要持久化

知其所以然

可以看一下vuex-persist內部確實是利用subscribe實現的

https://github.com/championswimmer/vuex-persist/blob/master/src/index.ts#L277


27-你覺得vuex有什麼缺點?

分析

相較於redux,vuex已經相當簡便好用了。但模組的使用比較繁瑣,對ts支援也不好。

體驗

使用模組:用起來比較繁瑣,使用模式也不統一,基本上得不到型別系統的任何支援

const store = createStore({
  modules: {
    a: moduleA
  }
})
store.state.a // -> 要帶上 moduleA 的key,內嵌模組的話會很長,不得不配合mapState使用
store.getters.c // -> moduleA裡的getters,沒有namespaced時又變成了全域性的
store.getters['a/c'] // -> 有namespaced時要加path,使用模式又和state不一樣
store.commit('d') // -> 沒有namespaced時變成了全域性的,能同時觸發多個子模組中同名mutation
store.commit('a/d') // -> 有namespaced時要加path,配合mapMutations使用感覺也沒簡化

思路

  • 先誇再貶
  • 使用感受
  • 解決方案

回答範例

  • vuex利用響應式,使用起來已經相當方便快捷了。但是在使用過程中感覺模組化這一塊做的過於複雜,用的時候容易出錯,還要經常檢視檔案
  • 比如:存取state時要帶上模組key,內嵌模組的話會很長,不得不配合mapState使用,加不加namespaced區別也很大,getters,mutations,actions這些預設是全域性,加上之後必須用字串型別的path來匹配,使用模式不統一,容易出錯;對ts的支援也不友好,在使用模組時沒有程式碼提示。
  • 之前Vue2專案中用過vuex-module-decorators的解決方案,雖然型別支援上有所改善,但又要學一套新東西,增加了學習成本。pinia出現之後使用體驗好了很多,Vue3 + pinia會是更好的組合。

知其所以然

下面我們來看看vuex中store.state.x.y這種巢狀的路徑是怎麼搞出來的。

首先是子模組安裝過程:父模組狀態parentState上面設定了子模組名稱moduleName,值為當前模組state物件。放在上面的例子中相當於:store.state['x'] = moduleX.state。此過程是遞迴的,那麼store.state.x.y安裝時就是:store.state['x']['y'] = moduleY.state

if (!isRoot && !hot) {
    // 獲取父模組state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 獲取子模組名稱
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
        // 把子模組state設定到父模組上
        parentState[moduleName] = module.state
    })
}

這下大家明白了吧!

原始碼地址:https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115


28-Composition API 與 Options API 有什麼不同

分析

Vue3最重要更新之一就是Composition API,它具有一些列優點,其中不少是針對Options API暴露的一些問題量身打造。是Vue3推薦的寫法,因此掌握好Composition API應用對掌握好Vue3至關重要。

8.png

https://vuejs.org/guide/extras/composition-api-faq.html

體驗

Composition API能更好的組織程式碼,下面這個程式碼用options api實現

9.png

如果用composition api可以提取為useCount(),用於組合、複用

10.png

思路

  • 總述不同點
  • composition api動機
  • 兩者選擇

回答範例

  • Composition API是一組API,包括:Reactivity API、生命週期勾點、依賴注入,使使用者可以通過匯入函數方式編寫vue元件。而Options API則通過宣告元件選項的物件形式編寫元件。
  • Composition API最主要作用是能夠簡潔、高效複用邏輯。解決了過去Options APImixins的各種缺點;另外Composition API具有更加敏捷的程式碼組織能力,很多使用者喜歡Options API,認為所有東西都有固定位置的選項放置程式碼,但是單個元件增長過大之後這反而成為限制,一個邏輯關注點分散在元件各處,形成程式碼碎片,維護時需要反覆橫跳,Composition API則可以將它們有效組織在一起。最後Composition API擁有更好的型別推斷,對ts支援更友好,Options API在設計之初並未考慮型別推斷因素,雖然官方為此做了很多複雜的型別體操,確保使用者可以在使用Options API時獲得型別推斷,然而還是沒辦法用在mixins和provide/inject上。
  • Vue3首推Composition API,但是這會讓我們在程式碼組織上多花點心思,因此在選擇上,如果我們專案屬於中低複雜度的場景,Options API仍是一個好選擇。對於那些大型,高擴充套件,強維護的專案上,Composition API會獲得更大收益。

可能的追問

  • Composition API能否和Options API一起使用?

29-vue-router中如何保護路由?

分析

路由保護在應用開發過程中非常重要,幾乎每個應用都要做各種路由許可權管理,因此相當考察使用者基本功。

體驗

全域性守衛:

const router = createRouter({ ... })

router.beforeEach((to, from) => {
  // ...
  // 返回 false 以取消導航
  return false
})

路由獨享守衛:

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

元件內的守衛:

const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {
    // 在渲染該元件的對應路由被驗證前呼叫
  },
  beforeRouteUpdate(to, from) {
    // 在當前路由改變,但是該元件被複用時呼叫
  },
  beforeRouteLeave(to, from) {
    // 在導航離開渲染該元件的對應路由時呼叫
  },
}

思路

  • 路由守衛的概念
  • 路由守衛的使用
  • 路由守衛的原理
  • vue-router中保護路由的方法叫做路由守衛,主要用來通過跳轉或取消的方式守衛導航。
  • 路由守衛有三個級別:全域性,路由獨享,元件級。影響範圍由大到小,例如全域性的router.beforeEach(),可以註冊一個全域性前置守衛,每次路由導航都會經過這個守衛,因此在其內部可以加入控制邏輯決定使用者是否可以導航到目標路由;在路由註冊的時候可以加入單路由獨享的守衛,例如beforeEnter,守衛只在進入路由時觸發,因此只會影響這個路由,控制更精確;我們還可以為路由元件新增守衛設定,例如beforeRouteEnter,會在渲染該元件的對應路由被驗證前呼叫,控制的範圍更精確了。
  • 使用者的任何導航行為都會走navigate方法,內部有個guards佇列按順序執行使用者註冊的守衛勾點函數,如果沒有通過驗證邏輯則會取消原有的導航。

知其所以然

runGuardQueue(guards)鏈式的執行使用者在各級別註冊的守衛勾點函數,通過則繼續下一個級別的守衛,不通過進入catch流程取消原本導航。

【相關視訊教學推薦:】