滴滴前端高頻vue面試題(邊面邊更)
Vue-router 路由模式有幾種
vue-router
有 3
種路由模式:hash
、history
、abstract
,對應的源碼如下所示
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
所識別 (且獲取) 的特性綁定 (class
和style
除外 )。當一個組件沒有聲明任何prop
時,這裏會包含所有父作用域的綁定 (class
和style
除外 ),並且可以通過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:
// 子組件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
進行傳遞 - 祖先與後代組件數據傳遞可選擇
attrs
與listeners
或者Provide
與Inject
- 複雜關係的組件數據傳遞可以通過
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算法
vue
的diff
算法是平級比較,不考慮跨級比較的情況。內部採用深度遞歸的方式+雙指針(頭尾都加指針)
的方式進行比較。
簡單來説,Diff算法有以下過程
- 同級比較,再比較子節點(根據
key
和tag
標籤名判斷) - 先判斷一方有子節點和一方沒有子節點的情況(如果新的
children
沒有子節點,將舊的子節點移除) - 比較都有子節點的情況(核心
diff
) - 遞歸比較子節點
- 正常
Diff
兩個樹的時間複雜度是O(n^3)
,但實際情況下我們很少會進行跨層級的移動DOM
,所以Vue
將Diff
進行了優化,從O(n^3) -> O(n)
,只有當新舊children
都為多個子節點時才需要用核心的Diff
算法進行同層級比較。 Vue2
的核心Diff
算法採用了雙端比較
的算法,同時從新舊children
的兩端開始進行比較,藉助key
值找到可複用的節點,再進行相關操作。相比React
的Diff
算法,同樣情況下可以減少移動節點次數,減少不必要的性能損耗,更加的優雅- 在創建
VNode
時就確定其類型,以及在mount/patch
的過程中採用位運算來判斷一個VNode
的類型,在這個基礎之上再配合核心的Diff
算法,使得性能上較Vue2.x
有了提升
vue3中採用最長遞增子序列來實現
diff
優化
回答範例
思路
diff
算法是幹什麼的- 它的必要性
- 它何時執行
- 具體執行方式
- 拔高:説一下
vue3
中的優化
回答範例
Vue
中的diff
算法稱為patching
算法,它由Snabbdo
m修改而來,虛擬DOM
要想轉化為真實DOM
就需要通過patch
方法轉換- 最初
Vue1.x
視圖中每個依賴均有更新函數對應,可以做到精準更新,因此並不需要虛擬DOM
和patching
算法支持,但是這樣粒度過細導致Vue1.x
無法承載較大應用;Vue 2.x
中為了降低Watcher
粒度,每個組件只有一個Watcher
與之對應,此時就需要引入patching
算法才能精確找到發生變化的地方並高效更新 vue
中diff
執行的時刻是組件內響應式數據變更觸發實例執行其更新函數時,更新函數會再次執行render
函數獲得最新的虛擬DOM
,然後執行patc
h函數,並傳入新舊兩次虛擬DOM,通過比對兩者找到變化的地方,最後將其轉化為對應的DOM
操作patch
過程是一個遞歸過程,遵循深度優先、同層比較的策略;以vue3
的patch
為例- 首先判斷兩個節點是否為相同同類節點,不同則刪除重新創建
- 如果雙方都是文本則更新文本內容
- 如果雙方都是元素節點則遞歸更新子元素,同時更新元素屬性
- 更新子節點時又分了幾種情況
- 新的子節點是文本,老的子節點是數組則清空,並設置文本;
- 新的子節點是文本,老的子節點是文本則直接更新文本;
- 新的子節點是數組,老的子節點是文本則清空文本,並創建新子節點數組中的子元素;
- 新的子節點是數組,老的子節點也是數組,那麼比較兩組子節點,更新細節blabla
vue3
中引入的更新策略:靜態節點標記等
vdom中diff算法的簡易實現
以下代碼只是幫助大家理解diff
算法的原理和流程
- 將
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; } ```
- 用簡易
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); } }) } ```
參考 前端進階面試題詳細解答
為什麼要使用異步組件
- 節省打包出的結果,異步組件分開打包,採用
jsonp
的方式進行加載,有效解決文件過大的問題。 - 核心就是包組件定義變成一個函數,依賴
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項目,説説有哪些步驟、哪些重要插件、目錄結構你會怎麼組織
綜合實踐類題目,考查實戰能力。沒有什麼絕對的正確答案,把平時工作的重點有條理的描述一下即可
思路
- 構建項目,創建項目基本結構
- 引入必要的插件:
- 代碼規範:
prettier
,eslint
- 提交規範:
husky
,lint-staged` - 其他常用:
svg-loader
,vueuse
,nprogress
- 常見目錄結構
回答範例
- 從
0
創建一個項目我大致會做以下事情:項目構建、引入必要插件、代碼規範、提交規範、常用庫和組件 - 目前
vue3
項目我會用vite
或者create-vue
創建項目 - 接下來引入必要插件:路由插件
vue-router
、狀態管理vuex/pinia
、ui
庫我比較喜歡element-plu
s和antd-vue
、http
工具我會選axios
- 其他比較常用的庫有
vueuse
,nprogress
,圖標可以使用vite-svg-loader
- 下面是代碼規範:結合
prettier
和eslint
即可 - 最後是提交規範,可以使用
husky
,lint-staged
,commitlint
- 目錄結構我有如下習慣:
.vscode
:用來放項目中的vscode
配置 plugins
:用來放vite
插件的plugin
配置public
:用來放一些諸如 頁頭icon
之類的公共文件,會被打包到dist
根目錄下src
:用來放項目代碼文件api
:用來放http
的一些接口配置assets
:用來放一些CSS
之類的靜態資源components
:用來放項目通用組件layout
:用來放項目的佈局router
:用來放項目的路由配置store
:用來放狀態管理Pinia
的配置utils
:用來放項目中的工具方法類views
:用來放項目的頁面文件
keep-alive 使用場景和原理
keep-alive
是Vue
內置的一個組件, 可以實現組件緩存 ,當組件切換時不會對當前組件進行卸載。 一般結合路由和動態組件一起使用 ,用於緩存組件- 提供
include
和exclude
屬性, 允許組件有條件的進行緩存 。兩者都支持字符串或正則表達式,include
表示只有名稱匹配的組件會被緩存,exclude
表示任何名稱匹配的組件都不會被緩存 ,其中exclude
的優先級比include
高 - 對應兩個鈎子函數
activated
和deactivated
,當組件被激活時,觸發鈎子函數activated
,當組件被移除時,觸發鈎子函數deactivated
keep-alive
的中還運用了LRU
(最近最少使用) 算法,選擇最近最久未使用的組件予以淘汰
<keep-alive></keep-alive>
包裹動態組件時,會緩存不活動的組件實例,主要用於保留組件狀態或避免重新渲染- 比如有一個列表和一個詳情,那麼用户就會經常執行打開詳情=>返回列表=>打開詳情…這樣的話列表和詳情都是一個頻率很高的頁面,那麼就可以對列表組件使用
<keep-alive></keep-alive>
進行緩存,這樣用户每次返回列表的時候,都能從緩存中快速渲染,而不是重新渲染
關於keep-alive的基本用法
html
<keep-alive>
<component :is="view"></component>
</keep-alive>
使用includes
和exclude
:
```html
匹配首先檢查組件自身的 name
選項,如果 name
選項不可用,則匹配它的局部註冊名稱 (父組件 components
選項的鍵值),匿名組件不能被匹配
設置了 keep-alive
緩存的組件,會多出兩個生命週期鈎子(activated
與deactivated
):
- 首次進入組件時:
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-alive
是vue
中內置的一個組件
源碼位置: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
鈎子函數中觀測 include
和 exclude
的變化,如下:
javascript
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
如果include
或exclude
發生了變化,即表示定義需要緩存的組件的規則或者不需要緩存的組件的規則發生了變化,那麼就執行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: "
const router = new VueRouter({ routes: [ // 動態路徑參數 以冒號開頭 { path: "/user/:id", component: User }, ], }); ```
問題: vue-router
組件複用導致路由參數失效怎麼辦?
解決方法:
- 通過
watch
監聽路由參數再發請求
javascript
watch: { //通過watch來監聽路由變化
"$route": function(){
this.getData(this.$route.params.xxx);
}
}
- 用
:key
來阻止“複用”
html
<router-view :key="$route.fullPath" />
回答範例
- 很多時候,我們需要將給定匹配模式的路由映射到同一個組件,這種情況就需要定義動態路由
- 例如,我們可能有一個
User
組件,它應該對所有用户進行渲染,但用户ID
不同。在Vue Router
中,我們可以在路徑中使用一個動態字段來實現,例如:{ path: '/users/:id', component: User }
,其中:id
就是路徑參數 - 路徑參數 用冒號
:
表示。當一個路由被匹配時,它的params
的值將在每個組件中以this.$route.params
的形式暴露出來。 - 參數還可以有多個,例如/
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算法的優化策略:四種命中查找,四個指針
- 舊前與新前(先比開頭,後插入和刪除節點的這種情況)
- 舊後與新後(比結尾,前插入或刪除的情況)
- 舊前與新後(頭與尾比,此種發生了,涉及移動節點,那麼新前指向的節點,移動到舊後之後)
- 舊後與新前(尾與頭比,此種發生了,涉及移動節點,那麼新前指向的節點,移動到舊前之前)
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
來劫持各個屬性的 setter
、getter
(只會劫持已經存在的屬性),數組則是通過重寫數組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
為例説明,通過數據響應式加上虛擬DOM
和patch
算法,開發人員只需要操作數據,關心業務,完全不用接觸繁瑣的DOM操作,從而大大提升開發效率,降低開發難度 vue2
中的數據響應式會根據數據類型來做不同處理,如果是 對象則採用Object.defineProperty()
的方式定義數據攔截,當數據被訪問或發生變化時,我們感知並作出響應;如果是數組則通過覆蓋數組對象原型的7個變更方法 ,使這些方法可以額外的做更新通知,從而作出響應。這種機制很好的解決了數據響應化的問題,但在實際使用中也存在一些缺點:比如初始化時的遞歸遍歷會造成性能損失;新增或刪除屬性時需要用户使用Vue.set/delete
這樣特殊的api
才能生效;對於es6
中新產生的Map
、Set
這些數據結構不支持等問題- 為了解決這些問題,
vue3
重新編寫了這一部分的實現:利用ES6
的Proxy
代理要響應化的數據,它有很多好處,編程體驗是一致的,不需要使用特殊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-once
是Vue
中內置指令,很有用的API
,在優化方面經常會用到
體驗
僅渲染元素和組件一次,並且跳過未來更新
```html
This will never change: {{msg}}
comment
{{msg}}
- {{i}}
```
回答範例
v-once
是vue
的內置指令,作用是僅渲染指定組件或元素一次,並跳過未來對其更新- 如果我們有一些元素或者組件在初始化渲染之後不再需要變化,這種情況下適合使用
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。這樣使得我們可以方便地跟蹤每一個狀態的變化。