天天看點

超全面總結Vue面試知識點,助力金三銀四

作者:Mason程

1.MVC 與 MVVM的差別

MVC

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

  • Model(模型):是應用程式中用于處理應用程式資料邏輯的部分。通常模型對象負責在資料庫中存取資料
  • View(視圖):是應用程式中處理資料顯示的部分。通常視圖是依據模型資料建立的
  • Controller(控制器):是應用程式中處理使用者互動的部分。通常控制器負責從視圖讀取資料,控制使用者輸入,并向模型發送資料

下面看斯坦福大學公開課上的這幅圖來說明,這可以說是最經典和最規範的MVC标準

幾乎所有的App都隻幹這麼一件事:将資料展示給使用者看,并處理使用者對界面的操作。MVC的思想:一句話描述就是Controller負責将Model的資料用View顯示出來,換句話說就是在Controller裡面把Model的資料指派給View。

MVVM

MVVM:Model、View、ViewModel。

你會下意識地把它和MVC來對比,你會發現,MVVM多了一個ViewModel而少了Controller。

首先說一下多出來的ViewModel(VM,不是顯存)。VM的意義,和Model一樣,在于資料。Model負責對資料進行取和存,然而我們對資料的操作除了取和存以外,還有一個非常重要的操作:解析

超全面總結Vue面試知識點,助力金三銀四

M:對應于MVC的M

V:對應于MVC的V

VM:ViewModel,是把MVC裡的controller的資料加載,加工功能分離出來

差別

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

Vue 并沒有完全遵循 MVVM 的思想

這一點Vue官網自己也有說明

超全面總結Vue面試知識點,助力金三銀四

這是因為從嚴格意義上來講,MVVM要求View與Model是不能直接通信的,而 Vue 提供了$refs 這個屬性,讓 Model 可以直接操作 View,違反了這一規定,是以說 Vue 沒有完全遵循 MVVM。

2.為什麼data需要是一個函數?

這個說法主要是在元件中出現,因為元件是可以複用的,js裡對象是引用關系,如果元件data是一個對象,那麼子元件中的data 屬性值會互相污染,産生副作用。如果元件中 data 選項是一個函數,那麼每個執行個體可以維護一份被傳回對象的獨立的拷貝,元件執行個體之間的 data 屬性值不會互相影響;而 new Vue 的執行個體,是不會被複用的,是以不存在引用對象的問題。

3.v-if與v-show

v-if與v-show的差別

「v-if」 是「真正」的條件渲染,因為它會確定在切換過程中條件塊内的事件監聽器和子元件适當地被銷毀和重建;也是「惰性的」:如果在初始渲染時條件為假,則什麼也不做——直到條件第一次變為真時,才會開始渲染條件塊。

「v-show」 就簡單得多——不管初始條件是什麼,元素總是會被渲染,并且隻是簡單地基于 CSS 的 “display” 屬性進行切換。

是以,v-if 适用于在運作時很少改變條件,不需要頻繁切換條件的場景;v-show 則适用于需要非常頻繁切換條件的場景。

v-show指令算是重排嗎?

v-show本質是通過元素css的display屬性來控制是否顯示,在DOM渲染時仍然會先渲染元素,然後才會進行判斷是否顯示(通過display屬性),而對于重排的定義是渲染樹中的節點資訊發生了大小、邊距等改變,要重新計算各節點和css具體的大小和位置。當用display來控制元素的顯示和隐藏時,會改變節點的大小和渲染樹的布局,導緻發生重排,是以v-show指令算是重排。

4.v-for

v-if 與 v-for 為什麼不建議一起使用?

首先,關于v-if和v-for的優先級,可以在源碼compiler/codegen/index.js中找到genElement方法,裡面的if else判斷,可以清楚看到for的判斷在if判斷之上,由此,可證明v-for的優先級高于v-if

如果v-if和v-for同時出現,分兩種情況:

  • 當同時出現在同一标簽内,可以通過vue.$options.render列印出渲染函數,可以清晰的看到會優先執行for循環,再執行if判斷
  • 當v-if出現在父級中,子級有v-for,此時再列印vue.$options.render,會發現會優先執行if判斷。

