Vue 的這些技巧你真的都掌握了嗎?(整理彙總 Vue 框架中重要的特性、框架的原理)

語言: CN / TW / HK

前言

文章目的昭然若揭 ‍ ,整理彙總 Vue 框架中重要的特性、框架的原理。

那 "前車之鑑" 從何而來?

是的,我又要講小故事了,但這次是故事的續集。

故事第 1 集: CSS前處理器,你還是隻會巢狀麼 ?[2]

故事第 2 集: 【自適應】px 轉 rem,你還在手算麼?[3]

為什麼說是續集,因為這些都是同一大佬問的,在此感謝大佬,天降素材 。

故事續集

大佬:有看過 Vue 原始碼麼?

我:嗯嗯,看過。

大佬:那大概講一講 nextTick 的底層實現 ?

我:停頓了大概10s,說了句忘了。(理不直氣還壯)

大佬:噢噢,沒事。(內心大概已經放棄對我知識面的挖掘)

因為是影片面試,強裝自信的尷尬從螢幕中溢位,這大概就是 普通且自信 ‍♂️?裝X失敗案例引以為戒,能寫出續集的面試結果不提也罷。

這次面試打擊還是蠻大的,考察內容全面且細節。面試後一直在整理 Vue 相關的知識點,所以不會將 nextTick實現 單獨成文,只是收錄在下方試題中。 前車之鑑可以為鑑 ,大家可以把本篇文章當測驗,考察自己是否對這些知識點熟練於心。

萬字長文,持續更新,若有遺漏知識點,後續會補充。

題目

Vue 的優缺點

優點

  1. 建立單頁面應用的輕量級Web應用框架
  2. 簡單易用
  3. 雙向資料繫結
  4. 元件化的思想
  5. 虛擬DOM
  6. 資料驅動檢視

缺點

不支援IE8(現階段只能勉強湊出這麼半點 )

SPA 的理解

SPA是 Single-Page-Application 的縮寫,翻譯過來就是單頁應用。在WEB頁面初始化時一同載入Html、Javascript、Css。一旦頁面載入完成,SPA不會因為使用者操作而進行頁面重新載入或跳轉,取而代之的是利用路由機制實現Html內容的變換。

優點

  1. 良好的使用者體驗,內容更改無需過載頁面。
  2. 基於上面一點,SPA相對服務端壓力更小。
  3. 前後端職責分離,架構清晰。

缺點

  1. 由於單頁WEB應用,需在載入渲染頁面時請求JavaScript、Css檔案,所以耗時更多。
  2. 由於前端渲染,搜尋引擎不會解析JS,只能抓取首頁未渲染的模板,不利於SEO。
  3. 由於單頁應用需在一個頁面顯示所有的內容,預設不支援瀏覽器的前進後退。

缺點3,想必有人和我有同樣的疑問。

通過資料查閱,其實是 前端路由機制 解決了單頁應用無法前進後退的問題。Hash模式中Hash變化會被瀏覽器記錄( onhashchange 事件),History模式利用 H5 新增的 pushStatereplaceState 方法可改變瀏覽器歷史記錄棧。

new Vue(options) 都做了些什麼

如下 Vue 建構函式所示,主要執行了 this._init(options) 方法,該方法在 initMixin 函式中註冊。

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // Vue.prototype._init 方法
  this._init(options)
}

// _init 方法在 initMixin 註冊
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
複製程式碼

檢視 initMixin 方法的實現,其他函式具體實現可自行檢視,這裡就不貼出了。

let uid = 0
export function initMixin() {
  Vue.prototype._init = function(options) {
    const vm = this
    vm._uid = uid++
    vm._isVue = true
   
    // 處理元件配置項
    if (options && options._isComponent) {
       /**
       * 如果是子元件,走當前 if 分支
       * 函式作用是效能優化:將原型鏈上的方法都放到vm.$options中,減少原型鏈上的訪問
       */   
      initInternalComponent(vm, options)
    } else {
      /**
       * 如果是根元件,走當前 else 分支
       * 合併 Vue 的全域性配置到根元件中,如 Vue.component 註冊的全域性元件合併到根元件的 components 的選項中
       * 子元件的選項合併發生在兩個地方
       * 1. Vue.component 方法註冊的全域性元件在註冊時做了選項合併
       * 2. { component: {xx} } 方法註冊的區域性元件在執行編譯器生成的 render 函式時做了選項合併
       */  
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
  
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }

    vm._self = vm
    /**
    * 初始化元件例項關係屬性,如:$parent $root $children $refs
    */
    initLifecycle(vm)
    /**
    * 初始化自定義事件
    * <component @click="handleClick"></component>
    * 元件上註冊的事件,監聽者不是父元件,而是子元件本身
    */
    initEvents(vm)
    /**
    * 解析元件插槽資訊,得到vm.$slot,處理渲染函式,得到 vm.$createElement 方法,即 h 函式。
    */
    initRender(vm)
    /**
    * 執行 beforeCreate 生命週期函式
    */
    callHook(vm, 'beforeCreate')
    /**
    * 解析 inject 配置項,得到 result[key] = val 的配置物件,做響應式處理且代理到 vm 實力上
    */
    initInjections(vm) 
    /**
    * 響應式處理核心,處理 props、methods、data、computed、watch
    */
    initState(vm)
    /**
    * 解析 provide 物件,並掛載到 vm 例項上
    */
    initProvide(vm) 
    /**
    * 執行 created 生命週期函式
    */
    callHook(vm, 'created')

    // 如果 el 選項,自動執行$mount
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
複製程式碼

MVVM 的理解

MVVM是 Model-View-ViewModel 的縮寫。Model 代表資料層,可定義修改資料、編寫業務邏輯。View 代表檢視層,負責將資料渲染成頁面。ViewModel 負責監聽資料層資料變化,控制檢視層行為互動,簡單講,就是同步資料層和檢視層的物件。ViewModel 通過雙向繫結把 View 和 Model 層連線起來,且同步工作無需人為干涉,使開發人員只關注業務邏輯,無需頻繁操作DOM,不需關注資料狀態的同步問題。

如何實現 v-model

v-model指令用於實現 inputselect 等表單元素的雙向繫結,是個語法糖。

原生 input 元素若是 text/textarea 型別,使用 value 屬性和 input 事件。

原生 input 元素若是 radio/checkbox 型別,使用 checked屬性和 change 事件。

原生 select 元素,使用 value 屬性和 change 事件。

input 元素上使用 v-model 等價於

<input :value="message" @input="message = $event.target.value" />
複製程式碼

實現自定義元件的 v-model

自定義元件的 v-model 使用prop值為 valueinput 事件。若是 radio/checkbox 型別,需要使用 model 來解決原生 DOM 使用的是 checked 屬性 和 change 事件,如下所示。

// 父元件
<template>
  <base-checkbox v-model="baseCheck" />
</template>
複製程式碼

// 子元件
<template>
  <input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" />
</template>
<script>
export default {
  model: {
    prop: 'checked',
    event: 'change'
  },
  prop: {
    checked: Boolean
  }
}
</script>
複製程式碼

如何理解 Vue 單向資料流

Vue 官方文件 Prop 選單下的有個名為 單項資料流 的子選單。

我們經常說 Vue 的雙向繫結,其實是在單向繫結的基礎上給元素新增 input/change 事件,來動態修改檢視。Vue 元件間傳遞資料仍然是單項的,即父元件傳遞到子元件。子元件內部可以定義依賴 props 中的值,但無權修改父元件傳遞的資料,這樣做防止子元件意外變更父元件的狀態,導致應用資料流向難以理解。

如果在子元件內部 直接 更改prop,會遇到警告處理。

2 種定義依賴 props 中的值

  1. 通過 data 定義屬性並將 prop 作為初始值。
<script>
export default {
  props: ['initialNumber'],
  data() {
    return {
      number: this.initailNumber
    }
  }
}
</script>
複製程式碼
  1. 用 computed 計算屬性去定義依賴 prop 的值。若頁面會更改當前值,得分 get 和 set 方法。
<script>
export default {
  props: ['size'],
  computed: {
    normalizedSize() {
      return this.size.trim().toLowerCase()
    }
  }
}
</sciprt>
複製程式碼

Vue 響應式原理

核心原始碼位置:vue/src/core/observer/index.js

響應式原理 3 個步驟:資料劫持、依賴收集、派發更新。

資料分為兩類:物件、陣列。

物件

遍歷物件,通過 Object.defineProperty 為每個屬性新增 getter 和 setter,進行資料劫持。getter 函式用於在資料讀取時進行依賴收集,在對應的 dep 中儲存所有的 watcher;setter 則是資料更新後通知所有的 watcher 進行更新。

核心原始碼

function defineReactive(obj, key, val, shallow) {
  // 例項化一個 dep, 一個 key 對應一個 dep
  const dep = new Dep()
 
  // 獲取屬性描述符
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // 通過遞迴的方式處理 val 為物件的情況,即處理巢狀物件
  let childOb = !shallow && observe(val)
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 攔截obj.key,進行依賴收集
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Dep.target 是當前元件渲染的 watcher
      if (Dep.target) {
        // 將 dep 新增到 watcher 中
        dep.depend()
        if (childOb) {
          // 巢狀物件依賴收集
          childOb.dep.depend()
          // 響應式處理 value 值為陣列的情況
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 獲取舊值
      const value = getter ? getter.call(obj) : val
      // 判斷新舊值是否一致
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }

      if (getter && !setter) return
      // 如果是新值,用新值替換舊值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 新值做響應式處理
      childOb = !shallow && observe(newVal)
      // 當響應式資料更新,依賴通知更新
      dep.notify()
    }
  })
}
複製程式碼

