最全的 Vue 面試題 - 詳解答案

前言

本文整理了高頻出現的 Vue 相關面試題並且附帶詳解答案 難度分爲簡單 中等 困難 三種類型 大家可以先不看答案自測一下自己的 Vue 水平哈 


簡單

1 MVC 和 MVVM 區別

MVC

MVC 全名是 Model View Controller,是模型 (model)-視圖(view)-控制器(controller) 的縮寫,一種軟件設計典範

mvc.png

MVC 的思想:一句話描述就是 Controller 負責將 Model 的數據用 View 顯示出來,換句話說就是在 Controller 裏面把 Model 的數據賦值給 View。

MVVM

MVVM 新增了 VM 類

mvvm.png

MVVM 與 MVC 最大的區別就是:它實現了 View 和 Model 的自動同步,也就是當 Model 的屬性改變時,我們不用再自己手動操作 Dom 元素,來改變 View 的顯示,而是改變屬性後該屬性對應 View 層顯示會自動改變(對應 Vue 數據驅動的思想)

整體看來,MVVM 比 MVC 精簡很多,不僅簡化了業務與界面的依賴,還解決了數據頻繁更新的問題,不用再用選擇器操作 DOM 元素。因爲在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也觀察不到 View,這種低耦合模式提高代碼的可重用性

注意:Vue 並沒有完全遵循 MVVM 的思想 這一點官網自己也有說明

vue-mvvm.png

那麼問題來了 爲什麼官方要說 Vue 沒有完全遵循 MVVM 思想呢?

  • 嚴格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了 $refs 這個屬性,讓 Model 可以直接操作 View,違反了這一規定,所以說 Vue 沒有完全遵循 MVVM。

2 爲什麼 data 是一個函數

組件中的 data 寫成一個函數,數據以函數返回值形式定義,這樣每複用一次組件,就會返回一份新的 data,類似於給每個組件實例創建一個私有的數據空間,讓各個組件實例維護各自的數據。而單純的寫成對象形式,就使得所有組件實例共用了一份 data,就會造成一個變了全都會變的結果

3 Vue 組件通訊有哪幾種方式

  1. props 和父組件向子組件傳遞數據是通過傳遞的,子組件傳遞數據給父組件是通過 emit 觸發事件來做到的

  2. children 獲取當前組件的父組件和當前組件的子組件

  3. 和 listeners A->B->C。Vue 2.4 開始提供了和 listeners 來解決這個問題

  4. 父組件中通過 provide 來提供變量,然後在子組件中通過 inject 來注入變量。(官方不推薦在實際業務中使用,但是寫組件庫時很常用)

  5. $refs 獲取組件實例

  6. envetBus 兄弟組件數據傳遞 這種情況下可以使用事件總線的方式

  7. vuex 狀態管理

4 Vue 的生命週期方法有哪些 一般在哪一步發請求

beforeCreate 在實例初始化之後,數據觀測 (data observer) 和 event/watcher 事件配置之前被調用。在當前階段 data、methods、computed 以及 watch 上的數據和方法都不能被訪問

created 實例已經創建完成之後被調用。在這一步,實例已完成以下的配置:數據觀測 (data observer),屬性和方法的運算, watch/event 事件回調。這裏沒有如果非要想與進行交互,可以通過 nextTick 來訪問 Dom

beforeMount 在掛載開始之前被調用:相關的 render 函數首次被調用。

mounted 在掛載完成後發生,在當前階段,真實的 Dom 掛載完畢,數據完成雙向綁定,可以訪問到 Dom 節點

beforeUpdate 數據更新時調用,發生在虛擬 DOM 重新渲染和打補丁(patch)之前。可以在這個鉤子中進一步地更改狀態,這不會觸發附加的重渲染過程

updated 發生在更新完成之後,當前階段組件 Dom 已完成更新。要注意的是避免在此期間更改數據,因爲這可能會導致無限循環的更新,該鉤子在服務器端渲染期間不被調用。

beforeDestroy 實例銷燬之前調用。在這一步,實例仍然完全可用。我們可以在這時進行善後收尾工作,比如清除計時器。

destroyed Vue 實例銷燬後調用。調用後,Vue 實例指示的所有東西都會解綁定,所有的事件監聽器會被移除,所有的子實例也會被銷燬。該鉤子在服務器端渲染期間不被調用。

activated keep-alive 專屬,組件被激活時調用

