滴滴前端高頻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。這樣使得我們可以方便地跟蹤每一個狀態的變化。