陣列

用陣列增強的方式,覆蓋原屬性上預設的陣列方法,保證在新增或刪除資料時,通過 dep 通知所有的 watcher 進行更新。

核心原始碼

const arrayProto = Array.prototype
// 基於陣列原型物件建立一個新的物件
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  // 分別在 arrayMethods 物件上定義7個方法
  def(arrayMethods, method, function mutator (...args) {
    // 先執行原生的方法
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 針對新增元素進行響應式處理
    if (inserted) ob.observeArray(inserted)
    // 資料無論是新增還是刪除都進行派發更新
    ob.dep.notify()
    return result
  })
})
複製程式碼

手寫觀察者模式

當物件間存在一對多的關係,使用觀察者模式。比如:當一個物件被修改,會自動通知依賴它的物件。

let uid = 0
class Dep {
  constructor() {
    this.id = uid++
    // 儲存所有的 watcher
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  removeSub(sub) {
    if(this.subs.length) {
      const index = this.subs.indexOf(sub)
      if(index > -1) return this.subs.splice(index, 1)
    }
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

class Watcher {
  constructor(name) {
    this.name = name
  }
  update() {
    console.log('更新')
  }
}
複製程式碼

手寫釋出訂閱模式

與觀察者模式相似,區別在於釋出者和訂閱者是解耦的,由中間的排程中心去與釋出者和訂閱者通訊。

Vue響應式原理個人更傾向於釋出訂閱模式。其中 Observer 是釋出者,Watcher 是訂閱者,Dep 是排程中心。

vue中資料繫結原理的設計模式到底觀察者還是釋出訂閱?[4] ,知乎有相關爭論,感興趣的可以看下。

class EventEmitter {
  constructor() {
    this.events = {}
  }
  on(type, cb) {
    if(!this.events[type]) this.events[type] = []
    this.events[type].push(cb)
  }
  emit(type, ...args) {
    if(this.events[type]) {
      this.events[type].forEach(cb => {
        cb(...args)
      })
    }
  }
  off(type, cb) {
    if(this.events[type]) {
      const index = this.events[type].indexOf(cb)
      if(index > -1) this.events[type].splice(index, 1)
    }
  }
}
複製程式碼

關於 Vue.observable 的瞭解

Vue.observable 可使物件可響應。返回的物件可直接用於 渲染函式計算屬性 內,並且在發生變更時觸發相應的更新。也可以作為最小化的跨元件狀態儲存器。

Vue 2.x 中傳入的物件和返回的物件是同一個物件。

Vue 3.x 則不是一個物件,源物件不具備響應式功能。

適用的場景:在專案中沒有大量的非父子元件通訊時,可以使用 Vue.observable 去替代 eventBusvuex 方案。

用法如下

// store.js
import Vue from 'vue'
export const state = Vue.observable({
  count: 1
})
export const mutations = {
  setCount(count) {
    state.count = count
  }
} 

// vue 檔案
<template>
  <div>{{ count }}</div>
</template>
<script>
import { state, mutation } from './store.js'
export default {
  computed: {
    count() {
      return state.count
    }
  }
}
</script>
複製程式碼

原理部分和響應式原理處理元件 data 是同一個函式,例項化一個 Observe,對資料劫持。

元件中的 data 為什麼是個函式

物件在棧中儲存的都是地址,函式的作用就是屬性私有化,保證元件修改自身屬性時不會影響其他複用元件。

Vue 生命週期

生命週期 描述

呼叫非同步請求可在 createdbeforeMountmounted 生命週期中呼叫,因為相關資料都已建立。最好的選擇是在 created 中呼叫。

獲取DOM在 mounted 中獲取,獲取可用 $ref 方法,這點毋庸置疑。

Vue 父元件和子元件生命週期執行順序

載入渲染過程

父先建立,才能有子;子建立完成,父才完整。

順序:父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

子元件更新過程

  1. 子元件更新 影響到 父元件的情況。

順序:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  1. 子元件更新 不影響到 父元件的情況。

順序:子 beforeUpdate -> 子 updated

父元件更新過程

  1. 父元件更新 影響到 子元件的情況。

順序:父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  1. 父元件更新 不影響到 子元件的情況。

順序:父 beforeUpdate -> 父 updated

銷燬過程

順序:父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

父元件如何監聽子元件生命週期的鉤子函式

兩種方式都以 mounted 為例子。

$emit實現

// 父元件
<template>
  <div class="parent">
    <Child @mounted="doSomething"/>
  </div>
</template>
<script>
export default {  
  methods: {
    doSomething() {
      console.log('父元件監聽到子元件 mounted 鉤子函式')
    }
  }
}
</script>
//子元件
<template>
  <div class="child">
  </div>
</template>
<script>
export default {
  mounted() {
    console.log('觸發mounted事件...')
    this.$emit("mounted")
  }
}
</script>
複製程式碼

@hook實現

// 父元件
<template>
  <div class="parent">
    <Child @hook:mounted="doSomething"/>
  </div>
</template>
<script>
export default {  
  methods: {
    doSomething() {
      console.log('父元件監聽到子元件 mounted 鉤子函式')
    }
  }
}
</script>

//子元件
<template>
  <div class="child">
  </div>
</template>
<script>
export default {
  mounted() {
    console.log('觸發mounted事件...')
  }
}
</script>
複製程式碼

Vue 元件間通訊方式

父子元件通訊

  1. props 與 $emit
  2. 與children

隔代元件通訊

  1. 與listeners
  2. provide 和 inject

父子、兄弟、隔代元件通訊

  1. EventBus
  2. Vuex

v-on 監聽多個方法

<button v-on="{mouseenter: onEnter, mouseleave: onLeave}">滑鼠進來1</button>`
複製程式碼

常用的修飾符

表單修飾符

  1. lazy: 失去焦點後同步資訊
  2. trim: 自動過濾首尾空格
  3. number: 輸入值轉為數值型別

事件修飾符

  1. stop:阻止冒泡
  2. prevent:阻止預設行為
  3. self:僅繫結元素自身觸發
  4. once:只觸發一次

滑鼠按鈕修飾符

  1. left:滑鼠左鍵
  2. right:滑鼠右鍵
  3. middle:滑鼠中間鍵

class 與 style 如何動態繫結

class 和 style 可以通過物件語法和陣列語法進行動態繫結

物件寫法

<template>
  <div :class="{ active: isActive }"></div>
  <div :style="{ fontSize: fontSize }">
</template>
<script>
export default {
  data() {
    return {
      isActive: true,
      fontSize: 30
    }
  }
}
</script>
複製程式碼

陣列寫法

<template>
  <div :class="[activeClass]"></div>
  <div :style="[styleFontSize]">
</template>
<script>
export default {
  data() {
    return {
      activeClass: 'active',
      styleFontSize: {
        fontSize: '12px'
      }
    }
  }
}
</script>
複製程式碼

v-show 和 v-if 區別

共同點:控制元素顯示和隱藏。

不同點:

  1. v-show 控制的是元素的CSS(display);v-if 是控制元素本身的新增或刪除。
  2. v-show 由 false 變為 true 的時候不會觸發元件的生命週期。v-if 由 false 變為 true 則會觸發元件的 beforeCreatecreatebeforeMountmounted 鉤子,由 true 變為 false 會觸發元件的 beforeDestorydestoryed 方法。
  3. v-if 比 v-show有更高的效能消耗。

為什麼 v-if 不能和 v-for 一起使用

效能浪費,每次渲染都要先迴圈再進行條件判斷,考慮用計算屬性替代。

Vue2.x中 v-forv-if 更高的優先順序。

Vue3.x中 v-ifv-for 更高的優先順序。

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

computed 和 watch 本質都是通過例項化 Watcher 實現,最大區別就是適用場景不同。

computed

計算屬性,依賴其他屬性值,且值具備快取的特性。只有它依賴的屬性值發生改變,下一次獲取的值才會重新計算。

適用於數值計算,並且依賴於其他屬性時。因為可以利用快取特性,避免每次獲取值,都需要重新計算。

watch

觀察屬性,監聽屬性值變動。每當屬性值發生變化,都會執行相應的回撥。

適用於資料變化時執行非同步或開銷比較大的操作。

slot 插槽

slot 插槽,可以理解為 slot 在元件模板中提前佔據了位置。當複用元件時,使用相關的slot標籤時,標籤裡的內容就會自動替換元件模板中對應slot標籤的位置,作為承載分發內容的出口。

主要作用是複用和擴充套件元件,做一些定製化元件的處理。

插槽主要有3種

  1. 預設插槽
// 子元件
<template>
  <slot>
    <div>預設插槽備選內容</div>
  </slot>
</template>

// 父元件
<template>
  <Child>
    <div>替換預設插槽內容</div>
  </Child>
</template>
複製程式碼
  1. 具名插槽

slot 標籤沒有 name 屬性,則為預設插槽。具備 name 屬性,則為具名插槽

// 子元件
<template>
  <slot>預設插槽的位置</slot>
  <slot name="content">插槽content內容</slot>
</template>

// 父元件
<template>
   <Child>
     <template v-slot:default>
       預設...
     </template>
     <template v-slot:content>
       內容...
     </template>
   </Child>
</template>
複製程式碼
  1. 作用域插槽

子元件在作用域上繫結的屬性來將元件的資訊傳給父元件使用,這些屬性會被掛在父元件接受的物件上。

// 子元件
<template>
  <slot name="footer" childProps="子元件">
    作用域插槽內容
  </slot>
</template>

// 父元件
<template>
  <Child v-slot="slotProps">
    {{ slotProps.childProps }}
  </Child>
</template>
複製程式碼

Vue.$delete 和 delete 的區別

Vue.$delete 是直接刪除了元素,改變了陣列的長度;delete 是將被刪除的元素變成內 undefined ,其他元素鍵值不變。

Vue.$set 如何解決物件新增屬性不能響應的問題

Vue.$set的出現是由於 Object.defineProperty 的侷限性:無法檢測物件屬性的新增或刪除。

原始碼位置:vue/src/core/observer/index.js

export function set(target, key, val) {
  // 陣列
  if(Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改陣列長度,避免索引大於陣列長度導致splice錯誤
    target.length = Math.max(target.length, key)
    // 利用陣列splice觸發響應
    target.splice(key, 1, val)
    return val
  }
  // key 已經存在,直接修改屬性值
  if(key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = target.__ob__
  // target 不是響應式資料,直接賦值
  if(!ob) {
    target[key] = val
    return val
  }
  // 響應式處理屬性
  defineReactive(ob.value, key, val)
  // 派發更新
  ob.dep.notify()
  return val
}
複製程式碼

實現原理:

  1. 若是陣列,直接使用陣列的 splice 方法觸發響應式。
  2. 若是物件,判斷屬性是否存在,物件是否是響應式。
  3. 以上都不滿足,最後通過 defineReactive 對屬性進行響應式處理。

Vue 非同步更新機制

Vue 非同步更新機制核心是利用瀏覽器的非同步任務佇列實現的。

當響應式資料更新後,會觸發 dep.notify 通知所有的 watcher 執行 update 方法。

dep 類的 notify 方法

notify() {
  // 獲取所有的 watcher
  const subs = this.subs.slice()
  // 遍歷 dep 中儲存的 watcher,執行 watcher.update
  for(let i = 0; i < subs.length; i++) {
    subs[i].update()
  }
}
複製程式碼

watcher.update 將自身放入全域性的 watcher 佇列,等待執行。

watcher 類的 update 方法

update() {
  if(this.lazy) {
    // 懶執行走當前 if 分支,如 computed
    // 這裡的 標識 主要用於 computed 快取複用邏輯
    this.dirty = true
  } else if(this.sync) {
    // 同步執行,在 watch 選項引數傳 sync 時,走當前分支
    // 若為 true ,直接執行 watcher.run(),不塞入非同步更新佇列
    this.run()
  } else {
    // 正常更新走當前 else 分支
    queueWatcher(this)
  }
}
複製程式碼

queueWatcher 方法,發現熟悉的 nextTick 方法。看到這可以先跳到nextTick的原理,看明白了再折返。

function queueWatcher(watcher) {
  const id = watcher.id
  // 根據 watcher id 判斷是否在佇列中,若在佇列中,不重複入隊 
  if (has[id] == null) {
    has[id] = true
    // 全域性 queue 佇列未處於重新整理狀態,watcher 可入隊
    if (!flushing) {
      queue.push(watcher)
    // 全域性 queue 佇列處於重新整理狀態
    // 在單調遞增序列尋找當前 id 的位置並進行插入操作
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
   
    if (!waiting) {
      waiting = true
      // 同步執行邏輯
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // 將回調函式 flushSchedulerQueue 放入 callbacks 陣列
      nextTick(flushSchedulerQueue)
    }
  }
}
複製程式碼

nextTick 函式最終其實是執行 flushCallbacks 函式,flushCallbacks 函式則是執行 flushSchedulerQueue 回撥和專案中呼叫 nextTick 函式傳入的回撥。

搬運 flushSchedulerQueue 原始碼看做了些什麼

/**
*  更新 flushing 為 true,表示正在重新整理佇列,在此期間加入的 watcher 必須有序插入佇列,保證單調遞增
*  按照佇列的 watcher.id 從小到大排序,保證先建立的先執行
*  遍歷 watcher 佇列,按序執行 watcher.before 和 watcher.run,最後清除快取的 watcher
*/
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  // 標識正在重新整理佇列
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)
  // 未快取長度是因為可能在執行 watcher 時加入 watcher
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    // 清除快取的 watcher
    has[id] = null
    // 觸發更新函式,如 updateComponent 或 執行使用者的 watch 回撥
    watcher.run()
  }

  
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  
  // 執行 waiting = flushing = false,標識重新整理佇列結束,可以向瀏覽器的任務佇列加入下一個 flushCallbacks
  resetSchedulerState()
 
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}
複製程式碼

檢視下 watcher.run 做了些什麼,首先呼叫了 get 函式,我們一起看下。

/**
*  執行例項化 watcher 傳遞的第二個引數,如 updateComponent
*  更新舊值為新值
*  執行例項化 watcher 時傳遞的第三個引數,使用者傳遞的 watcher 回撥
*/
run () {
  if (this.active) {
    // 呼叫 get
    const value = this.get()
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      // 更新舊值為新值
      const oldValue = this.value
      this.value = value
      // 若是專案傳入的 watcher,則執行例項化傳遞的回撥函式。
      if (this.user) {
        const info = `callback for watcher "${this.expression}"`
        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}
/**
* 執行 this.getter,並重新收集依賴。
* 重新收集依賴是因為觸發更新 setter 中只做了響應式觀測,但沒有收集依賴的操作。
* 所以,在更新頁面時,會重新執行一次 render 函式,執行期間會觸發讀取操作,這時進行依賴收集。
*/
get () {
  // Dep.target = this
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 執行回撥函式,如 updateComponent,進入 patch 階段
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // watch 引數為 deep 的情況
    if (this.deep) {
      traverse(value)
    }
    // 關閉 Dep.target 置空
    popTarget()
    this.cleanupDeps()
  }
  return value
}
複製程式碼

Vue.$nextTick 的原理

nextTick:在下次 DOM 更新迴圈結束之後執行延遲迴調。常用於修改資料後獲取更新後的DOM。

原始碼位置:vue/src/core/util/next-tick.js

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

// 是否使用微任務標識
export let isUsingMicroTask = false

// 回撥函式佇列
const callbacks = []
// 非同步鎖
let pending = false

function flushCallbacks () {
  // 表示下一個 flushCallbacks 可以進入瀏覽器的任務隊列了
  pending = false
  // 防止 nextTick 中包含 nextTick時出現問題,在執行回撥函式佇列前,提前複製備份,清空回撥函式佇列
  const copies = callbacks.slice(0)
  // 清空 callbacks 陣列
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc

// 瀏覽器能力檢測
// 使用巨集任務或微任務的目的是巨集任務和微任務必在同步程式碼結束之後執行,這時能保證是最終渲染好的DOM。
// 巨集任務耗費時間是大於微任務,在瀏覽器支援的情況下,優先使用微任務。
// 巨集任務中效率也有差距,最低的就是 setTimeout
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  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)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 將 nextTick 的回撥函式用 try catch 包裹一層,用於異常捕獲
  // 將包裹後的函式放到 callback 中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // pengding 為 false, 執行 timerFunc
  if (!pending) {
    // 關上鎖
    pending = true
    timerFunc()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

複製程式碼

總結:

  1. 運用非同步鎖的概念,保證同一時刻任務佇列中只有一個 flushCallbacks。當 pengding 為 false 的時候,表示瀏覽器任務佇列中沒有 flushCallbacks 函式;當 pengding 為 true 的時候,表示瀏覽器任務佇列中已經放入 flushCallbacks;待執行 flushCallback 函式時,pengding 會被再次置為 false,表示下一個 flushCallbacks 可進入任務佇列。
  2. 環境能力檢測,選擇可選中效率最高的(巨集任務/微任務)進行包裝執行,保證是在同步程式碼都執行完成後再去執行修改 DOM 等操作。
  3. flushCallbacks 先拷貝再清空,為了防止nextTick巢狀nextTick導致迴圈不結束。

實現虛擬 DOM

虛擬 DOM 的出現解決了瀏覽器的效能問題。虛擬 DOM 是一個用 JS 模擬的 DOM 結構物件(Vnode),用於頻繁更改 DOM 操作後不立即更新 DOM,而是對比新老 Vnode,更新獲取最新的Vnode,最後再一次性對映成真實的 DOM。這樣做的原因是操作記憶體中操作 JS 物件速度比操作 DOM 快很多。

舉個真實 DOM 的

<div id="container">
  <p>real dom </p>
  <ul>
    <li class="item">item 1</li>
    <li class="item">item 2</li>
    <li class="item">item 3</li>
  </ul>
</div
複製程式碼

用 JS 來模擬 DOM 節點實現虛擬 DOM

function Element(tagName, props, children) {
  this.tageName = tagName
  this.props = props || {}
  this.children = children || []
  this.key = props.key
  let count = 0
  this.children.forEach(child => {
    if(child instanceof Element) count += child.count
    count++
  })
  this.count = count
}
const tree = Element('div', { id: container }, [
  Element('p', {}, ['real dom'])
  Element('ul', {}, [
    Element('li', { class: 'item' }, ['item1']),
    Element('li', { class: 'item' }, ['item2']),
    Element('li', { class: 'item' }, ['item3'])
  ])
])
複製程式碼

虛擬 DOM 轉為真實的節點

Element.prototype.render = function() {
  let el = document.createElement(this.tagName)
  let props = this.props
  for(let key in props) {
    el.setAttribute(key, props[key])
  }
  let children = this.children || []
  children.forEach(child => {
    let child = (child instanceof Element) ? child.render() : document.createTextNode(child)
    el.appendChild(child)
  })
  return el
}
複製程式碼

Vue 中 Diff 的原理

核心原始碼:vue/src/core/vdom/patch.js

搬運對比新老節點 patch 函式入口

/**
* 新節點不存在,老節點存在,呼叫 destroy,銷燬老節點
* 如果 oldVnode 是真實元素,則表示首次渲染,建立新節點,並插入 body,然後移除來節點
* 如果 oldVnode 不是真實元素,則表示更新階段,執行patchVnode
*/
function patch(oldVnode, vnode) {
  // 新的 Vnode 不存在,老的 Vnode 存在,銷燬老節點
  if(isUndef(vnode)) {
    if(isDef(oldVnode)) invokeDestroyHook(oldVnode) 
    return 
  }
  
  // 新的 Vnode 存在,老的 Vnode 不存在
  // <div id="app"><comp></comp></div>
  // 這裡的 com 元件初次渲染就走當前的 if 邏輯
  if(isUndef(oldVnode)) { 
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
      const isRealElement = isDef(oldVnode.nodeType)
      // 新老節點相同,更精細化對比
      if(!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode) 
      } else {
        // 是真實元素,渲染根元件
        if(isRealElement) { 
          // 掛載到真實元素以及處理服務端渲染情況
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // 基於真實節點建立一個 vnode
          oldVnode = emptyNodeAt(oldVnode)
       }
       // 獲取老節點的真實元素
       const oldElm = oldVnode.elm
       // 獲取老節點的父元素,即 body
       const parentElm = nodeOps.parentNode(oldElm)
       
       // 基於新的 vnode 建立整顆 DOM 樹並插入到 body 元素下
       creatElm(
         vnode, 
         insertedVnodeQueue, 
         oldElm._leaveCb ? null : parentElm, 
         nodeOps.nextSibling(oldElm)
       )
       
       // 遞迴更新父佔位符節點元素
       if(isDef(vnode.parent)) {
         ...
       }
       
       // 移除老節點
       if(isDef(parentEle)) {
         ...
       } else if(isDef(oldVnode.tag)) {
         ...
       }
    }
  }
}
複製程式碼

搬運 patchVnode 部分原始碼。

/**
* 更新節點
* 如果新老節點都有孩子,則遞迴執行 updateChildren
* 如果新節點有孩子,老節點沒孩子,則新增新節點的這些孩子節點
* 如果老節點有孩子,新節點沒孩子,則刪除老節點這些孩子
* 更新文字節點
*/
function patchVnode(oldVnode, vnode) {
  // 如果新老節點相同,直接返回   
  if(oldVnode === vnode) return 
  
  // 獲取新老節點的孩子節點
  const oldCh = oldVnode.children
  const ch = vnode.children
  
  // 新節點不是文字節點
  if(isUndef(vnode.text)) {
    // 新老節點都有孩子,則遞迴執行 updateChildren
    if(isDef(oldCh) && isDef(ch) && oldCh !== ch) { // oldVnode 與 vnode 的 children 不一致,更新children
      updateChildren(oldCh,ch)
    // 如果新節點有孩子,老節點沒孩子,則新增新節點的這些孩子節點
    } else if(isDef(ch)) { 
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    // 如果老節點有孩子,新節點沒孩子,則刪除老節點這些孩子
    } else if(isDef(oldCh)) { 
      removeVnodes(oldCh, 0, oldCh.length - 1)
    // 老節點文字存在,新的節點不存在文字,清空文字 
    } else if(isDef(oldVnode.text)){
      nodeOps.setTextContent(elm, '')
    }
  // 新老文字節點都是文字節點,且文字發生改變,則更新文字節點
  } else if(oldVnode.text !== vnode.text) { 
    nodeOps.setTextContent(elm, vnode.text)
  }
}
複製程式碼

搬運 updateChildren 原始碼。

function updateChildren(oldCh, ch) {
   // const oldCh = [n1, n2, n3, n4]
   // const ch = [n1, n2, n3, n4, n5]
   // 舊節點起始索引
   let oldStartIdx = 0 
   // 新節點起始索引
   let newStartIdx = 0 
   // 舊節點結束索引
   let oldEndIdx = oldCh.length - 1 
   // 新節點結束索引
   let newEndIdx = newCh.length - 1 
   while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
     const newStartVnode = ch[newStartIdx]
     const oldStartVnode = oldCh[oldStartIdx]
     const newEndVnode = ch[newEndIdx]
     const oldEndVnode = oldCh[oldEndIdx]
     // 如果節點被移動,在當前索引上可能不存在,檢測這種情況,如果節點不存在則調整索引
     if(isUndef(oldStartVnode)) {
       oldStartVnode = oldCh[++oldStartIdx]
     } else if(isUndef(oldEndVnode)) {
       oldEndVnode = oldCh[--oldEndIdx]
     // 新開始和老開始節點是同一個節點
     } else if(sameVnode(oldStartNode, newStartNode)) { 
       patchVnode(oldStartNode , newStartNode)
       oldStartIdx++
       newStartIdx++
     // 新開始節點和老結束節點是同一節點
     } else if(sameVnode(oldEndNode, newEndNode)) { 
       patchVnode(oldEndNode, newEndNode)
       oldEndIdx--
       newEndIdx--
     // 老開始和新結束是同一節點
     } else if(sameVnode(oldStartNode, newEndNode)) { 
       patchVnode(oldStartNode, newEndNode)
       oldStartIdx++
       newEndIdx--
     // 老結束和新開始是同一節點
     } else if(sameVnode(oldEndNode, newStartNode)) { 
       patchVnode(oldEndNode, newStartNode)
       oldEndIdx--
       newStartIdx++
     } else {
       // 上面假設都不成立,則通過遍歷找到新開始節點和老節點中的索引位置
       
       // 建立老節點每個節點 key 和 索引的關係 { key: idx }
       if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
       // 尋找新開始節點在老節點的索引位置
       idxInOld = isDef(newStartVnode.key)
         ? oldKeyToIdx[newStartVnode.key]
         : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
       
       // 沒有找到,則說明是新建立的元素,執行建立
       if (isUndef(idxInOld)) { 
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 在關係對映表中找到新開始節點
          vnodeToMove = oldCh[idxInOld]
          // 如果是同一個節點,則執行patch
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // patch 結束後將老節點置為 undefined
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
          // 最後這種情況是,找到節點,但發現兩個節點不是同一個節點,則視為新元素,執行建立
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        // 新節點向後移動一位
        newStartVnode = newCh[++newStartIdx]
     }
     if(newStartIdx < newEndIdx) {} // 舊節點先遍歷結束,將剩餘的新節點新增到DOM中
     if(oldStartIdx < oldEndIdx) {} // 新節點先遍歷結束,將剩餘的舊節點刪掉
   }
}
複製程式碼

Vue 中的 key 的作用

key 是 Vue 中 vnode 的唯一標記,我們的 diff 的演算法中 sameVnode 和 updateChildren 中就使用到了 key。

sameVnode 用來判斷是否為同一節點。常見的業務場景是一個列表,若 key 值是列表索引,在新增或刪除的情況下會存在就地複用的問題。(簡單說,複用了上一個在當前位置元素的狀態)所以 key 值的唯一,確保 diff 更準確。

updateChildren 中當其中四種假設都未匹配,就需要依賴老節點的 key 和 索引建立關係對映表,再用新節點的 key 去關係對映表去尋找索引進行更新,這保證 diff 演算法更加快速。

Vue 動態元件是什麼

動態元件通過 is 特性實現。適用於根據資料、動態渲染的場景,即元件型別不確定。

舉個新聞詳情頁案例,如下圖所示。

但是每篇新聞的詳情頁元件順序可能是不一樣的,所以我們得通過資料來動態渲染元件,而非寫死每個元件的順序。

<template>
  <div v-for="val in componentsData" :key="val.id">
    <component :is="val.type">
  </div>
</template>
<script>
import CustomTitle from './CustomTitle'
import CustomText from './CustomText'
import CustomImage from './CustomImage'

export default {
  data() {
    return {
      componentsData: [{
        id: 1,
        type: 'CustomTitle'
      },{
        id: 2,
        type: 'CustomText'
      },{
        id: 3
        type: 'CustomImage'
      }]
    }
  }
}
</script>
複製程式碼

Vue.directive 有寫過麼,應用場景有哪些?

Vue.directive 可以註冊全域性指令和區域性指令。

指令定義函式提供如下鉤子函式

  1. bind:指令第一次繫結到元素時呼叫(只調用一次)
  2. inserted: 被繫結元素插入父節點時使用(父節點存在即可呼叫)
  3. update:被繫結元素所在模板更新時呼叫,不論繫結值是否變化。通過比較更新前後的繫結值。
  4. componentUpdated: 被繫結元素所在模板完成一次更新週期時呼叫。
  5. unbind: 只調用一次,指令與元素解綁時呼叫。

我專案中有涉及 一鍵copy、許可權控制 都可以用指令的方式控制,目的就是簡化我們的工作量。

推薦一篇 分享8個非常實用的Vue自定義指令[5]

Vue 過濾器瞭解麼

Vue 過濾器可用在兩個地方:雙花括號插值和 v-bind 表示式。

Vue3 中已經廢棄這個特點。

過濾器分為 全域性過濾器 和 區域性過濾器。

區域性過濾器

<template>
  <div>{{ message | formatMessage }}</div>
</template>
<script>
export default {
  filters: {
    formatMessage: function(value) {
      // 可基於源值做一些處理
      return value
    }
  }
}
</script>
複製程式碼

全域性過濾器

Vue.filter('formatMessage', function(value) {
  // 可基於源值做一些處理
  return value
})
複製程式碼

過濾器可串聯,執行順序從左到右,第二個過濾器輸入值是第一個過濾器的輸出值。

<div>{{ message | formatMessage1 | formatMessage2 }}</div>
複製程式碼

關於 mixin 的理解,有什麼應用場景

定義:mixin(混入),提供了一種非常靈活的方式,來分發 Vue 元件中可複用的功能。

mixin 混入分全域性混入和區域性混入,本質是 JS 物件,如 data、components、computed、methods 等。

全域性混入不推薦使用,會影響後續每個Vue例項的建立。區域性混入可提取元件間相同的程式碼,進行邏輯複用。

適用場景:如多個頁面具備 相同 的懸浮定位浮窗,可嘗試用 mixin 封裝。

// customFloatDialog.js
export const customFloatDialog = {
  data() {
    return {
      visible: false
    }
  },
  methods: {
    toggleShow() {
      this.visible = !this.visible
    }
  }
}
</script>

//需要引入的元件
<template>
  <div></div>
</template>
<script>
import { customFloatDialog } from './customFloatDialog.js'
export default {
  mixins: [customFloatDialog],
}
</script>
複製程式碼

介紹一下 keep-alive

keep-alive 是 Vue 內建的一個元件,可以快取元件的狀態,避免重複渲染,提高效能。

keep-alive 內建元件有3個屬性

  1. include:字串或正則表示式,名稱匹配的元件會被快取。
  2. exclude:字串或正則表示式,名稱匹配的元件不會被快取。
  3. max:快取元件數量閾值

設定 keep-alive 的元件,會增加兩個生命鉤子(activated / deactivated)。

首次進入元件:beforeCreate -> created -> beforeMount -> mounted -> activated

離開元件觸發 deactivated ,因為元件快取不銷燬,所以不會觸發 beforeDestroy 和 destroyed 生命鉤子。再次進入元件後直接從 activated 生命鉤子開始。

常見業務場景:在列表頁的第 2 頁進入詳情頁,詳情頁返回,依然停留在第 2 頁,不重新渲染。但從其他頁面進入列表頁,還是需要重新渲染。

思路:vuex 使用陣列儲存列表頁名字,列表頁離開結合 beforeRouteLeave 鉤子判斷是否需要快取,對全域性陣列進行更改。

在 router-view 標籤位置如下使用

<template>
  <keep-alive :include="cacheRouting">
    <router-view></router-view>
  </keep-alive>
</template>
<script>
export default {
  computed: {
    cacheRouting() {
      return this.$store.state.cacheRouting
    }
  }
}
</script>
複製程式碼

列表頁如下使用

<template>
  <div></div>
</template>
<script>
export default {
  beforeRouteLeave(to, from, next) {
    if(to.name === '詳情頁') {
      // ... 向全域性快取路由陣列新增列表頁      
      next()
    } else {
      // ... 向全域性快取路由陣列刪除列表頁     
      next()
    }
  }
}
</script>
複製程式碼

keep-alive 的實現

核心原始碼:vue/src/core/components/keep-alive.js

LRU(Least Recently Used) 替換策略核心思想是替換最近最少使用。

/**
* 遍歷 cache 將不需要的快取的從 cache 中清除
*/
function pruneCache (keepAliveInstance, filter) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode = cache[key]
    if (cachedNode) {
      const name = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}
/**
* 刪除 cache 中鍵值為 key 的虛擬DOM
*/
function pruneCacheEntry (cache, key, keys, current) {
  const entry = cache[key]
  if (entry && (!current || entry.tag !== current.tag)) {
    // 執行元件的 destroy 鉤子
    entry.componentInstance.$destroy()
  }
  // cache 中元件對應的虛擬DOM置null
  cache[key] = null
  // 刪除快取虛擬DOM的 key
  remove(keys, key)
}

export default {
  name: 'keep-alive',
  abstract: true,  

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    // 快取虛擬 DOM
    this.cache = Object.create(null) 
    // 快取虛擬DOM的鍵集合
    this.keys = [] 
  },

  destroyed () {
    // 刪除所有的快取內容
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    // 監聽 include、exclude 引數變化,呼叫 pruneCache修改快取中的快取資料
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  // 由 render 函式決定渲染結果
  render () {
    const slot = this.$slots.default
    // 獲取第一個子元件虛擬DOM
    const vnode: VNode = getFirstComponentChild(slot)
    // 獲取虛擬 DOM 的配置引數
    const componentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // 獲取元件名稱
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      // 若不在include或者在exclude中,直接退出,不走快取機制
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      // 獲取元件key
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      // 命中快取
      if (cache[key]) {
        // 從 cache 中獲取快取的例項設定到當前的元件上
        vnode.componentInstance = cache[key].componentInstance
        // 刪除原有存在的key,並置於最後
        remove(keys, key)
        keys.push(key)
      // 未命中快取
      } else {
        // 快取當前虛擬節點
        cache[key] = vnode
        // 添加當前元件key
        keys.push(key)
        // 若快取元件超過max值,LRU 替換
        if(this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
      // 設定當前元件 keep-alive 為 true
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}
複製程式碼

Vue-Router 配置 404 頁面

* 代表萬用字元,若放在任意路由前,會被先匹配,導致跳轉到 404 頁面,所以需將如下配置置於最後。

{
  path: '*',
  name: '404'
  component: () => import('./404.vue')  
}
複製程式碼

Vue-Router 有哪幾種導航守衛

全域性前置守衛

在路由跳轉前觸發,可在執行 next 方法前做一些身份登入驗證的邏輯。

const router = new VueRouter({})

router.beforeEach((to, from, next) => {
  ...
  // 必須執行 next 方法來觸發路由跳轉 
  next() 
})
複製程式碼

全域性解析守衛

與 beforeEach 類似,也是路由跳轉前觸發,區別是還需在 所有元件內守衛和非同步路由元件被解析之後 ,也就是在元件內 beforeRouteEnter 之後被呼叫。

const router = new VueRouter({})

router.beforeResolve((to, from, next) => {
  ...
  // 必須執行 next 方法來觸發路由跳轉 
  next() 
})
複製程式碼

全域性後置鉤子

和守衛不同的是,這些鉤子不會接受 next 函式也不會改變導航本身。

router.afterEach((to, from) => {
  // ...
})
複製程式碼
  1. 路由獨享守衛

可在路由配置上直接定義 beforeEnter

const router = new VueRouter({
  routes: [
    {
      path: '/home',
      component: Home,
      beforeEnter: (to, from, next) => {
      
      }
    }
  ]
})
複製程式碼

元件內的守衛

元件內可直接定義如下路由導航守衛

const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 不能獲取元件例項 this
    // 當守衛執行前,元件例項還沒被建立
  },
  beforeRouteUpdate(to, from, next) {
    // 當前路由改變,但是元件被複用時呼叫
    // 可訪問例項 this
  },
  beforeRouteLeave(to, from, next) {
    // 導航離開元件時被呼叫
  }
}
複製程式碼

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 的回撥函式,建立好的元件例項會作為回撥函式的引數傳入

Vue-Router 路由有幾種模式?說說他們的區別?

Vue-Router 有 3 種路由模式:hash、history、abstract。

hash 模式

Vue-Router 預設為 hash 模式,基於瀏覽器的 onhashchange 事件,地址變化時,通過 window.location.hash 獲取地址上的hash值;根據hash值匹配 routes 物件對應的元件內容。

特點

#
onhashchange

案例程式碼,需在本地啟用服務 (http-server) 訪問, 已測試過,可直接 cv 體驗。

實現原理

<div class="main">
  <a href="#/home">home</a>
  <a href="#/detail">detail</a>
  <div id="content"><span>暫無內容</span></div>
</div>
<script>
  const routers = [{
    path: '/',
    component: `<span>暫無內容</span>`
  },
  {
    path: '/home',
    component: `<span>我是Home頁面</span>`
  }, {
    path: '/detail',
    component: `<span>我是Detail頁面</span>`
  }]

  function Router(routers) {
    console.log('執行')
    this.routers = {}
    // 初始化生成 routers
    routers.forEach((router) => {
      this.routers[router.path] = () => {
        document.getElementById("content").innerHTML = router.component;
      }
    })  
    this.updateView = function(e) {
      let hash = window.location.hash.slice(1) || '/'
      console.log('hash更新', hash, this.routers[hash])
      this.routers[hash] && this.routers[hash]()
    }
    // 路由載入觸發檢視更新
    window.addEventListener('load', this.updateView.bind(this))
    // 路由改變觸發檢視更新
    window.addEventListener('hashchange', this.updateView.bind(this))
  }
  // 例項化 hash 模式的 Router
  let router = new Router(routers) 
</scrip
複製程式碼

history 模式

基於HTML5新增的 pushState 和 replaceState 實現在不重新整理的情況下,操作瀏覽器的歷史紀錄。前者是新增歷史記錄,後者是直接替換歷史記錄。

特點

  1. URL 不攜帶`#`,利用 pushState 和 replaceState 完成 URL 跳轉而無須重新載入頁面。
  2. URL 更改會觸發 http 請求。所以在服務端需增加一個覆蓋所有情況的候選資源:若URL匹配不到任何靜態資源,則應該返回同一個`index.html`。這個頁面就是app依賴的頁面。
// nginx 服務端配置
location / {
  try_files $uri $uri/ /index.html;
}
複製程式碼
  1. 相容性 IE10+

實現原理

<div class="main">
  <a href="javascript:;" path="/home">home</a>
  <a href="javascript:;" path="/detail">detail</a>
  <div id="content"><span>暫無內容</span></div>
</div>

<script>
const routers = [{
  path: '/home',
  component: `<span>我是Home頁面</span>`
}, {
  path: '/detail',
  component: `<span>我是Detail頁面</span>`
}, {
  path: '/',
  component: '<span>暫無內容</span>'
}]

function Router(routers) {
  this.routers = {}
  // 初始化生成 routers
  routers.forEach((router) => {
    this.routers[router.path] = () => {
      document.getElementById("content").innerHTML = router.component;
    }
  })
  const links = [...document.getElementsByTagName('a')]
  links.forEach(link => {
    link.addEventListener('click', () => {
      window.history.pushState({}, null, link.getAttribute('path'))
      this.updateView()
    })
  })
  this.updateView = function() {
    let url = window.location.pathname || '/'
    this.routers[url] && this.routers[url]()
  }
  // 路由載入觸發檢視更新
  window.addEventListener('load', this.updateView.bind(this))
  // 路由改變觸發檢視更新
  window.addEventListener('popstate', this.updateView.bind(this))
}
// 例項化 history 模式的 Router
const router = new Router(routers)
</script>
複製程式碼

abstract 模式

支援所有 JS 執行模式,Vue-Router 自身會對環境做校驗,如果發現沒有瀏覽器 API,路由會自動強制進入 abstract 模式。在移動端原生環境也是使用 abstract 模式。

Vue 路由傳參方式

Vue 路由有 三種 方式進行傳參

  1. 方案一
// 路由配置
{
  path: '/detail/:id',
  name: 'Detail',
  component: () => import('./Detail.vue')
}
// 路由跳轉
let id = 1
this.$router.push({ path: '/detail/${id}'})
// 獲取引數
this.$route.params.id
複製程式碼
  1. 方案二

方案二,URL 雖然不顯示我們的傳參,但是是可以在子元件獲取引數的。當然也有問題:會存在重新整理丟失引數。

若想不丟失,需和方案一路由配置一樣。原因是第二種方式傳參是上一個頁面 push 函式中攜帶的,重新整理沒有 push 的動作。

// 路由配置
{
  path: '/detail',
  name: 'Detail',
  component: () => import('./Detail.vue')
}
// 路由跳轉
let id = 1
this.$router.push({ name: 'Detail', params: { id: id } })
// 獲取引數
this.$route.params.id
複製程式碼
  1. 方案三
// 路由配置
{
  path: '/detail',
  name: 'Detail',
  component: () => import('./Detail.vue')
}
// 路由跳轉
let id = 1
this.$router.push({ name: 'Detail', query: { id: id } })
// 獲取引數
this.$route.query.id
複製程式碼

Vuex 的理解及使用

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式,採用集中式儲存管理應用的所有元件的狀態。

主要解決如下 兩個 問題

  1. 多個檢視依賴同一狀態。
  2. 來自不同檢視的行為需要變更同一個狀態。

其包含如下模組,搬官網圖

State:定義並初始化全域性狀態。

Getter: 依賴 State 中的狀態,進行二次包裝,不會影響 State 源資料。

Mutation: 更改 State 狀態的函式,必須是同步。

Action:用於提交 Mutation,可包含任意非同步操作。

Module:若應用複雜,Store 會集中一個比較大的物件而顯得臃腫,Module允許我們將 Store模組化管理。

當然,若應用比較簡單,共享狀態也比較少,可以用 Vue.observe 去替代 Vuex,省去安裝一個庫也挺好。

Vuex 重新整理後資料丟失怎麼辦

持久化快取:localStorage、sessionStorage

Vuex 如何知道 State 是通過 Mutation 修改還是外部修改?

Vuex 中修改 state 唯一渠道是執行 commit 方法,底層通過執行 this._withCommit(fn),且設定_committing識別符號為 true,才能修改 state,修改完還需要將識別符號置為 false。外部修改是無法設定標識位的,所以通過 watch 監聽 state 變化,來判斷修改的合法性。

Vue SSR 瞭解麼

Vue SSR 專案中暫時還沒有運用過,後續會寫個 Demo 單獨成文吧。這邊搬運下其他答案。

SSR 服務端渲染,將 HTML 渲染工作放在服務端完成後,將 HTML 返回到瀏覽器端。

優點:SSR有更好的 SEO,首屏載入更快。

缺點:服務端負載大。

如果是內部系統,SSR其實沒有太多必要。如果是對外的專案,維護高可用的node伺服器是個難點。

Vue2 與 Vue3 的區別 ?Vue3有哪些優化點?

自產自銷: 【持續更新】梳理 Vue3 相比於 Vue2 的有哪些 “與眾不同” ?[6]

Vue 效能優化

  1. 非響應式資料通過 Object.freeze 凍結資料
  2. 巢狀層級不要過深
  3. computed 和 watch 區別使用
  4. v-if 和 v-show 區別使用
  5. v-for 避免和 v-if 一起使用,且繫結 key 值要唯一
  6. 列表資料過多采用分頁或者虛擬列表
  7. 元件銷燬後清除定時器和事件
  8. 圖片懶載入
  9. 路由懶載入
  10. 防抖、節流
  11. 按需引入外部庫
  12. keep-alive快取使用
  13. 服務端渲染和預渲染

總結

萬字長文總結,若覺得有幫助的,順帶的點贊、關注、收藏。

原作者姓名: shaoqing

原出處:掘金

原文連結: 【前車之鑑】Vue,你真的熟練了麼? - 掘金