deactivated keep-alive 專屬,組件被銷燬時調用

異步請求在哪一步發起?

可以在鉤子函數 created、beforeMount、mounted 中進行異步請求,因爲在這三個鉤子函數中,data 已經創建,可以將服務端端返回的數據進行賦值。

如果異步請求不需要依賴 Dom 推薦在 created 鉤子函數中調用異步請求,因爲在 created 鉤子函數中調用異步請求有以下優點:

5 v-if 和 v-show 的區別

v-if 在編譯過程中會被轉化成三元表達式, 條件不滿足時不渲染此節點。

v-show 會被編譯成指令,條件不滿足時控制樣式將對應節點隱藏 (display:none)

使用場景

v-if 適用於在運行時很少改變條件,不需要頻繁切換條件的場景

v-show 適用於需要非常頻繁切換條件的場景

擴展補充:display:none、visibility:hidden 和 opacity:0 之間的區別?

display.png

6 說說 vue 內置指令

內置指令. png

7 怎樣理解 Vue 的單向數據流

數據總是從父組件傳到子組件,子組件沒有權利修改父組件傳過來的數據,只能請求父組件對原始數據進行修改。這樣會防止從子組件意外改變父級組件的狀態,從而導致你的應用的數據流向難以理解。

注意:在子組件直接用 v-model 綁定父組件傳過來的 prop 這樣是不規範的寫法 開發環境會報警告

如果實在要改變父組件的 prop 值 可以再 data 裏面定義一個變量 並用 prop 的值初始化它 之後用 $emit 通知父組件去修改

8 computed 和 watch 的區別和運用的場景

computed 是計算屬性,依賴其他屬性計算值,並且 computed 的值有緩存,只有當計算值變化纔會返回內容,它可以設置 getter 和 setter。

watch 監聽到值的變化就會執行回調,在回調中可以進行一些邏輯操作。

計算屬性一般用在模板渲染中,某個值是依賴了其它的響應式對象甚至是計算屬性計算而來;而偵聽屬性適用於觀測某個值的變化去完成一段複雜的業務邏輯

計算屬性原理詳解 傳送門

偵聽屬性原理詳解 傳送門

9 v-if 與 v-for 爲什麼不建議一起使用

v-for 和 v-if 不要在同一個標籤中使用, 因爲解析時先解析 v-for 再解析 v-if。如果遇到需要同時使用時可以考慮寫成計算屬性的方式。


中等

10 Vue2.0 響應式數據的原理

整體思路是數據劫持 + 觀察者模式

對象內部通過 defineReactive 方法,使用 Object.defineProperty 將屬性進行劫持(只會劫持已經存在的屬性),數組則是通過重寫數組方法來實現。當頁面使用對應屬性時,每個屬性都擁有自己的 dep 屬性,存放他所依賴的 watcher(依賴收集),當屬性變化後會通知自己對應的 watcher 去更新 (派發更新)。

相關代碼如下

class Observer {
  // 觀測值
  constructor(value) {
    this.walk(value);
  }
  walk(data) {
    // 對象上的所有屬性依次進行觀測
    let keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}
// Object.defineProperty數據劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
  observe(value); // 遞歸關鍵
  // --如果value還是一個對象會繼續走一遍odefineReactive 層層遍歷一直到value不是對象才停止
  //   思考?如果Vue數據嵌套層級過深 >>性能會受影響
  Object.defineProperty(data, key, {
    get() {
      console.log("獲取值");

      //需要做依賴收集過程 這裏代碼沒寫出來
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      console.log("設置值");
      //需要做派發更新過程 這裏代碼沒寫出來
      value = newValue;
    },
  });
}
export function observe(value) {
  // 如果傳過來的是對象或者數組 進行屬性劫持
  if (
    Object.prototype.toString.call(value) === "[object Object]" ||
    Array.isArray(value)
  ) {
    return new Observer(value);
  }
}

響應式數據原理詳解 傳送門

11 Vue 如何檢測數組變化

數組考慮性能原因沒有用 defineProperty 對數組的每一項進行攔截,而是選擇對 7 種數組(push,shift,pop,splice,unshift,sort,reverse)方法進行重寫 (AOP 切片思想)

所以在 Vue 中修改數組的索引和長度是無法監控到的。需要通過以上 7 種變異方法修改數組纔會觸發數組對應的 watcher 進行更新

相關代碼如下