若想優化,提升性能,v-if需要優先執行,可以在v-for外層加一層template搭配v-if使用。若是v-if與v-for必須出現在同一層或v-if為v-for的子級的情況下,優化的方式可以将for循環的數組提前通過計算屬性處理,盡量減少過多渲染導緻的性能消耗。

v-for中的key有什麼作用?為什麼在v-for中的key不推薦使用随機數或者index?

「key的作用:」可以使vue的diff操作更加準确和快速

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

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

「更快速」:利用 key 的唯一性生成 map 對象來擷取對應節點,比周遊方式更快

「為什麼在v-for中的key不推薦使用随機數或者index?」

因為在插入資料或者删除資料的時候,會導緻後面的資料的key綁定的index變化,進而導緻重新渲染,效率會降低,同時也會導緻渲染出錯;當資料進行更改的時候,會通過key來判斷虛拟dom樹是否進行了更改。如果發現了相同的dom-key就可以直接複用。減少了渲染的性能損耗。是以使用随機數或index作為key會導緻性能浪費,并且使用index作為key可能會導緻渲染出錯。

v-for周遊對象時,是按什麼順序周遊的?如何保證順序?

1、會先判斷是否有iterator接口,如果有循環執行next()方法

2、沒有iterator的情況下,會調用Object.keys()方法,在不同浏覽器中,JS引擎不能保證輸出順序一緻

3、保證對象的輸出順序可以把對象放在數組中,作為數組的元素

5.常見的Vue内置指令

超全面總結Vue面試知識點,助力金三銀四

6.Vue元件通信的幾種方式

props/$emit

這個一般用于父子元件之間的通信,父元件通過props的方式向子元件傳遞資料,子元件可以通過$emit的方式向父元件進行通信。
<!-- 父元件 -->
<template>
 <div class="section">
 <childItem :list="list" @update="update"></childItem>
 </div>
</template>
 
<script>
import childItem from './childItem'
export default {
 name: 'parent',
 components: { childItem },
 data() {
 return {
   currentIndex: 0,
   list: [{id:1,title: 'nanjiu'}, {id:2, title:'FE'}, {id:3, title:'boy'}]
   }
 },
  methods: {
    update(index) {
      this.currentIndex = index
    }
  }
}
</script>
           
<!-- 子元件 -->
<template>
 <div>
 <span v-for="(item, index) in list" :key="item.id" @click="update(index)">{{item}}</span>
 </div>
</template>
 
<script>
export default {
 props: {
   list: Array
 },
  methods: {
    update(index) {
      this.$emit('update', index)
    }
  }
}
</script>
           

總結:

  • props 隻可以從上一級元件傳遞到下一級元件(父子元件),即所謂的單向資料流。而且是 props 隻讀的,不可被修改,所有修改都會失效并警告。
  • $emit綁定一個自定義事件, 當這個語句被執行時, 就會将參數傳遞給父元件,父元件通過v-on監聽并接收參數。

$parent/$Children

我們來看看官方是怎麼解釋這兩個API的:

超全面總結Vue面試知識點,助力金三銀四

從上面詳細中我們可以知道,通過$parent和$children就可以通路到對應元件的執行個體,既然都通路到元件執行個體了,那麼元件内的所有内容(data、methods等)就都能夠通路到了。

<!-- 父元件 -->
<template>
  <div class="section">
    <childItem></childItem>
  </div>
</template>

<script>
import childItem from "./components/item";
export default {
  name: "parent",
  components: { childItem },
  data() {
    return {
      currentIndex: 0,
      msg: "父元件",
    };
  },
  mounted() {
    console.log(`我是父元件,我正在通路${this.$children[0].msg}...`);
  }
};
</script>
           
<!-- 子元件 --> 
<template>
  <div class="item">
  </div>
</template>

<script>
export default {
  name: "item",
  data() {
    return {
      msg: "子元件"
    };
  },
  mounted() {
    console.log(`我是子元件,我正在通路${this.$parent.msg}...`);
  }
};
</script>
           
超全面總結Vue面試知識點,助力金三銀四

ref

