我為什么使用 Vue:Vue 常見面試題整理

談談你對 Vue 的理解?

根據官方說法,Vue 是一套用于構建用戶界面的漸進式框架。Vue 的設計受到了 MVVM 的啟發。Vue 的兩個核心是數據驅動組件系統。

但我為什么使用 Vue,有以下幾個原因:

  • Vue 對于前端初學者比較友好。一個 Vue 文件的結構和原生 HTML 保持了高度相似,分為靜態頁面,用于放置 html 標簽,和 script,用于處理用戶操作和業務邏輯,最后是 style 樣式,用于書寫 CSS 代碼,這種寫法可以讓初學者感到習慣。

  • 其次,Vue 提供了許多 JS 定制化的操作,比如 v-bind 和事件監聽的 @ 符號,開發者可以直接使用,從而減少一些重復代碼的書寫。

  • 最后,就是 Vue 提供一套高效的響應式的系統用于更新 DOM,可以讓開發者專注于處理業務而非技術實現。

什么是 MVVM,可以介紹一下嗎?

MVVM,即 Model–View–ViewModel,是一種軟件架構模式。

  • Model

    即模型,是指代表真實狀態內容的領域模型(面向對象),或指代表內容的數據訪問層(以數據為中心)。

  • View

    即視圖,是用戶在屏幕上看到的結構、布局和外觀(UI)。

  • ViewModel

    即視圖模型,是暴露公共屬性和命令的視圖的抽象。用于把 ModelView 關聯起來。ViewModel 負責把 Model 的數據同步到 View 顯示出來,還負責把 View 的修改同步回 Model

MVVM

在 MVVM 架構下,ViewModel 之間并沒有直接的聯系,而是通過 ViewModel 進行交互,ModelViewModel 之間的交互是雙向的,View 數據的變化會同步到 Model 中,而 Model 數據的變化也會立即反應到 View 上。

因此開發者只需關注業務邏輯,不需要手動操作 DOM,不需要關注數據狀態的同步問題,復雜的數據狀態維護完全由 MVVM 來統一管理。

Vue 是如何實現數據雙向綁定的?

Vue 實現數據雙向綁定主要是采用數據劫持結合發布者-訂閱者模式的方式。具體實現就是整合 Observer,Compiler 和 Watcher 三者。

  • Observer

    觀察者。Vue 通過 Observer 對數據對象的所有屬性進行監聽,當把一個普通對象傳給 Vue 實例的 data 選項時,Observer 將遍歷它的所有屬性,并為其添加 gettersetter。getter 將收集此屬性所有的訂閱者,setter 將在屬性發生變動的時候,重新為此屬性賦值,并通知訂閱者調用其對應的更新函數。

    在 Vue 2 中是通過 ES5 的 Object.defineProperty() 方法實現。

    在 Vue 3 中是通過 ES6 的 new Proxy() 實現的。

  • Compiler

    模板編譯器。它的作用是對每個元素節點的指令 v- 和模板語法 {{}} 進行掃描,替換對應的真實數據,或綁定相應的事件函數。

  • Watcher

    發布者/訂閱者。Watcher 作為連接 Observer 和 Compiler 的橋梁,能夠訂閱并收到每個屬性變動的通知,然后執行相應的回調函數。Compiler 在編譯時通過 Watcher 綁定對應的數據更新回調函數,Observer 在監聽到數據變化時執行此回調。在 Observer 中,Watcher 就是訂閱者,在 Compiler 中,Watcher 就是發布者。

v-model 的原理?

v-model 是 vue 的一個語法糖,它用于監聽數據的改變并將數據更新。以 input 元素為例:

<el-input v-model="foo" />

其實就等價于

<el-input :value="foo" @input="foo = $event" />

如何在組件中實現 v-model ?

在 Vue 2 組件中實現 v-model,只需定義 model 屬性即可。

export default {
  model: {
    prop: "value", // 屬性
    event: "input", // 事件
  },
}

在 Vue 3 組合式 API 實現 v-model,需要定義 modelValue 參數,和 emits 方法。

defineProps({
  modelValue: { type: String, default: "" },
})

const emits = defineEmits(["update:modelValue"])

function onInput(val) {
  emits("update:modelValue", val)
}

當數據改變時,Vue 是如何更新 DOM 的?(Diff 算法和虛擬 DOM)

當我們修改了某個數據時,如果直接重新渲染到真實 DOM,開銷是很大的。Vue 為了減少開銷和提高性能采用了 Diff 算法。當數據發生改變時,Observer 會通知所有 WatcherWatcher 就會調用 patch() 方法(Diff 的具體實現),把變化的內容更新到真實的 DOM,俗稱打補丁。