// src/obserber/array.js
// 先保留數組原型
const arrayProto = Array.prototype;
// 然後將arrayMethods繼承自數組原型
// 這裏是面向切片編程思想(AOP)--不破壞封裝的前提下,動態的擴展功能
export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "reverse",
  "sort",
];
methodsToPatch.forEach((method) ={
  arrayMethods[method] = function (...args) {
    //   這裏保留原型方法的執行結果
    const result = arrayProto[method].apply(this, args);
    // 這句話是關鍵
    // this代表的就是數據本身 比如數據是{a:[1,2,3]} 那麼我們使用a.push(4)  this就是a  ob就是a.__ob__ 這個屬性就是上段代碼增加的 代表的是該數據已經被響應式觀察過了指向Observer實例
    const ob = this.__ob__;

    // 這裏的標誌就是代表數組有新增操作
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    // 如果有新增的元素 inserted是一個數組 調用Observer實例的observeArray對數組每一項進行觀測
    if (inserted) ob.observeArray(inserted);
    // 之後咱們還可以在這裏檢測到數組改變了之後從而觸發視圖更新的操作--後續源碼會揭曉
    return result;
  };
});

數組的觀測原理詳解 傳送門

12 vue3.0 用過嗎 瞭解多少

Vue3.0 新特性以及使用經驗總結 傳送門

13 Vue3.0 和 2.0 的響應式原理區別

Vue3.x 改用 Proxy 替代 Object.defineProperty。因爲 Proxy 可以直接監聽對象和數組的變化,並且有多達 13 種攔截方法。

相關代碼如下

import { mutableHandlers } from "./baseHandlers"; // 代理相關邏輯
import { isObject } from "./util"; // 工具方法

export function reactive(target) {
  // 根據不同參數創建不同響應式對象
  return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
  if (!isObject(target)) {
    return target;
  }
  const observed = new Proxy(target, baseHandler);
  return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {
  return function get(target, key, receiver) {
    // 對獲取的值進行放射
    const res = Reflect.get(target, key, receiver);
    console.log("屬性獲取", key);
    if (isObject(res)) {
      // 如果獲取的值是對象類型,則返回當前對象的代理對象
      return reactive(res);
    }
    return res;
  };
}
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];
    const hadKey = hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) {
      console.log("屬性新增", key, value);
    } else if (hasChanged(value, oldValue)) {
      console.log("屬性值被修改", key, value);
    }
    return result;
  };
}
export const mutableHandlers = {
  get, // 當獲取屬性時調用此方法
  set, // 當修改屬性時調用此方法
};

14 Vue 的父子組件生命週期鉤子函數執行順序

父 beforeCreate-> 父 created-> 父 beforeMount-> 子 beforeCreate-> 子 created-> 子 beforeMount-> 子 mounted-> 父 mounted

父 beforeUpdate-> 子 beforeUpdate-> 子 updated-> 父 updated

父 beforeUpdate-> 父 updated

父 beforeDestroy-> 子 beforeDestroy-> 子 destroyed-> 父 destroyed

15 虛擬 DOM 是什麼 有什麼優缺點

由於在瀏覽器中操作 DOM 是很昂貴的。頻繁的操作 DOM,會產生一定的性能問題。這就是虛擬 Dom 的產生原因。Vue2 的 Virtual DOM 借鑑了開源庫 snabbdom 的實現。Virtual DOM 本質就是用一個原生的 JS 對象去描述一個 DOM 節點,是對真實 DOM 的一層抽象。

優點:

  1. 保證性能下限:框架的虛擬 DOM 需要適配任何上層 API 可能產生的操作,它的一些 DOM 操作的實現必須是普適的,所以它的性能並不是最優的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虛擬 DOM 至少可以保證在你不需要手動優化的情況下,依然可以提供還不錯的性能,即保證性能的下限;

  2. 無需手動操作 DOM:我們不再需要手動去操作 DOM,只需要寫好 View-Model 的代碼邏輯,框架會根據虛擬 DOM 和 數據雙向綁定,幫我們以可預期的方式更新視圖,極大提高我們的開發效率;

  3. 跨平臺:虛擬 DOM 本質上是 JavaScript 對象, 而 DOM 與平臺強相關,相比之下虛擬 DOM 可以進行更方便地跨平臺操作,例如服務器渲染、weex 開發等等。