如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子元件上,引用就指向元件執行個體
<!-- 子元件 -->
export default {
 data () {
   return {
    name: 'nanjiu'
   }
 },
 methods: {
   sayHello () {
    console.log('hello, I am nanjiu')
   }
 }
}
           
<!-- 父元件 -->
<template>
 <child ref="child"></child>
</template>
<script>
  import child from "./child"
 export default {
   components: {child},
   mounted () {
    const child = this.$refs.child;
    console.log(child.name); // nanjiu
    child.sayHello(); // hello, I am nanjiu
   }
 }
</script>
           

provide/inject

provide/ inject 是vue2.2.0新增的api, 簡單來說就是父元件中通過provide來提供變量, 然後再子元件中通過inject來注入變量。并且不論子元件嵌套有多深, 隻要調用了inject 那麼就可以注入provide中的資料
<!-- 根元件 -->
<template>
  <div class="section">
    <childItem></childItem>
  </div>
</template>

<script>
import childItem from "./components/item";
export default {
  name: "parent",
  components: { childItem },
  data() {
    return {
      currentIndex: 0,
      msg: "根元件"
    };
  },
  provide() {
    return {
      rootMsg: this.msg
    };
  },
  mounted() {
    console.log(`我是父元件,我正在通路${this.$children[0].msg}...`);
  }
};
</script>
           
<!-- 子元件 -->
<template>
  <div class="item">
    <subItem />
  </div>
</template>
<script>
import subItem from "./subItem.vue";
export default {
  name: "item",
  components: {
    subItem
  },
  inject: ["rootMsg"],
  data() {
    return {
      msg: "子元件"
    };
  },
  mounted() {
    console.log(`我是子元件,我正在通路${this.rootMsg}...`); //我是子元件,我正在通路根元件...
  }
};
</script>
           
<!-- 孫子元件-->
<template>
  <div class="sub_item"></div>
</template>

<script>
export default {
  name: "subItem",
  inject: ["rootMsg"],
  mounted() {
    console.log(`我是孫子元件,我正在通路${this.rootMsg}`); // 我是孫子元件,我正在通路根元件
  }
};
</script>
           

eventBus

eventBus 又稱為事件總線,在vue中可以使用它來作為溝通橋梁的概念, 就像是所有元件共用相同的事件中心,可以向該中心注冊發送事件或接收事件, 是以元件都可以通知其他元件。使用$on訂閱事件,$emit釋出事件
// index.js
Vue.prototype.$bus = new Vue() //使用一個vue執行個體來承載中央事件

// 訂閱事件
<template>
  <div class="section">
    <childItem :list="list" @update="update"></childItem>
  </div>
</template>

<script>
import childItem from "./components/item";
export default {
  name: "parent",
  components: { childItem },
  mounted() {
    // 訂閱事件
    this.$bus.$on("childLoad", () => {
      console.log(`我是父元件,已監測到子元件加載完成`); //我是父元件,已監測到子元件加載完成
    });
  }
};
</script>

// 釋出事件
<template>
  <div class="item">
    child
  </div>
</template>

<script>
export default {
  name: "item",
  mounted() {
    window.addEventListener("load", e => {
      this.$bus.$emit("childLoad");
    });
  }
};
</script>
           

$attrs/$listeners

如果遇到跨級元件使用props與emit來通信的話,那麼就需要将資料與事件一層一層往下傳遞,這樣做太麻煩了。是以在vue2.4中,為了解決該需求,引入了$attrs 和$listeners , 新增了inheritAttrs 選項。在版本2.4以前,預設情況下,父作用域中不作為 props 被識别 (且擷取) 的特性綁定 (class 和 style 除外),将會“回退”且作為普通的HTML特性應用在子元件的根元素上。

// 根元件
<template>
  <div class="section">
    <childItem
      :list="list"
      :msg="msg"
      @update="update"
      @rootFun="rootFun"
    ></childItem>
  </div>
</template>
<script>
import childItem from "./components/item";
export default {
  name: "parent",
  components: { childItem },

  data() {
    return {
      currentIndex: 0,
      msg: "根元件",
      list: [
        { id: 1, title: "nanjiu" },
        { id: 2, title: "FE" },
        { id: 3, title: "boy" }
      ]
    };
  },
  methods: {
    rootFun() {
      console.log("我是根元件的方法");
    },
    update(index) {
      this.currentIndex = index;
    }
  }
};
</script>

