滴滴前端高頻vue面試題(邊面邊更)

語言: CN / TW / HK

Vue-router 路由模式有幾種

vue-router3 種路由模式:hashhistoryabstract,對應的源碼如下所示

javascript switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } }

其中,3 種路由模式的説明如下:

  • hash: 使用 URL hash 值來作路由,支持所有瀏覽器
  • history : 依賴 HTML5 History API 和服務器配置
  • abstract : 支持所有 JavaScript 運行環境,如 Node.js 服務器端。如果發現沒有瀏覽器的 API,路由會自動強制進入這個模式.

為什麼 Vuex 的 mutation 中不能做異步操作?

  • Vuex中所有的狀態更新的唯一途徑都是mutation,異步操作通過 Action 來提交 mutation實現,這樣可以方便地跟蹤每一個狀態的變化,從而能夠實現一些工具幫助更好地瞭解我們的應用。
  • 每個mutation執行完成後都會對應到一個新的狀態變更,這樣devtools就可以打個快照存下來,然後就可以實現 time-travel 了。如果mutation支持異步操作,就沒有辦法知道狀態是何時更新的,無法很好的進行狀態的追蹤,給調試帶來困難。

Vue組件之間通信方式有哪些

Vue 組件間通信是面試常考的知識點之一,這題有點類似於開放題,你回答出越多方法當然越加分,表明你對 Vue 掌握的越熟練。 Vue 組件間通信只要指以下 3 類通信父子組件通信隔代組件通信兄弟組件通信,下面我們分別介紹每種通信方式且會説明此種方法可適用於哪類組件間通信

組件傳參的各種方式

組件通信常用方式有以下幾種

  • props / $emit 適用 父子組件通信
  • 父組件向子組件傳遞數據是通過 prop 傳遞的,子組件傳遞數據給父組件是通過$emit 觸發事件來做到的
  • ref$parent / $children(vue3廢棄) 適用 父子組件通信
  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子組件上,引用就指向組件實例
  • $parent / $children:訪問訪問父組件的屬性或方法 / 訪問子組件的屬性或方法
  • EventBus ($emit / $on) 適用於 父子、隔代、兄弟組件通信
  • 這種方法通過一個空的 Vue 實例作為中央事件總線(事件中心),用它來觸發事件和監聽事件,從而實現任何組件間的通信,包括父子、隔代、兄弟組件
  • $attrs / $listeners(vue3廢棄) 適用於 隔代組件通信
  • $attrs:包含了父作用域中不被 prop 所識別 (且獲取) 的特性綁定 ( classstyle 除外 )。當一個組件沒有聲明任何 prop時,這裏會包含所有父作用域的綁定 ( classstyle 除外 ),並且可以通過 v-bind="$attrs" 傳入內部組件。通常配合 inheritAttrs 選項一起使用
  • $listeners:包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它可以通過 v-on="$listeners" 傳入內部組件
  • provide / inject 適用於 隔代組件通信
  • 祖先組件中通過 provider 來提供變量,然後在子孫組件中通過 inject 來注入變量。 provide / inject API 主要解決了跨級組件間的通信問題, 不過它的使用場景,主要是子組件獲取上級組件的狀態 ,跨級組件間建立了一種主動提供與依賴注入的關係
  • $root 適用於 隔代組件通信 訪問根組件中的屬性或方法,是根組件,不是父組件。$root只對根組件有用
  • Vuex 適用於 父子、隔代、兄弟組件通信
  • Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。每一個 Vuex 應用的核心就是 store(倉庫)。“store” 基本上就是一個容器,它包含着你的應用中大部分的狀態 ( state )
  • Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的組件也會相應地得到高效更新。
  • 改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化。

根據組件之間關係討論組件通信最為清晰有效

  • 父子組件:props/$emit/$parent/ref
  • 兄弟組件:$parent/eventbus/vuex
  • 跨層級關係:eventbus/vuex/provide+inject/$attrs + $listeners/$root

下面演示組件之間通訊三種情況: 父傳子、子傳父、兄弟組件之間的通訊

1. 父子組件通信

使用props,父組件可以使用props向子組件傳遞數據。

父組件vue模板father.vue:

```html

```

子組件vue模板child.vue:

```html

```

回調函數(callBack)

父傳子:將父組件裏定義的method作為props傳入子組件

javascript // 父組件Parent.vue: <Child :changeMsgFn="changeMessage"> methods: { changeMessage(){ this.message = 'test' } }

javascript // 子組件Child.vue: <button @click="changeMsgFn"> props:['changeMsgFn']

子組件向父組件通信

父組件向子組件傳遞事件方法,子組件通過$emit觸發事件,回調給父組件

父組件vue模板father.vue:

```html

```

子組件vue模板child.vue:

```html

```

2. provide / inject 跨級訪問祖先組件的數據

父組件通過使用provide(){return{}}提供需要傳遞的數據