缺點:

  1. 無法進行極致優化:雖然虛擬 DOM + 合理的優化,足以應對絕大部分應用的性能需求,但在一些性能要求極高的應用中虛擬 DOM 無法進行鍼對性的極致優化。

  2. 首次渲染大量 DOM 時,由於多了一層虛擬 DOM 的計算,會比 innerHTML 插入慢。

16 v-model 原理

v-model 只是語法糖而已

v-model 在內部爲不同的輸入元素使用不同的 property 並拋出不同的事件:

注意: 對於需要使用輸入法 (如中文、日文、韓文等) 的語言,你會發現 v-model 不會在輸入法組合文字過程中得到更新。

在普通標籤上

 <input v-model="sth" />  //這一行等於下一行
    <input v-bind:value="sth" v-on:input="sth = $event.target.value" />

在組件上

<currency-input v-model="price"></currentcy-input>
<!--上行代碼是下行的語法糖
 <currency-input :value="price" @input="price = arguments[0]"></currency-input>
-->

<!-- 子組件定義 -->
Vue.component('currency-input'{
 template: `
  <span>
   <input
    ref="input"
    :value="value"
    @input="$emit('input', $event.target.value)"
   >
  </span>
 `,
 props: ['value'],
})

17 v-for 爲什麼要加 key

如果不使用 key,Vue 會使用一種最大限度減少動態元素並且儘可能的嘗試就地修改 / 複用相同類型元素的算法。key 是爲 Vue 中 vnode 的唯一標記,通過這個 key,我們的 diff 操作可以更準確、更快速

更準確:因爲帶 key 就不是就地複用了,在 sameNode 函數 a.key === b.key 對比中可以避免就地複用的情況。所以會更加準確。

更快速:利用 key 的唯一性生成 map 對象來獲取對應節點,比遍歷方式更快

相關代碼如下

// 判斷兩個vnode的標籤和key是否相同 如果相同 就可以認爲是同一節點就地複用
function isSameVnode(oldVnode, newVnode) {
  return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}

// 根據key來創建老的兒子的index映射表  類似 {'a':0,'b':1} 代表key爲'a'的節點在第一個位置 key爲'b'的節點在第二個位置
function makeIndexByKey(children) {
  let map = {};
  children.forEach((item, index) ={
    map[item.key] = index;
  });
  return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);

diff 算法詳解 傳送門

18 Vue 事件綁定原理

原生事件綁定是通過 addEventListener 綁定給真實元素的,組件事件綁定是通過 Vue 自定義的 $on 實現的。如果要在組件上使用原生事件,需要加. native 修飾符,這樣就相當於在父組件中把子組件當做普通 html 標籤,然後加上原生事件。

、emit 是基於發佈訂閱模式的,維護一個事件中心,on 的時候將事件按名稱存在事件中心裏,稱之爲訂閱者,然後 emit 將對應的事件進行發佈,去執行事件中心裏的對應的監聽器

手寫發佈訂閱原理 傳送門

19 vue-router 路由鉤子函數是什麼 執行順序是什麼

路由鉤子的執行流程, 鉤子函數種類有: 全局守衛、路由守衛、組件守衛

完整的導航解析流程:

  1. 導航被觸發。

  2. 在失活的組件裏調用 beforeRouteLeave 守衛。

  3. 調用全局的 beforeEach 守衛。

  4. 在重用的組件裏調用 beforeRouteUpdate 守衛 (2.2+)。

  5. 在路由配置裏調用 beforeEnter。

  6. 解析異步路由組件。

  7. 在被激活的組件裏調用 beforeRouteEnter。

  8. 調用全局的 beforeResolve 守衛 (2.5+)。

  9. 導航被確認。

  10. 調用全局的 afterEach 鉤子。

  11. 觸發 DOM 更新。

  12. 調用 beforeRouteEnter 守衛中傳給 next 的回調函數,創建好的組件實例會作爲回調函數的參數傳入。

20 vue-router 動態路由是什麼 有什麼問題

我們經常需要把某種模式匹配到的所有路由,全都映射到同個組件。例如,我們有一個 User 組件,對於所有 ID 各不相同的用戶,都要使用這個組件來渲染。那麼,我們可以在 vue-router 的路由路徑中使用 “動態路徑參數”(dynamic segment) 來達到這個效果:

const User = {
  template: "<div>User</div>",
};

const router = new VueRouter({
  routes: [
    // 動態路徑參數 以冒號開頭
    { path: "/user/:id", component: User },
  ],
});