//子元件
<template>
  <div class="item">
    <subItem v-bind="$attrs" v-on="$listeners" />
  </div>
</template>

<script>
import subItem from "./subItem.vue";
export default {
  name: "item",
  components: {
    subItem
  }
};
</script>
//孫子元件
<template>
  <div class="sub_item"></div>
</template>

<script>
export default {
  name: "subItem",
  mounted() {
    console.log(this.$attrs); // {list: Array(3), msg: '根元件'}
    console.log(this.$listeners); //{update: ƒ, rootFun: ƒ}
  }
};
</script>
           

Vuex

「1. Vuex介紹」

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

Vuex 解決了多個視圖依賴于同一狀态和來自不同視圖的行為需要變更同一狀态的問題,将開發者的精力聚焦于資料的更新而不是資料在元件之間的傳遞上

「2. Vuex各個子產品」

  1. state:用于資料的存儲,是store中的唯一資料源
  2. getters:如vue中的計算屬性一樣,基于state資料的二次包裝,常用于資料的篩選和多個資料的相關性計算
  3. mutations:類似函數,改變state資料的唯一途徑,且不能用于處理異步事件
  4. actions:類似于mutation,用于送出mutation來改變狀态,而不直接變更狀态,可以包含任意異步操作
  5. modules:類似于命名空間,用于項目中将各個子產品的狀态分開定義和操作,便于維護

localStorage/sessionStorage

使用localStorage/sessionStorage也能夠進行通信,缺點就是不易維護

總結

  1. 父子元件通信: props; $parent / $children; provide / inject ; ref ; $attrs / $listeners
  2. 兄弟元件通信: eventBus ; vuex
  3. 跨級通信: eventBus;Vuex;provide / inject 、$attrs / $listeners

7.怎麼了解Vue的單向資料流?

在Vue中,所有的 prop 都使得其父子 prop 之間形成了一個「單向資料流」:父級 prop 的更新會向下流動到子元件中,但是反過來則不行。這樣會防止從子元件意外改變父級元件的狀态,進而導緻你的應用的資料流向難以了解。

額外的,每次父級元件發生更新時,子元件中所有的 prop 都将會重新整理為最新的值。這意味着你不應該在一個子元件内部改變 prop。如果你這樣做了,Vue 會在浏覽器的控制台中發出警告。子元件想修改時,隻能通過 $emit 派發一個自定義事件,父元件接收到後,由父元件修改。

8.computed 和 watch 的差別和運用的場景?

「差別:」

「computed:」 是計算屬性,依賴其它屬性值,并且 computed 的值有緩存,隻有它依賴的屬性值發生改變,下一次擷取 computed 的值時才會重新計算 computed 的值;

  • 支援緩存,隻有依賴資料發生改變,才會重新進行計算
  • 不支援異步,當computed内有異步操作時無效,無法監聽資料的變化
  • computed 屬性值會預設走緩存,計算屬性是基于它們的響應式依賴進行緩存的,也就是基于data中聲明過或者父元件傳遞的props中的資料通過計算得到的值
  • 如果一個屬性是由其他屬性計算而來的,這個屬性依賴其他屬性,是一個多對一或者一對一,一般用computed
  • 如果computed屬性屬性值是函數,那麼預設會走get方法;函數的傳回值就是屬性的屬性值;在computed中的,屬性都有一個get和一個set方法,當資料變化時,調用set方法。

「watch:」 更多的是「觀察」的作用,類似于某些資料的監聽回調 ,每當監聽的資料變化時都會執行回調進行後續操作;

  • 不支援緩存,資料變,直接會觸發相應的操作;
  • watch支援異步;
  • 監聽的函數接收兩個參數,第一個參數是最新的值;第二個參數是輸入之前的值;
  • 當一個屬性發生變化時,需要執行對應的操作;一對多;
  • 監聽資料必須是data中聲明過或者父元件傳遞過來的props中的資料,當資料變化時,觸發其他操作,