Diff 算法會對新舊節點進行同層級比較,當兩個新舊節點是相同節點的時候,再去比較他們的子節點(如果是文本則直接更新文本內容),逐層比較然后找到最小差異部分,進行 DOM 更新。如果不是相同節點,則刪除之前的內容,重新渲染。

逐層比較

patch() 方法先根據真實 DOM 生成一顆虛擬 DOM,保存到變量 oldVnode,當某個數據改變后會生成一個新的 Vnode,然后 VnodeoldVnode 進行對比,發現有不一樣的地方就直接修改在真實 DOM 上,最后再返回新節點作為下次更新的 oldVnode。

什么是虛擬 DOM ?

虛擬 DOM(Virtual DOM)就是將真實 DOM 的主要數據抽取出來,并以對象的形式表達。

比如真實 DOM 如下:

<div id="id" class="cls">
  <h1>123</h1>
</div>

對應的虛擬 DOM 就是(偽代碼):

{
  tag: 'div',
  sel: 'div#id.cls',
  children: [
    { tag: 'h1', text: '123' }
  ]
}

Vue 中的 key 有什么用?

  • 在 Vue 中,key 被用來作為 VNode 的唯一標識。

  • key 主要用在 Vue 的虛擬 DOM Diff 算法,在新舊節點對比時作為識別 VNode 的一個線索。然后找到正確的位置插入或更新節點。如果新舊節點中提供了 key,能更快速地進行比較及復用。反之,Vue 會盡可能復用相同類型元素。

    <ul>
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
    
  • 手動改變 key 值,可以強制 DOM 進行重新渲染。

    <transition>
      <span :key="text">{{ text }}</span>
    </transition>
    

Vue 3 對 diff 算法進行了哪些優化

在 Vue 2 中,每當數據發生變化時,Vue 會創建一個新的虛擬 DOM 樹,并對整個虛擬 DOM 樹進行遞歸比較,即使其中大部分內容是靜態的,最后再找到不同的節點,然后進行更新。

Vue 3 引入了靜態標記的概念,通過靜態標記,Vue 3 可以將模板中的靜態內容和動態內容區分開來。這樣,在更新過程中,Vue 3 只會關注動態部分的比較,而對于靜態內容,它將跳過比較的步驟,從而避免了不必要的比較,提高了性能和效率。

Vue 實例的生命周期鉤子都有哪些?

每個 Vue 實例在被創建時都要經過一系列的初始化過程——例如,需要設置數據監聽、編譯模板、將實例掛載到 DOM 并在數據變化時更新 DOM 等。同時在這個過程中也會運行一些叫做生命周期鉤子的函數,這給了用戶在不同階段添加自己的代碼的機會。

Vue 2 有以下鉤子:

  • beforeCreate

    實例初始化之前,$eldata 都為 undefined。

  • created

    實例創建完成,data 已經綁定。

  • beforeMount

    <template>data 生成虛擬 DOM 節點,可以訪問到 $el,但還沒有渲染到 html 上。

  • mounted

    實例掛載完成,渲染到 html 頁面中。

  • beforeUpdate

    data 更新之前,虛擬 DOM 重新渲染之前。

  • updated

    由于 data 更新導致的虛擬 DOM 重新渲染之后。

  • activated

    keep-alive 專用,實例被激活時調用。

  • deactivated

    keep-alive 專用,實例被移除時調用。

  • beforeDestroy

    實例銷毀之前(實例仍然可用)。

  • destroyed

    實例銷毀之后。所有的事件監聽器會被移除,所有的子實例也會被銷毀,但 DOM 節點依舊存在。該鉤子在服務器端渲染期間不被調用。

第一次頁面加載會觸發這四個鉤子:

  • beforeCreate

  • created

  • beforeMount

  • mounted

Vue 3 組合式 API 有以下鉤子:

  • onBeforeMount()

    在組件被掛載之前被調用。

  • onMounted()

    在組件掛載完成后執行。

  • onBeforeUpdate()

    在組件即將因為響應式狀態變更而更新其 DOM 樹之前調用。

  • onUpdated()

    在組件因為響應式狀態變更而更新其 DOM 樹之后調用。

  • onBeforeUnmount()

    在組件實例被卸載之前調用。

  • onUnmounted()

    在組件實例被卸載之后調用。相當于 Vue 2 的 destroyed。

  • onErrorCaptured()

    在捕獲了后代組件傳遞的錯誤時調用。

  • onRenderTracked()

    當組件渲染過程中追蹤到響應式依賴時調用。只在開發環境生效。

  • onRenderTriggered()

    當響應式依賴的變更觸發了組件渲染時調用。只在開發環境生效。

  • onActivated()

    keep-alive 專用,當組件被插入到 DOM 中時調用。

  • onDeactivated()

    keep-alive 專用,當組件從 DOM 中被移除時調用。

  • onServerPrefetch()

    在組件實例在服務器上被渲染之前調用。只在 SSR 模式下生效。