問題: vue-router 組件複用導致路由參數失效怎麼辦?

解決方法:

  1. 通過 watch 監聽路由參數再發請求
watch: { //通過watch來監聽路由變化

 "$route"function(){
 this.getData(this.$route.params.xxx);
 }
}
  1. 用 :key 來阻止 “複用”
<router-view :key="$route.fullPath" />

21 談一下對 vuex 的個人理解

vuex 是專門爲 vue 提供的全局狀態管理系統,用於多個組件中數據共享、數據緩存等。(無法持久化、內部核心原理是通過創造一個全局實例 new Vue)

主要包括以下幾個模塊:

22 Vuex 頁面刷新數據丟失怎麼解決

需要做 vuex 數據持久化 一般使用本地存儲的方案來保存數據 可以自己設計存儲方案 也可以使用第三方插件

推薦使用 vuex-persist 插件,它就是爲 Vuex 持久化存儲而生的一個插件。不需要你手動存取 storage ,而是直接將狀態保存至 cookie 或者 localStorage 中

23 Vuex 爲什麼要分模塊並且加命名空間

模塊: 由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常複雜時,store 對象就有可能變得相當臃腫。爲了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊。

命名空間:默認情況下,模塊內部的 action、mutation 和 getter 是註冊在全局命名空間的——這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。如果希望你的模塊具有更高的封裝度和複用性,你可以通過添加 namespaced: true 的方式使其成爲帶命名空間的模塊。當模塊被註冊後,它的所有 getter、action 及 mutation 都會自動根據模塊註冊的路徑調整命名。

24 使用過 Vue SSR 嗎?說說 SSR

SSR 也就是服務端渲染,也就是將 Vue 在客戶端把標籤渲染成 HTML 的工作放在服務端完成,然後再把 html 直接返回給客戶端。

優點:

SSR 有着更好的 SEO、並且首屏加載速度更快

缺點: 開發條件會受到限制,服務器端渲染只支持 beforeCreate 和 created 兩個鉤子,當我們需要一些外部擴展庫時需要特殊處理,服務端渲染應用程序也需要處於 Node.js 的運行環境。

服務器會有更大的負載需求

25 vue 中使用了哪些設計模式

  1. 工廠模式 - 傳入參數即可創建實例

虛擬 DOM 根據參數的不同返回基礎標籤的 Vnode 和組件 Vnode

  1. 單例模式 - 整個程序有且僅有一個實例

vuex 和 vue-router 的插件註冊方法 install 判斷如果系統存在實例就直接返回掉

  1. 發佈 - 訂閱模式 (vue 事件機制)

  2. 觀察者模式 (響應式數據原理)

  3. 裝飾模式: (@裝飾器的用法)

  4. 策略模式 策略模式指對象有某個行爲, 但是在不同的場景中, 該行爲有不同的實現方案 - 比如選項的合併策略

... 其他模式歡迎補充

26 你都做過哪些 Vue 的性能優化

這裏只列舉針對 Vue 的性能優化 整個項目的性能優化是一個大工程 可以另寫一篇性能優化的文章 哈哈


困難

27 Vue.mixin 的使用場景和原理

在日常的開發中,我們經常會遇到在不同的組件中經常會需要用到一些相同或者相似的代碼,這些代碼的功能相對獨立,可以通過 Vue 的 mixin 功能抽離公共的業務邏輯,原理類似 “對象的繼承”,當組件初始化時會調用 mergeOptions 方法進行合併,採用策略模式針對不同的屬性進行合併。當組件和混入對象含有同名選項時,這些選項將以恰當的方式進行 “合併”。

相關代碼如下

export default function initMixin(Vue){
  Vue.mixin = function (mixin) {
    //   合併對象
      this.options=mergeOptions(this.options,mixin)
  };
}
};

// src/util/index.js
// 定義生命週期
export const LIFECYCLE_HOOKS = [
  "beforeCreate",
  "created",
  "beforeMount",
  "mounted",
  "beforeUpdate",
  "updated",
  "beforeDestroy",
  "destroyed",
];

// 合併策略
const strats = {};
// mixin核心方法
export function mergeOptions(parent, child) {
  const options = {};
  // 遍歷父親
  for (let k in parent) {
    mergeFiled(k);
  }
  // 父親沒有 兒子有
  for (let k in child) {
    if (!parent.hasOwnProperty(k)) {
      mergeFiled(k);
    }
  }

  //真正合並字段方法
  function mergeFiled(k) {
    if (strats[k]) {
      options[k] = strats[k](parent[k], child[k]);
    } else {
      // 默認策略
      options[k] = child[k] ? child[k] : parent[k];
    }
  }
  return options;
}