「watch和computed各自處理的資料關系場景不同:」

  • watch擅長處理的場景:一個資料影響多個資料
  • computed擅長處理的場景:一個資料受多個資料影響

9.Vue2最低相容到IE幾?

vue2相容IE8以上版本,IE8及以下版本不支援Object.defineProperty方法,但這個是vue實作響應式的所必須的。

10.說說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 鈎子函數中調用異步請求有以下優點:

  • 能更快擷取到服務端資料,減少頁面 loading 時間;
  • ssr 不支援 beforeMount 、mounted 鈎子函數,是以放在 created 中有助于一緻性;

11.說說Vue2的資料響應式原理

推薦閱讀

【Vue源碼學習】響應式原理探秘

【Vue源碼學習】依賴收集

Vue2與Vue3的資料響應式原理有什麼差別

  • Vue2 中的變化偵測實作對 Object 及 Array 分别進行了不同的處理,Objcet 使用了Object.defineProperty API ,Array 使用了攔截器對 Array 原型上的能夠改變資料的方法進行攔截。雖然也實作了資料的變化偵測,但存在很多局限 ,比如對象新增屬性無法被偵測,以及通過數組下邊修改數組内容,也是以在 Vue2 中經常會使用到 $set 這個方法對資料修改,以保證依賴更新。
  • Vue3 中使用了 es6 的 Proxy API 對資料代理,沒有像 Vue2 中對原資料進行修改,隻是加了代理包裝,是以首先性能上會有所改善。其次解決了 Vue2 中變化偵測的局限性,可以不使用 $set 新增的對象屬性及通過下标修改數組都能被偵測到。

12.Vue 的父元件和子元件生命周期鈎子函數執行順序?

  • 加載渲染過程父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
  • 子元件更新過程父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
  • 父元件更新過程父 beforeUpdate -> 父 updated
  • 銷毀過程父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

13.Vue事件綁定原理

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

1.原生dom事件的綁定,采用的是addEventListener實作

  • Vue在建立真是dom時會調用createElm,預設會調用invokeCreateHooks
  • 會周遊目前平台下相對的屬性處理代碼,其中就有updateDOMListeners方法,内部會傳入add方法
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  target = vnode.elm
  normalizeEvents(on)
  updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
  target = undefined
}
function add (
  name: string,
  handler: Function,
  capture: boolean,
  passive: boolean
) {
  target.addEventListener( // 給目前的dom添加事件
    name,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}
           

2.元件綁定事件采用的是$on方法

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}
function add (event, fn) {
  target.$on(event, fn)
}

           

14.直接給一個數組項指派,Vue 能檢測到變化嗎?

看過源碼應該都知道這是不能的,因為Vue2對數組的劫持本質上是劫持數組原型上的那七個方法,是以隻有通過調用這七個方法中的其中一個Vue才能夠監測到變化。

但我們可以通過調用set或$set來來觸發視圖更新

mounted() {
    this.$set(this.list, this.list[0], { id: 1, title: "南玖" });
  },
           

「vm.$set 的實作原理是:」

  • 如果目标是數組,直接使用數組的 splice 方法觸發相應式;
  • 如果目标是對象,會先判讀屬性是否存在、對象是否是響應式,最終如果要對屬性進行響應式處理,則是通過調用 defineReactive 方法進行響應式處理( defineReactive 方法就是 Vue 在初始化對象時,給對象屬性采用 Object.defineProperty 動态添加 getter 和 setter 的功能所調用的方法)

15.vue中data的屬性可以和methods中的方法同名嗎?為什麼?

這個通過看源碼也能知道這個不行的,vue在初始化過程中會先進行初始化props(initProps),然後會初始化methods(initMethods),這裡會判斷是否與props中有重名的key,有的話會告警,然後再初始化data( initData),這裡又會判斷是否有與props和methods重名的屬性,有的話會告警。

16.父元件可以監聽到子元件的生命周期嗎?

答案是可以的。

「第一種可以使用$emit來實作」

// Parent.vue
<Child @mounted="doSomething"/>

// Child.vue
mounted() {
  this.$emit("mounted");
}

           