$nextTick 的使用場景和原理

  • $nextTick 是什么?

    在下次 DOM 更新循環結束之后執行的一個方法。

    export default {
      data() {
        return {
          message: "Hello Vue!",
        }
      },
      methods: {
        example() {
          // 修改數據
          this.message = "changed"
          // DOM 尚未更新
          this.$nextTick(() => {
            // DOM 現在更新了
            console.log("DOM 現在更新了")
          })
        },
      },
    }
    
  • $nextTick 的使用場景

    在修改數據之后使用這個方法,用于獲取更新后的 DOM。

  • 使用 $nextTick 的原理

    在下次循環結束之后,Vue 會自動觸發一個 update 事件,在這個事件中會調用所有的 $nextTick 回調。

為什么 Vue 組件中的 data 必須是函數?

因為在 Vue 中組件是可以被復用的,組件復用其實就是創建多個 Vue 實例,實例之間共享 prototype.data 屬性,當 data 的值引用的是同一個對象時,改變其中一個就會影響其他組件,造成互相污染,而改用函數的形式將數據 return 出去,則每次復用都是嶄新的對象。

這里我們舉個例子:

function Component() {}

Component.prototype.data = {
  name: "vue",
  language: "javascript",
}

const A = new Component()
const B = new Component()

A.data.language = "typescript"

console.log(A.data) // { name: 'vue', language: 'typescript' }
console.log(B.data) // { name: 'vue', language: 'typescript' }

此時,A 和 B 的 data 都指向了同一個內存地址,language 都變成了 'typescript'。

我們改成函數式的寫法,就不會有這樣的問題了。

function Component() {
  this.data = this.data()
}

Component.prototype.data = function () {
  return { name: "vue", language: "javascript" }
}

const A = new Component()
const B = new Component()

A.data.language = "typescript"

console.log(A.data) // { name: 'vue', language: 'typescript' }
console.log(B.data) // { name: 'vue', language: 'javascript' }

所以組件的 data 選項必須是一個函數,該函數返回一個獨立的拷貝,這樣就不會出現數據相互污染的問題。

組件之間是如何進行通信?

  • 父子組件通信

    • 通過 props 傳參

    • 通過 $emit 觸發

    • 通過 $refs 調用子組件方法

  • 兄弟組件通信

    • 狀態管理 vuex

    • 事件總線 EventBus

      // event-bus.js
      import Vue from "vue"
      export default new Vue()
      
      // 組件 A
      import Bus from "event-bus.js"
      
      export default {
        methods: {
          handleClick(val) {
            Bus.$emit("functionName", val)
          },
        },
      }
      
      // 組件 B
      import Bus from "event-bus.js"
      
      export default {
        created() {
          Bus.$on("functionName", val => {
            console.log(val)
          })
        },
      }
      
    • localStoragesessionStorageCookies

Vue 項目中做過哪些性能優化?

  • UI 庫按需加載,減小打包體積,以 ElementUI 為例:

    // main.js
    import { Button, Select } from "element-ui"
    
    Vue.use(Button)
    Vue.use(Select)
    
  • 路由按需加載

    // router.js
    export default new VueRouter({
      routes: [
        { path: "/", component: () => import("@/components/Home") },
        { path: "/about", component: () => import("@/components/About") },
      ],
    })
    
  • 組件銷毀后把同時銷毀全局變量和移除事件監聽和清除定時器,防止內存泄漏

    beforeDestroy() {
      clearInterval(this.timer)
      window.removeEventListener('resize', this.handleResize)
    },
    
  • 合理使用 v-if 和 v-show 使用

Vue 和 React 的區別?

這里

Vue 3.x 帶來了哪些新的特性和性能方面的提升?

  1. 引入了 Composition API(組合式 API)。允許開發者更靈活地組織和重用組件邏輯。它使用函數而不是選項對象來組織組件的代碼,使得代碼更具可讀性和維護性。

  2. 多根組件??梢灾苯釉?template 中使用多個根級別的元素,而不需要額外的包裝元素。這樣更方便地組織組件的結構。

  3. 引入了 Teleport(傳送)。可以將組件的內容渲染到指定 DOM 節點的新特性。一般用于創建全局彈窗和對話框等組件。

  4. 響應式系統升級。從 defineProperty 升級到 ES2015 原生的 Proxy,不需要初始化遍歷所有屬性,就可以監聽新增和刪除的屬性。

  5. 編譯優化。重寫了虛擬 DOM,提升了渲染速度。diff 時靜態節點會被直接跳過。

  6. 源碼體積優化。移除了一些非必要的特性,如 filter,一些新增的模塊也將會被按需引入,減小了打包體積。

  7. 打包優化。更強的 Tree Shaking,可以過濾不使用的模塊,沒有使用到的組件,比如過渡(transition)組件,則打包時不會包含它。