Vue.mixin 原理詳解 傳送門

28 nextTick 使用場景和原理

nextTick 中的回調是在下次 DOM 更新循環結束之後執行的延遲迴調。在修改數據之後立即使用這個方法,獲取更新後的 DOM。主要思路就是採用微任務優先的方式調用異步方法去執行 nextTick 包裝的方法

相關代碼如下

let callbacks = [];
let pending = false;
function flushCallbacks() {
  pending = false; //把標誌還原爲false
  // 依次執行回調
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i]();
  }
}
let timerFunc; //定義異步方法  採用優雅降級
if (typeof Promise !== "undefined") {
  // 如果支持promise
  const p = Promise.resolve();
  timerFunc = () ={
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== "undefined") {
  // MutationObserver 主要是監聽dom變化 也是一個異步方法
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () ={
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
} else if (typeof setImmediate !== "undefined") {
  // 如果前面都不支持 判斷setImmediate
  timerFunc = () ={
    setImmediate(flushCallbacks);
  };
} else {
  // 最後降級採用setTimeout
  timerFunc = () ={
    setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb) {
  // 除了渲染watcher  還有用戶自己手動調用的nextTick 一起被收集到數組
  callbacks.push(cb);
  if (!pending) {
    // 如果多次調用nextTick  只會執行一次異步 等異步隊列清空之後再把標誌變爲false
    pending = true;
    timerFunc();
  }
}

nextTick 原理詳解 傳送門

29 keep-alive 使用場景和原理

keep-alive 是 Vue 內置的一個組件,可以實現組件緩存,當組件切換時不會對當前組件進行卸載。

相關代碼如下

export default {
  name: "keep-alive",
  abstract: true, //抽象組件

  props: {
    include: patternTypes, //要緩存的組件
    exclude: patternTypes, //要排除的組件
    max: [String, Number], //最大緩存數
  },

  created() {
    this.cache = Object.create(null); //緩存對象  {a:vNode,b:vNode}
    this.keys = []; //緩存組件的key集合 [a,b]
  },

  destroyed() {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    //動態監聽include  exclude
    this.$watch("include"(val) ={
      pruneCache(this, (name) => matches(val, name));
    });
    this.$watch("exclude"(val) ={
      pruneCache(this, (name) => !matches(val, name));
    });
  },

  render() {
    const slot = this.$slots.default; //獲取包裹的插槽默認值
    const vnode: VNode = getFirstComponentChild(slot); //獲取第一個子組件
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions);
      const { include, exclude } = this;
      // 不走緩存
      if (
        // not included  不包含
        (include && (!name || !matches(include, name))) ||
        // excluded  排除裏面
        (exclude && name && matches(exclude, name))
      ) {
        //返回虛擬節點
        return vnode;
      }

      const { cache, keys } = this;
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      if (cache[key]) {
        //通過key 找到緩存 獲取實例
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key); //通過LRU算法把數組裏面的key刪掉
        keys.push(key); //把它放在數組末尾
      } else {
        cache[key] = vnode; //沒找到就換存下來
        keys.push(key); //把它放在數組末尾
        // prune oldest entry  //如果超過最大值就把數組第0項刪掉
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }

      vnode.data.keepAlive = true; //標記虛擬節點已經被緩存
    }
    // 返回虛擬節點
    return vnode || (slot && slot[0]);
  },
};

擴展補充:LRU 算法是什麼?

lrusuanfa.png

LRU 的核心思想是如果數據最近被訪問過,那麼將來被訪問的幾率也更高,所以我們將命中緩存的組件 key 重新插入到 this.keys 的尾部,這樣一來,this.keys 中越往頭部的數據即將來被訪問幾率越低,所以當緩存數量達到最大值時,我們就刪除將來被訪問幾率最低的數據,即 this.keys 中第一個緩存的組件。

30 Vue.set 方法原理

瞭解 Vue 響應式原理的同學都知道在兩種情況下修改數據 Vue 是不會觸發視圖更新的

  1. 在實例創建之後添加新的屬性到實例上(給響應式對象新增屬性)

  2. 直接更改數組下標來修改數組的值