「第二種可以使用@hook來實作」

// Parent.vue
<template>
  <div class="section">
    <childItem
      @hook:mounted="childMounted"
    ></childItem>
  </div>
</template>
<script>
export default {
  name: "parent",
  components: { childItem },
  mounted() {
    console.log("父元件mounted");
  },
  methods: {
    childMounted() {
      console.log("父元件監聽到子元件mounted");
    }
  }
};
</script>

// Child.vue
mounted() {
    console.log("子元件mounted");
}
           

列印順序應該是:子元件mounted --> 父元件監聽到子元件mounted --> 父元件mounted

17.虛拟DOM

虛拟DOM是什麼?

虛拟DOM是将狀态映射成試圖的衆多解決方案之一。頁面互動的本質還是通過改變狀态(變量)來改變試圖渲染,而架構(像主流架構vue、react、angular)的應用可以讓我們把關注的焦點更多的放在狀态上,省略對DOM的操作(架構内部已經幫我們完成了)。而虛拟DOM映射視圖的方式是通過狀态生成一個虛拟節點樹,然後通過虛拟節點樹進行渲染。虛拟DOM本質上是一個普通的js對象,它包含了建立一個DOM元素所需要的屬性。

虛拟DOM的優缺點

「優點:」

  • 保證性能下限:架構的虛拟 DOM 需要适配任何上層 API 可能産生的操作,它的一些 DOM 操作的實作必須是普适的,是以它的性能并不是最優的;但是比起粗暴的 DOM 操作性能要好很多,是以架構的虛拟 DOM 至少可以保證在你不需要手動優化的情況下,依然可以提供還不錯的性能,即保證性能的下限;
  • 無需手動操作 DOM:我們不再需要手動去操作 DOM,隻需要寫好 View-Model 的代碼邏輯,架構會根據虛拟 DOM 和 資料雙向綁定,幫我們以可預期的方式更新視圖,極大提高我們的開發效率;
  • 具備跨平台的優勢:由于 虛拟 DOM 是以 JavaScript 對象為基礎而不依賴真實平台環境,是以使它具有了跨平台的能力,比如說浏覽器平台、Weex、Node 等。

「缺點:」

  • 首次渲染大量DOM時,由于多了一層虛拟DOM的計算,會比innerHTML插入慢。

18.Vue-router 的路由鈎子函數是什麼,以及執行順序是怎樣的?

vue-router 提供的導航守衛主要用來通過 「跳轉」 或 「取消」 的方式 「守衛導航」 ,Vue的路由鈎子分為:「全局守衛、路由守衛、元件守衛」

執行順序

  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 的回調函數,建立好的元件執行個體會作為回調函數的參數傳入。

19.說說你對slot的了解

Slot 名為插槽,我們可以了解為solt在元件模闆中占好了位置,當使用該元件标簽時候,元件标簽裡面的内容就會自動填坑(替換元件模闆中slot位置),作為承載分發内容的出口

slot可以分為三類

「預設插槽」

子元件用<slot>标簽來确定渲染的位置,标簽裡面可以放DOM結構,當父元件使用的時候沒有往插槽傳入内容,标簽内DOM結構就會顯示在頁面

父元件在使用的時候,直接在子元件的标簽内寫入内容即可

<!-- 父元件 -->
<Child>
  <div>預設插槽</div>  
</Child>

<!-- 子元件-->
<template>
    <slot>
      <p>插槽後備的内容</p>
    </slot>
</template>
           

「具名插槽」

子元件用name屬性來表示插槽的名字,不傳為預設插槽

父元件中在使用時在預設插槽的基礎上加上slot屬性,值為子元件插槽name屬性值

<!-- 父元件 -->
<child>
    <template v-slot:default>具名插槽</template>
    <!-- 具名插槽⽤插槽名做參數 -->
    <template v-slot:content>内容...</template>
</child>

<!-- 子元件-->
<template>
    <slot>插槽後備的内容</slot>
    <slot name="nanjiu">插槽後備的内容</slot>
</template>
           

「作用域插槽」

子元件在作用域上綁定屬性來将子元件的資訊傳給父元件使用,這些屬性會被挂在父元件v-slot接受的對象上