為什么 Vue 3.x 采用了 Proxy 拋棄了 Object.defineProperty() ?

  • Proxy 可以代理任何對象,包括數組,而 Vue 2 中是通過重寫數組的以下七種方法實現的。

    • push()(將一個或多個元素添加到數組的末尾,并返回該數組的新長度)

    • pop()(移除并返回數組的最后一個元素)

    • unshift()(將一個或多個元素添加到數組的開頭,并返回該數組的新長度)

    • shift()(移除并返回數組的第一個元素)

    • splice()(刪除數組中的一個或多個元素,并將其返回)

    • sort()(對數組進行排序)

    • reverse()(對數組進行反轉)

  • Proxy 可以直接監聽整個對象而非屬性,而 Object.defineProperty() 只能先遍歷對象屬性再去進行監聽。相比之下 Proxy 更加簡潔,更加高效,更加安全。

  • Proxy 返回的是一個新對象,我們可以只操作新的對象達到目的。

    const cat = {
      name: "Tom",
    }
    
    const myCat = new Proxy(cat, {
      get(target, property) {
        console.log(`我的 ${property} 被讀取了`)
        return property in target ? target[property] : undefined
      },
      set(target, property, value) {
        console.log(`我的 ${property} 被設置成了 ${value}`)
        target[property] = value
        return true
      },
    })
    
    myCat.name // expected output: 我被讀取了:name
    myCat.name = "Kitty" // expected output: 我的 name 被設置成了 Kitty
    
  • Object.defineProperty() 的本質是在一個對象上定義一個新屬性,或者修改一個現有屬性。

    const cat = {
      name: "Tom",
    }
    
    Object.defineProperty(cat, "name", {
      get() {
        console.log(`我被讀取了`)
      },
      set(value) {
        console.log(`我被設置成了 ${value}`)
      },
    })
    
    cat.name // expected output: 我被讀取了
    cat.name = "Kitty" // expected output: 我被設置成了 Kitty
    
  • 而 Proxy 天生用于代理一個對象,它有 13 種基本操作的攔截方法,是 Object.defineProperty() 不具備的。

    • apply()(攔截函數的調用)

    • construct()(攔截構造函數的調用)

    • defineProperty()(攔截屬性的定義)

    • deleteProperty()(攔截屬性的刪除)

    • get()(攔截對象屬性的讀?。?/p>

    • getOwnPropertyDescriptor()(攔截對象屬性的描述)

    • getPrototypeOf()(攔截對象的原型)

    • has()(攔截對象屬性的檢查)

    • isExtensible()(攔截對象是否可擴展的檢查)

    • ownKeys()(攔截對象的屬性列表)

    • preventExtensions()(攔截對象是否可擴展的設置)

    • set()(攔截對象屬性的設置)

    • setPrototypeOf()(攔截對象的原型的設置)

Composition API(組合式 API)與 Options API(選項式 API)有什么區別?

  • Options API 會將組件中的同一邏輯相關的代碼拆分到不同選項,比如 dataprops、methods 等,而使用 Composition API 較為靈活,開發者可以將同一個邏輯的相關代碼放在一起。

  • Composition API 通過 Vue 3.x 新增的 setup 選項進行使用,該選項會在組件創建之前執行,第一個參數 props,第二個參數 context,return 的所有內容都會暴露給組件的其余部分 (計算屬性、方法、生命周期鉤子等等) 以及組件的模板。

  • Composition API 上的生命周期鉤子與 Options API 基本相同,但需要添加前綴 on,比如 onMounted、onUpdated 等。

v-for 和 v-if 可以同時使用嗎

可以同時使用,但不推薦,具體原因參考官方說明

在 Vue 3 中,當 v-if 和 v-for 同時存在于一個節點上時,v-if 比 v-for 的優先級更高,此時 v-if 無法訪問 v-for 中的對象。

當確實需要條件遍歷渲染的話,有以下幾個方法:

  • fitler
<li v-for="todo in todos.filter(todo => !todo.isDone)">{{ todo.name }}</li>

使用數組的 filter 的方法可以提前對不需要的數據進行過濾,根源上解決這個問題。

  • 使用 v-show
<li v-for="todo in todos" v-show="!todo.isDone">{{ todo.name }}</li>

v-show 和 v-if 都可以用于隱藏某個元素,但 v-if 用于決定是否渲染,而 v-show 則使用 display 屬性決定是否顯示。此時可以避免 v-if 和 v-for 同時使用造成的的渲染問題。

  • 添加額外的標簽
<template v-for="todo in todos">
  <li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>

添加額外的標簽,根據層級的不同,可以自己決定 v-if 和 v-for 的優先級,這種方法更加靈活也更容易理解,但會有更深的代碼結構。


參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。