Vue.set 或者說是 $set 原理如下

因爲響應式數據 我們給對象和數組本身都增加了__ob__屬性,代表的是 Observer 實例。當給對象新增不存在的屬性 首先會把新的屬性進行響應式跟蹤 然後會觸發對象__ob__的 dep 收集到的 watcher 去更新,當修改數組索引時我們調用數組本身的 splice 方法去更新數組

相關代碼如下

export function set(target: Array | Object, key: any, val: any): any {
  // 如果是數組 調用我們重寫的splice方法 (這樣可以更新視圖)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val;
  }
  // 如果是對象本身的屬性,則直接添加即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  const ob = (target: any).__ob__;

  // 如果不是響應式的也不需要將其定義成響應式屬性
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 將屬性定義成響應式的
  defineReactive(ob.value, key, val);
  // 通知視圖更新
  ob.dep.notify();
  return val;
}

響應式數據原理詳解 傳送門

31 Vue.extend 作用和原理

官方解釋:Vue.extend 使用基礎 Vue 構造器,創建一個 “子類”。參數是一個包含組件選項的對象。

其實就是一個子類構造器 是 Vue 組件的核心 api 實現思路就是使用原型繼承的方法返回了 Vue 的子類 並且利用 mergeOptions 把傳入組件的 options 和父類的 options 進行了合併

相關代碼如下

export default function initExtend(Vue) {
  let cid = 0; //組件的唯一標識
  // 創建子類繼承Vue父類 便於屬性擴展
  Vue.extend = function (extendOptions) {
    // 創建子類的構造函數 並且調用初始化方法
    const Sub = function VueComponent(options) {
      this._init(options); //調用Vue初始化方法
    };
    Sub.cid = cid++;
    Sub.prototype = Object.create(this.prototype); // 子類原型指向父類
    Sub.prototype.constructor = Sub; //constructor指向自己
    Sub.options = mergeOptions(this.options, extendOptions); //合併自己的options和父類的options
    return Sub;
  };
}

Vue 組件原理詳解 傳送門

32 寫過自定義指令嗎 原理是什麼

指令本質上是裝飾器,是 vue 對 HTML 元素的擴展,給 HTML 元素增加自定義功能。vue 編譯 DOM 時,會找到指令對象,執行指令的相關方法。

自定義指令有五個生命週期(也叫鉤子函數),分別是 bind、inserted、update、componentUpdated、unbind

1. bind:只調用一次,指令第一次綁定到元素時調用。在這裏可以進行一次性的初始化設置。

2. inserted:被綁定元素插入父節點時調用 (僅保證父節點存在,但不一定已被插入文檔中)。

3. update:被綁定於元素所在的模板更新時調用,而無論綁定值是否變化。通過比較更新前後的綁定值,可以忽略不必要的模板更新。

4. componentUpdated:被綁定元素所在模板完成一次更新週期時調用。

5. unbind:只調用一次,指令與元素解綁時調用。

原理

  1. 在生成 ast 語法樹時,遇到指令會給當前元素添加 directives 屬性

  2. 通過 genDirectives 生成指令代碼

  3. 在 patch 前將指令的鉤子提取到 cbs 中, 在 patch 過程中調用對應的鉤子

  4. 當執行指令對應鉤子函數時,調用對應指令定義的方法

33 Vue 修飾符有哪些

事件修飾符

v-model 的修飾符

鍵盤事件的修飾符

系統修飾鍵

鼠標按鈕修飾符

34 Vue 模板編譯原理

Vue 的編譯過程就是將 template 轉化爲 render 函數的過程 分爲以下三步

第一步是將 模板字符串 轉換成 element ASTs(解析器)
第二步是對 AST 進行靜態節點標記,主要用來做虛擬DOM的渲染優化(優化器)
第三步是 使用 element ASTs 生成 render 函數代碼字符串(代碼生成器)

相關代碼如下

export function compileToFunctions(template) {
  // 我們需要把html字符串變成render函數
  // 1.把html代碼轉成ast語法樹  ast用來描述代碼本身形成樹結構 不僅可以描述html 也能描述css以及js語法
  // 很多庫都運用到了ast 比如 webpack babel eslint等等
  let ast = parse(template);
  // 2.優化靜態節點
  // 這個有興趣的可以去看源碼  不影響核心功能就不實現了
  //   if (options.optimize !== false) {
  //     optimize(ast, options);
  //   }

  // 3.通過ast 重新生成代碼
  // 我們最後生成的代碼需要和render函數一樣
  // 類似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
  // _c代表創建元素 _v代表創建文本 _s代表文Json.stringify--把對象解析成文本
  let code = generate(ast);
  //   使用with語法改變作用域爲this  之後調用render函數可以使用call改變this 方便code裏面的變量取值
  let renderFn = new Function(`with(this){return ${code}}`);
  return renderFn;
}