父元件中在使用時通過v-slot:(簡寫:#)擷取子元件的資訊,在内容中使用

<!-- 父元件 -->
<child> 
    <!-- 把v-slot的值指定為作⽤域上下⽂對象 -->
    <template v-slot:default="slotProps">
      來⾃⼦元件資料:{{slotProps.testProps}}
    </template>
  <template #default="slotProps">
      來⾃⼦元件資料:{{slotProps.testProps}}
    </template>
</child>

<!-- 子元件-->
<template> 
  <slot name="footer" testProps="子元件的值">
          <h3>沒傳footer插槽</h3>
    </slot>
</template>
           

「小結:」

  • v-slot屬性隻能在<template>上使用,但在隻有預設插槽時可以在元件标簽上使用
  • 預設插槽名為default,可以省略default直接寫v-slot
  • 縮寫為#時不能不寫參數,寫成#default
  • 使用作用域插槽時可以通過解構擷取v-slot={msg},還可以重命名v-slot="{msg: newMsg}"和定義預設值v-slot="{msg = '預設值'}"

「插槽的原理:」

slot本質上是傳回VNode的函數,一般情況下,Vue中的元件要渲染到頁面上需要經過 template >> render function >> VNode >> DOM 過程。元件挂載的本質就是執行渲染函數得到VNode,至于data/props/computed這些屬性都是給VNode提供資料來源。

在2.5之前,如果是普通插槽就「直接是VNode」的形式了,而如果是作用域插槽,由于子元件需要在父元件通路子元件的資料,是以父元件下是一個「未執行的函數」 (slotScope) => return h('div', slotScope.msg) ,接受子元件的slotProps參數,在子元件渲染執行個體時會調用該函數傳入資料。

在2.6之後,兩者合并,普通插槽也變成一個函數,隻是不接受參數了。

20.說說Vue的$nextTick的原理

Vue 在更新 DOM 時是異步執行的。隻要偵聽到資料變化,Vue 将開啟一個隊列,并緩沖在同一事件循環中發生的所有資料變更。如果同一個 watcher 被多次觸發,隻會被推入到隊列中一次。這種在緩沖時去除重複資料對于避免不必要的計算和 DOM 操作是非常重要的。然後,在下一個的事件循環“tick”中,Vue 重新整理隊列并執行實際 (已去重的) 工作。Vue 在内部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,如果執行環境不支援,則會采用 setTimeout(fn, 0) 代替。(後面會寫一篇關于nextTick的源碼的文章)

21.說說你對函數式元件的了解

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.函數式元件結構比較簡單,代碼結構更清晰

22.常見的Vue性能優化有哪些?

  • 響應式資料對象層級不要過深,非響應式資料不要放在data裡面或者使用Object.freeze() 當機資料
  • 合理使用v-if 和 v-show ,v-if适用于切換不頻繁的場景,v-show适用于切換頻繁的場景
  • computed 和 watch 區分使用場景,能使用computed實作的就不用watch(computed具有緩存效果)
  • v-for 周遊必須加 key,key 最好是 id 值,并且避免同時使用 v-if
  • 大資料清單和表格性能優化-虛拟清單/虛拟表格
  • 防止内部洩漏,元件銷毀後還應該把全局變量和事件銷毀
  • 圖檔懶加載
  • 路由懶加載
  • 第三方插件的按需引入
  • 适當采用 keep-alive 緩存元件
  • 防抖、節流運用
  • 服務端渲染 SSR 或者 預渲染

23.Vue.mixin的使用場景及原理

mixin(混入),提供了一種非常靈活的方式,來分發 Vue 元件中的可複用功能。混入對象可以包含任意元件選項。當元件使用混入對象時,所有混入對象的選項将被混入該元件本身的選項。混入也可以進行全局注冊。使用時格外小心!一旦使用全局混入,它将影響每一個之後建立的 Vue 執行個體。
// mixin.js
export default {
  data() {
    return {
      title: '我是mixin中的title'
    }
  },
  
}
           
<template>
  <div class="section">
  </div>
</template>

<script>
import mixin from "./mixin";
export default {
  name: "parent",
  mixins: [mixin],
  data() {
    return {
      currentIndex: 0,
      msg: "根元件",
    };
  },
  created() {
    console.log(this.$data);
    console.log("我是根元件中的created");
  },
</script>
           

先列印出我是mixin中的created,然後接着列印合并後的data(包含根元件的data與mixin中的data),再列印出我是根元件中的created

「使用場景:」

在日常的開發中,我們經常會遇到在不同的元件中經常會需要用到一些相同或者相似的代碼,這些代碼的功能相對獨立 這時,可以通過Vue的mixin功能将相同或者相似的代碼提出來。

「原理:」

主要原理就在于這個merOptions方法

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {

if (child.mixins) { // 判斷有沒有mixin 有的話遞歸進行合并
    for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
    }
}
  const options = {} 
  let key
  for (key in parent) {
    mergeField(key) // 先周遊parent的key
  }
  for (key in child) {
    if (!hasOwn(parent, key)) { // 如果parent已經處理過某個key 就不處理了
      mergeField(key) // 處理child中的key 也就parent中沒有處理過的key
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key) // 根據不同類型的options調用strats中不同的方法進行合并
  }
  return options
}
           

優先遞歸處理 mixins,先周遊合并parent 中的key,調用mergeField方法進行合并,然後儲存在變量options,再周遊 child,合并補上 parent 中沒有的key,調用mergeField方法進行合并,儲存在變量options。

24.Vue.extend的作用與原理

使用基礎 Vue 構造器,建立一個“子類”。參數是一個包含元件選項的對象。

data 選項是特例,需要注意 - 在 Vue.extend() 中它必須是函數,該方法傳回一個與Vue具有相同功能的構造函數(其實為建立了一個元件)-屬性options是 合并 「基礎 Vue 構造器」 與 extend的參數 的對象,

Vue.extend 核心思路就是建立一個 VueComponent構造函數 命名為 Sub,通過将 VueComponent 的原型指向 Vue構造函數 的原型的方式繼承 Vue構造函數 原型上所有的屬性和方法,接着檢查傳入的 extendOptions 是否擁有 props 和 computed 屬性,如果有就進行初始化。如果檢測出傳入的 extendOptions 中含有 name 屬性,則将其自動放入 VueComponent 的全局元件中。然後将 Vue.options 儲存到 VueComponent.superOptions 屬性中,将傳入的 extendOptions 儲存到 VueComponent.extendOptions 屬性中,并将傳入的 extendOptions 封存一份儲存到 VueComponent.sealedOptions 中。最後将 VueComponent 傳回,這就是繼承了 Vue構造函數 的 Vue元件 的構造函數。(後面會寫文章細講)

「與mixin的差別:」

  • mixin是對Vue類的options進行混入。所有Vue的執行個體對象都會具備混入進來的配置行為。
  • extend是産生一個繼承自Vue類的子類,隻會影響這個子類的執行個體對象,不會對Vue類本身以及Vue類的執行個體對象産生影響。

25.既然vue通過資料劫持可以精準的探測資料變化,為什麼還要進行diff檢測差異?

「現在前端架構有兩種資料變動偵測方式,一種是pull,一種是push.」

  • pull 的代表是React ,在進行 setState 操作後顯示更新資料,React 會使用 diff 算法一層層找出差異,然後 patch 到 DOM 樹上,React 一開始不知道那裡變化了,隻是知道變化了,然後暴力進行查找那變化了,另一個代表是 Angular 的髒檢查。
  • Vue 的響應式系統就是 Push 的代表,Vue 初始化的時候就會對 data 的資料進行依賴收集,是以Vue能實時知道那裡發生了變化,一般綁定的細粒度過高,會生成大量的Watcher 執行個體,則會造成過大的記憶體和依賴追蹤的開銷,而細粒度過低無法偵測到變化。是以,Vue采用的是中等細粒度的方案,隻針對元件級别的進行響應式監聽也就是push,這樣可以知道那個元件發生了變化,再對元件進行diff算法找到具體變化的位置,這是pull操作,vue是pull + push 結合進行變化偵測的。

繼續閱讀