javascript export default { data() { return { title: '我是父組件', name: 'poetry' } }, methods: { say() { alert(1) } }, // provide屬性 能夠為後面的後代組件/嵌套的組件提供所需要的變量和方法 provide() { return { message: '我是祖先組件提供的數據', name: this.name, // 傳遞屬性 say: this.say } } }

子組件通過使用inject:[“參數1”,”參數2”,…]接收父組件傳遞的參數

```html

```

3. $parent + $children 獲取父組件實例和子組件實例的集合

  • this.$parent 可以直接訪問該組件的父實例或組件
  • 父組件也可以通過 this.$children 訪問它所有的子組件;需要注意 $children 並不保證順序,也不是響應式的

```html

```

```html

```

```html

```

4. $attrs + $listeners多級組件通信

$attrs 包含了從父組件傳過來的所有props屬性

```javascript // 父組件Parent.vue:

// 子組件Child.vue:

// 孫子組件GrandChild

姓名:{{$attrs.name}}

年齡:{{$attrs.age}}

```

$listeners包含了父組件監聽的所有事件

```javascript // 父組件Parent.vue:

// 子組件Child.vue: ```

5. ref 父子組件通信

```javascript // 父組件Parent.vue: changeName(){ console.log(this.$refs.childComp.age); this.$refs.childComp.changeAge() }

// 子組件Child.vue: data(){ return{ age:20 } }, methods(){ changeAge(){ this.age=15 } } ```

6. 非父子, 兄弟組件之間通信

vue2中廢棄了broadcast廣播和分發事件的方法。父子組件中可以用props$emit()。如何實現非父子組件間的通信,可以通過實例一個vue實例Bus作為媒介,要相互通信的兄弟組件之中,都引入Bus,然後通過分別調用Bus事件觸發和監聽來實現通信和參數傳遞。Bus.js可以是這樣:

```javascript // Bus.js

// 創建一箇中央時間總線類
class Bus {
constructor() {
this.callbacks = {}; // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}

// main.js
Vue.prototype.$bus = new Bus() // 將$bus掛載到vue實例的原型上
// 另一種方式
Vue.prototype.$bus = new Vue() // Vue已經實現了Bus的功能
```

```html

```

另一個組件也在鈎子函數中監聽on事件

javascript export default { data() { return { message: '' } }, mounted() { this.$bus.$on('foo', (msg) => { this.message = msg }) } }

7. $root 訪問根組件中的屬性或方法

  • 作用:訪問根組件中的屬性或方法
  • 注意:是根組件,不是父組件。$root只對根組件有用

javascript var vm = new Vue({ el: "#app", data() { return { rootInfo:"我是根元素的屬性" } }, methods: { alerts() { alert(111) } }, components: { com1: { data() { return { info: "組件1" } }, template: "<p>{{ info }} <com2></com2></p>", components: { com2: { template: "<p>我是組件1的子組件</p>", created() { this.$root.alerts()// 根組件方法 console.log(this.$root.rootInfo)// 我是根元素的屬性 } } } } } });

8. vuex

  • 適用場景: 複雜關係的組件數據傳遞
  • Vuex作用相當於一個用來存儲共享變量的容器

  • state用來存放共享變量的地方
  • getter,可以增加一個getter派生狀態,(相當於store中的計算屬性),用來獲得共享變量的值
  • mutations用來存放修改state的方法。
  • actions也是用來存放修改state的方法,不過action是在mutations的基礎上進行。常用來做一些異步操作

小結

  • 父子關係的組件數據傳遞選擇 props$emit進行傳遞,也可選擇ref
  • 兄弟關係的組件數據傳遞可選擇$bus,其次可以選擇$parent進行傳遞
  • 祖先與後代組件數據傳遞可選擇attrslisteners或者 ProvideInject
  • 複雜關係的組件數據傳遞可以通過vuex存放共享的變量

Vue中組件和插件有什麼區別

1. 組件是什麼

組件就是把圖形、非圖形的各種邏輯均抽象為一個統一的概念(組件)來實現開發的模式,在Vue中每一個.vue文件都可以視為一個組件

組件的優勢

  • 降低整個系統的耦合度,在保持接口不變的情況下,我們可以替換不同的組件快速完成需求,例如輸入框,可以替換為日曆、時間、範圍等組件作具體的實現
  • 調試方便,由於整個系統是通過組件組合起來的,在出現問題的時候,可以用排除法直接移除組件,或者根據報錯的組件快速定位問題,之所以能夠快速定位,是因為每個組件之間低耦合,職責單一,所以邏輯會比分析整個系統要簡單
  • 提高可維護性,由於每個組件的職責單一,並且組件在系統中是被複用的,所以對代碼進行優化可獲得系統的整體升級

2. 插件是什麼

插件通常用來為 Vue 添加全局功能。插件的功能範圍沒有嚴格的限制——一般有下面幾種:

  • 添加全局方法或者屬性。如: vue-custom-element
  • 添加全局資源:指令/過濾器/過渡等。如 vue-touch
  • 通過全局混入來添加一些組件選項。如vue-router
  • 添加 Vue 實例方法,通過把它們添加到 Vue.prototype 上實現。
  • 一個庫,提供自己的 API,同時提供上面提到的一個或多個功能。如vue-router

3. 兩者的區別

兩者的區別主要表現在以下幾個方面:

  • 編寫形式
  • 註冊形式
  • 使用場景

3.1 編寫形式

編寫組件

編寫一個組件,可以有很多方式,我們最常見的就是vue單文件的這種格式,每一個.vue文件我們都可以看成是一個組件

vue文件標準格式

```html

```

我們還可以通過template屬性來編寫一個組件,如果組件內容多,我們可以在外部定義template組件內容,如果組件內容並不多,我們可直接寫在template屬性上

```html

Vue.component('componentA',{ template: '#testComponent'
template: <div>component</div> // 組件內容少可以通過這種形式 }) ```

編寫插件

vue插件的實現應該暴露一個 install 方法。這個方法的第一個參數是 Vue 構造器,第二個參數是一個可選的選項對象

```javascript MyPlugin.install = function (Vue, options) { // 1. 添加全局方法或 property Vue.myGlobalMethod = function () { // 邏輯... }

// 2. 添加全局資源 Vue.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // 邏輯... } ... })

// 3. 注入組件選項 Vue.mixin({ created: function () { // 邏輯... } ... })

// 4. 添加實例方法 Vue.prototype.$myMethod = function (methodOptions) { // 邏輯... } } ```

3.2 註冊形式

組件註冊

vue組件註冊主要分為全局註冊局部註冊

全局註冊通過Vue.component方法,第一個參數為組件的名稱,第二個參數為傳入的配置項

javascript Vue.component('my-component-name', { /* ... */ })

局部註冊只需在用到的地方通過components屬性註冊一個組件

```javascript const component1 = {...} // 定義一個組件

export default { components:{ component1 // 局部註冊 } } ```

插件註冊

插件的註冊通過Vue.use()的方式進行註冊(安裝),第一個參數為插件的名字,第二個參數是可選擇的配置項

javascript Vue.use(插件名字,{ /* ... */} )

注意的是:

註冊插件的時候,需要在調用 new Vue() 啟動應用之前完成

Vue.use會自動阻止多次註冊相同插件,只會註冊一次

4. 使用場景

  • 組件 (Component) 是用來構成你的 App 的業務模塊,它的目標是 App.vue
  • 插件 (Plugin) 是用來增強你的技術棧的功能模塊,它的目標是 Vue 本身

簡單來説,插件就是指對Vue的功能的增強或補充

Watch中的deep:true是如何實現的

當用户指定了 watch 中的deep屬性為 true 時,如果當前監控的值是數組類型。會對對象中的每一項進行求值,此時會將當前 watcher存入到對應屬性的依賴中,這樣數組中對象發生變化時也會通知數據更新

源碼相關

javascript get () { pushTarget(this) // 先將當前依賴放到 Dep.target上 let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { // 如果需要深度監控 traverse(value) // 會對對象中的每一項取值,取值時會執行對應的get方法 }popTarget() }

Vue中diff算法原理

DOM操作是非常昂貴的,因此我們需要儘量地減少DOM操作。這就需要找出本次DOM必須更新的節點來更新,其他的不更新,這個找出的過程,就需要應用diff算法

vuediff算法是平級比較,不考慮跨級比較的情況。內部採用深度遞歸的方式+雙指針(頭尾都加指針)的方式進行比較。

簡單來説,Diff算法有以下過程

  • 同級比較,再比較子節點(根據keytag標籤名判斷)
  • 先判斷一方有子節點和一方沒有子節點的情況(如果新的children沒有子節點,將舊的子節點移除)
  • 比較都有子節點的情況(核心diff)
  • 遞歸比較子節點
  • 正常Diff兩個樹的時間複雜度是O(n^3),但實際情況下我們很少會進行跨層級的移動DOM,所以VueDiff進行了優化,從O(n^3) -> O(n),只有當新舊children都為多個子節點時才需要用核心的Diff算法進行同層級比較。
  • Vue2的核心Diff算法採用了雙端比較的算法,同時從新舊children的兩端開始進行比較,藉助key值找到可複用的節點,再進行相關操作。相比ReactDiff算法,同樣情況下可以減少移動節點次數,減少不必要的性能損耗,更加的優雅
  • 在創建VNode時就確定其類型,以及在mount/patch的過程中採用位運算來判斷一個VNode的類型,在這個基礎之上再配合核心的Diff算法,使得性能上較Vue2.x有了提升

vue3中採用最長遞增子序列來實現diff優化

回答範例

思路

  • diff算法是幹什麼的
  • 它的必要性
  • 它何時執行
  • 具體執行方式
  • 拔高:説一下vue3中的優化

回答範例

  1. Vue中的diff算法稱為patching算法,它由Snabbdom修改而來,虛擬DOM要想轉化為真實DOM就需要通過patch方法轉換
  2. 最初Vue1.x視圖中每個依賴均有更新函數對應,可以做到精準更新,因此並不需要虛擬DOMpatching算法支持,但是這樣粒度過細導致Vue1.x無法承載較大應用;Vue 2.x中為了降低Watcher粒度,每個組件只有一個Watcher與之對應,此時就需要引入patching算法才能精確找到發生變化的地方並高效更新
  3. vuediff執行的時刻是組件內響應式數據變更觸發實例執行其更新函數時,更新函數會再次執行render函數獲得最新的虛擬DOM,然後執行patch函數,並傳入新舊兩次虛擬DOM,通過比對兩者找到變化的地方,最後將其轉化為對應的DOM操作
  4. patch過程是一個遞歸過程,遵循深度優先、同層比較的策略;以vue3patch為例
  5. 首先判斷兩個節點是否為相同同類節點,不同則刪除重新創建
  6. 如果雙方都是文本則更新文本內容
  7. 如果雙方都是元素節點則遞歸更新子元素,同時更新元素屬性
  8. 更新子節點時又分了幾種情況
  9. 新的子節點是文本,老的子節點是數組則清空,並設置文本;
  10. 新的子節點是文本,老的子節點是文本則直接更新文本;
  11. 新的子節點是數組,老的子節點是文本則清空文本,並創建新子節點數組中的子元素;
  12. 新的子節點是數組,老的子節點也是數組,那麼比較兩組子節點,更新細節blabla
  13. vue3中引入的更新策略:靜態節點標記等

vdom中diff算法的簡易實現

以下代碼只是幫助大家理解diff算法的原理和流程

  1. vdom轉化為真實dom

```javascript const createElement = (vnode) => { let tag = vnode.tag; let attrs = vnode.attrs || {}; let children = vnode.children || []; if(!tag) { return null; } //創建元素 let elem = document.createElement(tag); //屬性 let attrName; for (attrName in attrs) { if(attrs.hasOwnProperty(attrName)) { elem.setAttribute(attrName, attrs[attrName]); } } //子元素 children.forEach(childVnode => { //給elem添加子元素 elem.appendChild(createElement(childVnode)); })

//返回真實的dom元素 return elem; } ```

  1. 用簡易diff算法做更新操作

```javascript function updateChildren(vnode, newVnode) { let children = vnode.children || []; let newChildren = newVnode.children || [];

children.forEach((childVnode, index) => { let newChildVNode = newChildren[index]; if(childVnode.tag === newChildVNode.tag) { //深層次對比, 遞歸過程 updateChildren(childVnode, newChildVNode); } else { //替換 replaceNode(childVnode, newChildVNode); } }) } ```

參考 前端進階面試題詳細解答

為什麼要使用異步組件

  1. 節省打包出的結果,異步組件分開打包,採用jsonp的方式進行加載,有效解決文件過大的問題。
  2. 核心就是包組件定義變成一個函數,依賴import() 語法,可以實現文件的分割加載。

javascript components:{ AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }

原理

javascript export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默認調用此函數時返回 undefiend // 第二次渲染時Ctor不為undefined if (Ctor === undefined) { return createAsyncPlaceholder( // 渲染佔位符 空虛擬節點 asyncFactory, data, context, children, tag ) } } } function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void { if (isDef(factory.resolved)) { // 3.在次渲染時可以拿到獲取的最新組件 return factory.resolved } const resolve = once((res: Object | Class<Component>) => { factory.resolved = ensureCtor(res, baseCtor) if (!sync) { forceRender(true) //2. 強制更新視圖重新渲染 } else { owners.length = 0 } }) const reject = once(reason => { if (isDef(factory.errorComp)) { factory.error = true forceRender(true) } }) const res = factory(resolve, reject)// 1.將resolve方法和reject方法傳入,用户調用 resolve方法後 sync = false return factory.resolved }

從0到1自己構架一個vue項目,説説有哪些步驟、哪些重要插件、目錄結構你會怎麼組織

綜合實踐類題目,考查實戰能力。沒有什麼絕對的正確答案,把平時工作的重點有條理的描述一下即可

思路

  • 構建項目,創建項目基本結構
  • 引入必要的插件:
  • 代碼規範:prettiereslint
  • 提交規範:husky,lint-staged`
  • 其他常用:svg-loadervueusenprogress
  • 常見目錄結構

回答範例

  1. 0創建一個項目我大致會做以下事情:項目構建、引入必要插件、代碼規範、提交規範、常用庫和組件
  2. 目前vue3項目我會用vite或者create-vue創建項目
  3. 接下來引入必要插件:路由插件vue-router、狀態管理vuex/piniaui庫我比較喜歡element-plus和antd-vuehttp工具我會選axios
  4. 其他比較常用的庫有vueusenprogress,圖標可以使用vite-svg-loader
  5. 下面是代碼規範:結合prettiereslint即可
  6. 最後是提交規範,可以使用huskylint-stagedcommitlint
  7. 目錄結構我有如下習慣: .vscode:用來放項目中的 vscode 配置
  8. plugins:用來放 vite 插件的 plugin 配置
  9. public:用來放一些諸如 頁頭icon 之類的公共文件,會被打包到dist根目錄下
  10. src:用來放項目代碼文件
  11. api:用來放http的一些接口配置
  12. assets:用來放一些 CSS 之類的靜態資源
  13. components:用來放項目通用組件
  14. layout:用來放項目的佈局
  15. router:用來放項目的路由配置
  16. store:用來放狀態管理Pinia的配置
  17. utils:用來放項目中的工具方法類
  18. views:用來放項目的頁面文件

keep-alive 使用場景和原理

  • keep-aliveVue 內置的一個組件, 可以實現組件緩存 ,當組件切換時不會對當前組件進行卸載。 一般結合路由和動態組件一起使用 ,用於緩存組件
  • 提供 includeexclude 屬性, 允許組件有條件的進行緩存 。兩者都支持字符串或正則表達式,include 表示只有名稱匹配的組件會被緩存,exclude 表示任何名稱匹配的組件都不會被緩存 ,其中 exclude 的優先級比 include
  • 對應兩個鈎子函數 activateddeactivated ,當組件被激活時,觸發鈎子函數 activated,當組件被移除時,觸發鈎子函數 deactivated
  • keep-alive 的中還運用了 LRU(最近最少使用) 算法,選擇最近最久未使用的組件予以淘汰
  • <keep-alive></keep-alive> 包裹動態組件時,會緩存不活動的組件實例,主要用於保留組件狀態或避免重新渲染
  • 比如有一個列表和一個詳情,那麼用户就會經常執行打開詳情=>返回列表=>打開詳情…這樣的話列表和詳情都是一個頻率很高的頁面,那麼就可以對列表組件使用<keep-alive></keep-alive>進行緩存,這樣用户每次返回列表的時候,都能從緩存中快速渲染,而不是重新渲染

關於keep-alive的基本用法

html <keep-alive> <component :is="view"></component> </keep-alive>

使用includesexclude

```html

```

匹配首先檢查組件自身的 name 選項,如果 name 選項不可用,則匹配它的局部註冊名稱 (父組件 components 選項的鍵值),匿名組件不能被匹配

設置了 keep-alive 緩存的組件,會多出兩個生命週期鈎子(activateddeactivated):

  • 首次進入組件時:beforeRouteEnter > beforeCreate > created> mounted > activated > ... ... > beforeRouteLeave > deactivated
  • 再次進入組件時:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated

使用場景

使用原則:當我們在某些場景下不需要讓頁面重新加載時我們可以使用keepalive

舉個栗子:

當我們從首頁–>列表頁–>商詳頁–>再返回,這時候列表頁應該是需要keep-alive

首頁–>列表頁–>商詳頁–>返回到列表頁(需要緩存)–>返回到首頁(需要緩存)–>再次進入列表頁(不需要緩存),這時候可以按需來控制頁面的keep-alive

在路由中設置keepAlive屬性判斷是否需要緩存

javascript { path: 'list', name: 'itemList', // 列表頁 component (resolve) { require(['@/pages/item/list'], resolve) }, meta: { keepAlive: true, title: '列表頁' } }

使用<keep-alive>

```html

```

思考題:緩存後如何獲取數據

解決方案可以有以下兩種:

  • beforeRouteEnter:每次組件渲染的時候,都會執行beforeRouteEnter

javascript beforeRouteEnter(to, from, next){ next(vm=>{ console.log(vm) // 每次進入路由執行 vm.getData() // 獲取數據 }) },

  • actived:在keep-alive緩存的組件被激活的時候,都會執行actived鈎子

javascript // 注意:服務器端渲染期間avtived不被調用 activated(){ this.getData() // 獲取數據 },

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

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

相關代碼

keep-alivevue中內置的一個組件

源碼位置:src/core/components/keep-alive.js

```javascript 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); //獲取第一個子組件 // 獲取該組件節點的componentOptions const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions; if (componentOptions) { // 獲取該組件節點的名稱,優先獲取組件的name字段,如果name不存在則獲取組件的tag const name: ?string = getComponentName(componentOptions); const { include, exclude } = this; // 不走緩存 如果name不在inlcude中或者存在於exlude中則表示不緩存,直接返回vnode if ( // not included 不包含 (include && (!name || !matches(include, name))) || // excluded 排除裏面 (exclude && name && matches(exclude, name)) ) { //返回虛擬節點 return vnode; }

  const { cache, keys } = this;
  // 獲取組件的key值
  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;
  // 拿到key值後去this.cache對象中去尋找是否有該值,如果有則表示該組件有緩存,即命中緩存
  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]);

}, }; ```

可以看到該組件沒有template,而是用了render,在組件渲染的時候會自動執行render函數

this.cache是一個對象,用來存儲需要緩存的組件,它將以如下形式存儲:

javascript this.cache = { 'key1':'組件1', 'key2':'組件2', // ... }

在組件銷燬的時候執行pruneCacheEntry函數

javascript function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode ) { const cached = cache[key] /* 判斷當前沒有處於被渲染狀態的組件,將其銷燬*/ if (cached && (!current || cached.tag !== current.tag)) { cached.componentInstance.$destroy() } cache[key] = null remove(keys, key) }

mounted鈎子函數中觀測 includeexclude 的變化,如下:

javascript mounted () { this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }

如果includeexclude 發生了變化,即表示定義需要緩存的組件的規則或者不需要緩存的組件的規則發生了變化,那麼就執行pruneCache函數,函數如下

javascript 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) } } } }

在該函數內對this.cache對象進行遍歷,取出每一項的name值,用其與新的緩存規則進行匹配,如果匹配不上,則表示在新的緩存規則下該組件已經不需要被緩存,則調用pruneCacheEntry函數將其從this.cache對象剔除即可

關於keep-alive的最強大緩存功能是在render函數中實現

首先獲取組件的key值:

go const key = vnode.key == null? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key

拿到key值後去this.cache對象中去尋找是否有該值,如果有則表示該組件有緩存,即命中緩存,如下:

go /* 如果命中緩存,則直接從緩存中拿 vnode 的組件實例 */ if (cache[key]) { vnode.componentInstance = cache[key].componentInstance /* 調整該組件key的順序,將其從原來的地方刪掉並重新放在最後一個 */ remove(keys, key) keys.push(key) }

直接從緩存中拿 vnode 的組件實例,此時重新調整該組件key的順序,將其從原來的地方刪掉並重新放在this.keys中最後一個

this.cache對象中沒有該key值的情況,如下:

go /* 如果沒有命中緩存,則將其設置進緩存 */ else { cache[key] = vnode keys.push(key) /* 如果配置了max並且緩存的長度超過了this.max,則從緩存中刪除第一個 */ if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } }

表明該組件還沒有被緩存過,則以該組件的key為鍵,組件vnode為值,將其存入this.cache中,並且把key存入this.keys

此時再判斷this.keys中緩存組件的數量是否超過了設置的最大緩存數量值this.max,如果超過了,則把第一個緩存組件刪掉

vue-router 動態路由是什麼

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

```javascript const User = { template: "

User
", };

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

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

解決方法:

  1. 通過 watch 監聽路由參數再發請求

javascript watch: { //通過watch來監聽路由變化 "$route": function(){ this.getData(this.$route.params.xxx); } }

  1. :key 來阻止“複用”

html <router-view :key="$route.fullPath" />

回答範例

  1. 很多時候,我們需要將給定匹配模式的路由映射到同一個組件,這種情況就需要定義動態路由
  2. 例如,我們可能有一個 User 組件,它應該對所有用户進行渲染,但用户 ID 不同。在 Vue Router中,我們可以在路徑中使用一個動態字段來實現,例如:{ path: '/users/:id', component: User },其中:id就是路徑參數
  3. 路徑參數 用冒號 : 表示。當一個路由被匹配時,它的 params 的值將在每個組件中以 this.$route.params 的形式暴露出來。
  4. 參數還可以有多個,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 對象還公開了其他有用的信息,如 $route.query$route.hash

diff算法

時間複雜度: 個樹的完全diff 算法是一個時間複雜度為O(n*3) ,vue進行優化轉化成O(n)

理解:

  • 最小量更新,key 很重要。這個可以是這個節點的唯一標識,告訴diff 算法,在更改前後它們是同一個DOM節點
  • 擴展v-for 為什麼要有key ,沒有key 會暴力複用,舉例子的話隨便説一個比如移動節點或者增加節點(修改DOM),加key 只會移動減少操作DOM。
  • 只有是同一個虛擬節點才會進行精細化比較,否則就是暴力刪除舊的,插入新的。
  • 只進行同層比較,不會進行跨層比較。

diff算法的優化策略:四種命中查找,四個指針

  1. 舊前與新前(先比開頭,後插入和刪除節點的這種情況)
  2. 舊後與新後(比結尾,前插入或刪除的情況)
  3. 舊前與新後(頭與尾比,此種發生了,涉及移動節點,那麼新前指向的節點,移動到舊後之後)
  4. 舊後與新前(尾與頭比,此種發生了,涉及移動節點,那麼新前指向的節點,移動到舊前之前)

Class 與 Style 如何動態綁定

Class 可以通過對象語法和數組語法進行動態綁定

對象語法:

```javascript

data: { isActive: true, hasError: false } ```

數組語法:

```javascript

data: { activeClass: 'active', errorClass: 'text-danger' } ```

Style 也可以通過對象語法和數組語法進行動態綁定

對象語法:

```javascript

data: { activeColor: 'red', fontSize: 30 } ```

數組語法:

```javascript

data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' } } ```

Vue template 到 render 的過程

vue的模版編譯過程主要如下:template -> ast -> render函數

vue 在模版編譯版本的碼中會執行 compileToFunctions 將template轉化為render函數:

```javascript // 將模板編譯為render函數const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)

```

CompileToFunctions中的主要邏輯如下∶ (1)調用parse方法將template轉化為ast(抽象語法樹)

```javascript constast = parse(template.trim(), options)

```

  • parse的目標:把tamplate轉換為AST樹,它是一種用 JavaScript對象的形式來描述整個模板。
  • 解析過程:利用正則表達式順序解析模板,當解析到開始標籤、閉合標籤、文本的時候都會分別執行對應的 回調函數,來達到構造AST樹的目的。

AST元素節點總共三種類型:type為1表示普通元素、2為表達式、3為純文本

(2)對靜態節點做優化

```javascript optimize(ast,options)

```

這個過程主要分析出哪些是靜態節點,給其打一個標記,為後續更新渲染可以直接跳過靜態節點做優化

深度遍歷AST,查看每個子樹的節點元素是否為靜態節點或者靜態節點根。如果為靜態節點,他們生成的DOM永遠不會改變,這對運行時模板更新起到了極大的優化作用。

(3)生成代碼

```javascript const code = generate(ast, options)

```

generate將ast抽象語法樹編譯成 render字符串並將靜態部分放到 staticRenderFns 中,最後通過 new Function(`` render``) 生成render函數。

什麼是 mixin ?

  • Mixin 使我們能夠為 Vue 組件編寫可插拔和可重用的功能。
  • 如果希望在多個組件之間重用一組組件選項,例如生命週期 hook、 方法等,則可以將其編寫為 mixin,並在組件中簡單的引用它。
  • 然後將 mixin 的內容合併到組件中。如果你要在 mixin 中定義生命週期 hook,那麼它在執行時將優化於組件自已的 hook。

Vue2.x 響應式數據原理

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

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

Object.defineProperty基本使用

```javascript function observer(value) { // proxy reflect if (typeof value === 'object' && typeof value !== null) for (let key in value) { defineReactive(value, key, value[key]); } }

function defineReactive(obj, key, value) { observer(value); Object.defineProperty(obj, key, { get() { // 收集對應的key 在哪個方法(組件)中被使用 return value; }, set(newValue) { if (newValue !== value) { observer(newValue); value = newValue; // 讓key對應的方法(組件重新渲染)重新執行 } } }) } let obj1 = { school: { name: 'poetry', age: 20 } }; observer(obj1); console.log(obj1) ```

源碼分析

```javascript 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); } } ```

説一説你對vue響應式理解回答範例

  • 所謂數據響應式就是能夠使數據變化可以被檢測並對這種變化做出響應的機制
  • MVVM框架中要解決的一個核心問題是連接數據層和視圖層,通過數據驅動應用,數據變化,視圖更新,要做到這點的就需要對數據做響應式處理,這樣一旦數據發生變化就可以立即做出更新處理
  • vue為例説明,通過數據響應式加上虛擬DOMpatch算法,開發人員只需要操作數據,關心業務,完全不用接觸繁瑣的DOM操作,從而大大提升開發效率,降低開發難度
  • vue2中的數據響應式會根據數據類型來做不同處理,如果是 對象則採用Object.defineProperty()的方式定義數據攔截,當數據被訪問或發生變化時,我們感知並作出響應;如果是數組則通過覆蓋數組對象原型的7個變更方法 ,使這些方法可以額外的做更新通知,從而作出響應。這種機制很好的解決了數據響應化的問題,但在實際使用中也存在一些缺點:比如初始化時的遞歸遍歷會造成性能損失;新增或刪除屬性時需要用户使用Vue.set/delete這樣特殊的api才能生效;對於es6中新產生的MapSet這些數據結構不支持等問題
  • 為了解決這些問題,vue3重新編寫了這一部分的實現:利用ES6Proxy代理要響應化的數據,它有很多好處,編程體驗是一致的,不需要使用特殊api,初始化性能和內存消耗都得到了大幅改善;另外由於響應化的實現代碼抽取為獨立的reactivity包,使得我們可以更靈活的使用它,第三方的擴展開發起來更加靈活了

vue和react的區別

=> 相同點:

1. 數據驅動頁面,提供響應式的試圖組件 2. 都有virtual DOM,組件化的開發,通過props參數進行父子之間組件傳遞數據,都實現了webComponents規範 3. 數據流動單向,都支持服務器的渲染SSR 4. 都有支持native的方法,react有React native, vue有wexx

=> 不同點:

1.數據綁定:Vue實現了雙向的數據綁定,react數據流動是單向的 2.數據渲染:大規模的數據渲染,react更快 3.使用場景:React配合Redux架構適合大規模多人協作複雜項目,Vue適合小快的項目 4.開發風格:react推薦做法jsx + inline style把html和css都寫在js了 vue是採用webpack + vue-loader單文件組件格式,html, js, css同一個文件

Vue-router 路由有哪些模式?

一般有兩種模式: (1)hash 模式:後面的 hash 值的變化,瀏覽器既不會向服務器發出請求,瀏覽器也不會刷新,每次 hash 值的變化會觸發 hashchange 事件。 (2)history 模式:利用了 HTML5 中新增的 pushState() 和 replaceState() 方法。這兩個方法應用於瀏覽器的歷史記錄棧,在當前已有的 back、forward、go 的基礎之上,它們提供了對歷史記錄進行修改的功能。只是當它們執行修改時,雖然改變了當前的 URL,但瀏覽器不會立即向後端發送請求。

Vue.extend 作用和原理

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

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

  • extend是構造一個組件的語法器。然後這個組件你可以作用到Vue.component這個全局註冊方法裏還可以在任意vue模板裏使用組件。 也可以作用到vue實例或者某個組件中的components屬性中並在內部使用apple組件。
  • Vue.component你可以創建 ,也可以取組件。

相關代碼如下

javascript 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; }; }

v-once的使用場景有哪些

分析

v-onceVue中內置指令,很有用的API,在優化方面經常會用到

體驗

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

```html

This will never change: {{msg}}

comment

{{msg}}

  • {{i}}

```

回答範例

  • v-oncevue的內置指令,作用是僅渲染指定組件或元素一次,並跳過未來對其更新
  • 如果我們有一些元素或者組件在初始化渲染之後不再需要變化,這種情況下適合使用v-once,這樣哪怕這些數據變化,vue也會跳過更新,是一種代碼優化手段
  • 我們只需要作用的組件或元素上加上v-once即可
  • vue3.2之後,又增加了v-memo指令,可以有條件緩存部分模板並控制它們的更新,可以説控制力更強了
  • 編譯器發現元素上面有v-once時,會將首次計算結果存入緩存對象,組件再次渲染時就會從緩存獲取,從而避免再次計算

原理

下面例子使用了v-once

```html

​ ```

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

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

Vue 組件間通信有哪幾種方式?

Vue 組件間通信是面試常考的知識點之一,這題有點類似於開放題,你回答出越多方法當然越加分,表明你對 Vue 掌握的越熟練。Vue 組件間通信只要指以下 3 類通信:父子組件通信、隔代組件通信、兄弟組件通信,下面我們分別介紹每種通信方式且會説明此種方法可適用於哪類組件間通信。

(1)props / $emit 適用 父子組件通信 這種方法是 Vue 組件的基礎,相信大部分同學耳聞能詳,所以此處就不舉例展開介紹。

(2)ref 與 $parent / $children適用 父子組件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子組件上,引用就指向組件實例
  • $parent / $children:訪問父 / 子實例

(3)EventBus ($emit / $on) 適用於 父子、隔代、兄弟組件通信 這種方法通過一個空的 Vue 實例作為中央事件總線(事件中心),用它來觸發事件和監聽事件,從而實現任何組件間的通信,包括父子、隔代、兄弟組件。

(4)$attrs/$listeners適用於 隔代組件通信

  • $attrs:包含了父作用域中不被 prop 所識別 (且獲取) 的特性綁定 ( class 和 style 除外 )。當一個組件沒有聲明任何 prop 時,這裏會包含所有父作用域的綁定 ( class 和 style 除外 ),並且可以通過v-bind="$attrs" 傳入內部組件。通常配合 inheritAttrs 選項一起使用。
  • $listeners:包含了父作用域中的 (不含 .native 修飾器的) v-on事件監聽器。它可以通過 v-on="$listeners"傳入內部組件

(5)provide / inject適用於 隔代組件通信 祖先組件中通過 provider 來提供變量,然後在子孫組件中通過 inject來注入變量。 provide / inject API主要解決了跨級組件間的通信問題,不過它的使用場景,主要是子組件獲取上級組件的狀態,跨級組件間建立了一種主動提供與依賴注入的關係。 (6)Vuex適用於 父子、隔代、兄弟組件通信 Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。每一個 Vuex 應用的核心就是 store(倉庫)。“store” 基本上就是一個容器,它包含着你的應用中大部分的狀態 ( state )。

  • Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的組件也會相應地得到高效更新。
  • 改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化。