模板編譯原理詳解 傳送門

35 生命週期鉤子是如何實現的

Vue 的生命週期鉤子核心實現是利用發佈訂閱模式先把用戶傳入的的生命週期鉤子訂閱好(內部採用數組的方式存儲)然後在創建組件實例的過程中會一次執行對應的鉤子方法(發佈)

相關代碼如下

export function callHook(vm, hook) {
  // 依次執行生命週期對應的方法
  const handlers = vm.$options[hook];
  if (handlers) {
    for (let i = 0; i < handlers.length; i++) {
      handlers[i].call(vm); //生命週期裏面的this指向當前實例
    }
  }
}

// 調用的時候
Vue.prototype._init = function (options) {
  const vm = this;
  vm.$options = mergeOptions(vm.constructor.options, options);
  callHook(vm, "beforeCreate"); //初始化數據之前
  // 初始化狀態
  initState(vm);
  callHook(vm, "created"); //初始化數據之後
  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
};

生命週期實現詳解 傳送門

36 函數式組件使用場景和原理

函數式組件與普通組件的區別

1.函數式組件需要在聲明組件是指定 functional:true
2.不需要實例化,所以沒有this,this通過render函數的第二個參數context來代替
3.沒有生命週期鉤子函數,不能使用計算屬性,watch
4.不能通過$emit 對外暴露事件,調用事件只能通過context.listeners.click的方式調用外部傳入的事件
5.因爲函數式組件是沒有實例化的,所以在外部通過ref去引用組件時,實際引用的是HTMLElement
6.函數式組件的props可以不用顯示聲明,所以沒有在props裏面聲明的屬性都會被自動隱式解析爲prop,而普通組件所有未聲明的屬性都解析到$attrs裏面,並自動掛載到組件根元素上面(可以通過inheritAttrs屬性禁止)

優點 1. 由於函數式組件不需要實例化,無狀態,沒有生命週期,所以渲染性能要好於普通組件 2. 函數式組件結構比較簡單,代碼結構更清晰

使用場景:

一個簡單的展示組件,作爲容器組件使用 比如 router-view 就是一個函數式組件

“高階組件”——用於接收一個組件作爲參數,返回一個被包裝過的組件

相關代碼如下

if (isTrue(Ctor.options.functional)) {
  // 帶有functional的屬性的就是函數式組件
  return createFunctionalComponent(Ctor, propsData, data, context, children);
}
const listeners = data.on;
data.on = data.nativeOn;
installComponentHooks(data); // 安裝組件相關鉤子 (函數式組件沒有調用此方法,從而性能高於普通組件)

37 能說下 vue-router 中常用的路由模式實現原理嗎

hash 模式

  1. location.hash 的值實際就是 URL 中 #後面的東西 它的特點在於:hash 雖然出現 URL 中,但不會被包含在 HTTP 請求中,對後端完全沒有影響,因此改變 hash 不會重新加載頁面。

  2. 可以爲 hash 的改變添加監聽事件

window.addEventListener("hashchange", funcRef, false);

每一次改變 hash(window.location.hash),都會在瀏覽器的訪問歷史中增加一個記錄利用 hash 的以上特點,就可以來實現前端路由 “更新視圖但不重新請求頁面” 的功能了

特點:兼容性好但是不美觀

history 模式

利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。

這兩個方法應用於瀏覽器的歷史記錄站,在當前已有的 back、forward、go 的基礎之上,它們提供了對歷史記錄進行修改的功能。這兩個方法有個共同的特點:當調用他們修改瀏覽器歷史記錄棧後,雖然當前 URL 改變了,但瀏覽器不會刷新頁面,這就爲單頁應用前端路由 “更新視圖但不重新請求頁面” 提供了基礎。

特點:雖然美觀,但是刷新會出現 404 需要後端進行配置

原文地址:

https://juejin.cn/post/6961222829979697165

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BZihJPKmCaNtQXtRgSQa6w