[TOC]
## 一、Vue 基礎
### 1. Vue的基本原理
當一個Vue實例創建時,Vue會遍歷data中的屬性,用 Object.defineProperty(vue3.0使用proxy )將它們轉為 getter/setter,并且在內部追蹤相關依賴,在屬性被訪問和修改時通知變化。 每個組件實例都有相應的 watcher 程序實例,它會在組件渲染的過程中把屬性記錄為依賴,之后當依賴項的setter被調用時,會通知watcher重新計算,從而致使它關聯的組件得以更新。

### 2. 雙向數據綁定的原理
Vue.js 是采用**數據劫持**結合**發布者-訂閱者模式**的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數據變動時發布消息給訂閱者,觸發相應的監聽回調。主要分為以下幾個步驟:
1. 需要observe的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上setter和getter這樣的話,給這個對象的某個值賦值,就會觸發setter,那么就能監聽到了數據變化
2. compile解析模板指令,將模板中的變量替換成數據,然后初始化渲染頁面視圖,并將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖
3. Watcher訂閱者是Observer和Compile之間通信的橋梁,主要做的事情是: ①在自身實例化時往屬性訂閱器(dep)里面添加自己 ②自身必須有一個update()方法 ③待屬性變動dep.notice()通知時,能調用自身的update()方法,并觸發Compile中綁定的回調,則功成身退。
4. MVVM作為數據綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監聽自己的model數據變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變更的雙向綁定效果。

### 3. 使用 Object.defineProperty() 來進行數據劫持有什么缺點?
在對一些屬性進行操作時,使用這種方法無法攔截,比如通過下標方式修改數組數據或者給對象新增屬性,這都不能觸發組件的重新渲染,因為?Object.defineProperty?不能攔截到這些操作。更精確的來說,對于數組而言,大部分操作都是攔截不到的,只是 Vue 內部通過重寫函數的方式解決了這個問題。
在 Vue3.0 中已經不使用這種方式了,而是通過使用 Proxy 對對象進行代理,從而實現數據劫持。使用Proxy 的好處是它可以完美的監聽到任何方式的數據改變,唯一的缺點是兼容性的問題,因為 Proxy 是 ES6 的語法。
### 4. MVVM、MVC、MVP的區別
MVC、MVP 和 MVVM 是三種常見的軟件架構設計模式,主要通過分離關注點的方式來組織代碼結構,優化開發效率。
在開發單頁面應用時,往往一個路由頁面對應了一個腳本文件,所有的頁面邏輯都在一個腳本文件里。頁面的渲染、數據的獲取,對用戶事件的響應所有的應用邏輯都混合在一起,這樣在開發簡單項目時,可能看不出什么問題,如果項目變得復雜,那么整個文件就會變得冗長、混亂,這樣對項目開發和后期的項目維護是非常不利的。
**(1)MVC**
MVC 通過分離 Model、View 和 Controller 的方式來組織代碼結構。其中 View 負責頁面的顯示邏輯,Model 負責存儲頁面的業務數據,以及對相應數據的操作。并且 View 和 Model 應用了觀察者模式,當 Model 層發生改變的時候它會通知有關 View 層更新頁面。Controller 層是 View 層和 Model 層的紐帶,它主要負責用戶與應用的響應操作,當用戶與頁面產生交互的時候,Controller 中的事件觸發器就開始工作了,通過調用 Model 層,來完成對 Model 的修改,然后 Model 層再去通知 View 層更新。


**(2)MVVM**
MVVM 分為 Model、View、ViewModel:
* Model代表數據模型,數據和業務邏輯都在Model層中定義;
* View代表UI視圖,負責數據的展示;
* ViewModel負責監聽Model中數據的改變并且控制視圖的更新,處理用戶交互操作;
Model和View并無直接關聯,而是通過ViewModel來進行聯系的,Model和ViewModel之間有著雙向數據綁定的聯系。因此當Model中的數據改變時會觸發View層的刷新,View中由于用戶交互操作而改變的數據也會在Model中同步。
這種模式實現了 Model和View的數據自動同步,因此開發者只需要專注于數據的維護操作即可,而不需要自己操作DOM。

### 5. Computed 和 Watch 的區別
**對于Computed:**
* 它支持緩存,只有依賴的數據發生了變化,才會重新計算
* 不支持異步,當Computed中有異步操作時,無法監聽數據的變化
* computed的值會默認走緩存,計算屬性是基于它們的響應式依賴進行緩存的,也就是基于data聲明過,或者父組件傳遞過來的props中的數據進行計算的。
* 如果一個屬性是由其他屬性計算而來的,這個屬性依賴其他的屬性,一般會使用computed
* 如果computed屬性的屬性值是函數,那么默認使用get方法,函數的返回值就是屬性的屬性值;在computed中,屬性有一個get方法和一個set方法,當數據發生變化時,會調用set方法。
**對于Watch:**
* 它不支持緩存,數據變化時,它就會觸發相應的操作
* 支持異步監聽
* 監聽的函數接收兩個參數,第一個參數是最新的值,第二個是變化之前的值
* 當一個屬性發生變化時,就需要執行相應的操作
* 監聽數據必須是data中聲明的或者父組件傳遞過來的props中的數據,當發生變化時,會觸發其他操作,函數有兩個的參數:
* immediate:組件加載立即觸發回調函數
* deep:深度監聽,發現數據內部的變化,在復雜數據類型中使用,例如數組中的對象發生變化。需要注意的是,deep無法監聽到數組和對象內部的變化。
當想要執行異步或者昂貴的操作以響應不斷的變化時,就需要使用watch。
**總結:**
* computed 計算屬性 : 依賴其它屬性值,并且 computed 的值有緩存,只有它依賴的屬性值發生改變,下一次獲取 computed 的值時才會重新計算 computed 的值。
* watch 偵聽器 : 更多的是**觀察**的作用,**無緩存性**,類似于某些數據的監聽回調,每當監聽的數據變化時都會執行回調進行后續操作。
### 6. Computed 和 Methods 的區別
可以將同一函數定義為一個 method 或者一個計算屬性。對于最終的結果,兩種方式是相同的
**不同點:**
* computed: 計算屬性是基于它們的依賴進行緩存的,只有在它的相關依賴發生改變時才會重新求值;
* method 調用總會執行該函數。
### 7. slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的內容分發機制,組件內部的模板引擎使用slot元素作為承載分發內容的出口。插槽slot是子組件的一個模板標簽元素,而這一個標簽元素是否顯示,以及怎么顯示是由父組件決定的。slot又分三類,默認插槽,具名插槽和作用域插槽。
* 默認插槽:又名匿名插槽,當slot沒有指定name屬性值的時候一個默認顯示插槽,一個組件內只有有一個匿名插槽。
* 具名插槽:帶有具體名字的插槽,也就是帶有name屬性的slot,一個組件可以出現多個具名插槽。
* 作用域插槽:默認插槽、具名插槽的一個變體,可以是匿名插槽,也可以是具名插槽,該插槽的不同點是在子組件渲染作用域插槽時,可以將子組件內部的數據傳遞給父組件,讓父組件根據子組件的傳遞過來的數據決定如何渲染該插槽。
實現原理:當子組件vm實例化時,獲取到父組件傳入的slot標簽的內容,存放在`vm.$slot`中,默認插槽為`vm.$slot.default`,具名插槽為`vm.$slot.xxx`,xxx 為插槽名,當組件執行渲染函數時候,遇到slot標簽,使用`$slot`中的內容進行替換,此時可以為插槽傳遞數據,若存在數據,則可稱該插槽為作用域插槽。
### 8. 常見的事件修飾符及其作用
* `.stop`:等同于 JavaScript 中的 `event.stopPropagation()` ,防止事件冒泡;
* `.prevent` :等同于 JavaScript 中的 `event.preventDefault()` ,防止執行預設的行為(如果事件可取消,則取消該事件,而不停止事件的進一步傳播);
* `.capture` :與事件冒泡的方向相反,事件捕獲由外到內;
* `.self` :只會觸發自己范圍內的事件,不包含子元素;
* `.once` :只會觸發一次。
### 9. v-if、v-show、v-html 的原理
* v-if會調用addIfCondition方法,生成vnode的時候會忽略對應節點,render的時候就不會渲染;
* v-show會生成vnode,render的時候也會渲染成真實節點,只是在render過程中會在節點的屬性中修改show屬性值,也就是常說的display;
* v-html會先移除節點下的所有節點,調用html方法,通過addProp添加innerHTML屬性,歸根結底還是設置innerHTML為v-html的值。
### 10. v-if和v-show的區別
* **手段**:v-if是動態的向DOM樹內添加或者刪除DOM元素;v-show是通過設置DOM元素的display樣式屬性控制顯隱;
* **編譯過程**:v-if切換有一個局部編譯/卸載的過程,切換過程中合適地銷毀和重建內部的事件監聽和子組件;v-show只是簡單的基于css切換;
* **編譯條件**:v-if是惰性的,如果初始條件為假,則什么也不做;只有在條件第一次變為真時才開始局部編譯; v-show是在任何條件下,無論首次條件是否為真,都被編譯,然后被緩存,而且DOM元素保留;
* **性能消耗**:v-if有更高的切換消耗;v-show有更高的初始渲染消耗;
* **使用場景**:v-if適合運營條件不大可能改變;v-show適合頻繁切換。
### 11. v-model 是如何實現的,語法糖實際是什么?
**(1)作用在表單元素上**
動態綁定了 input 的 value 指向了 messgae 變量,并且在觸發 input 事件的時候去動態把 message設置為目標值:
```
<input v-model="sth" />
// 等同于
<input
v-bind:value="message"
v-on:input="message=$event.target.value"
>
//$event 指代當前觸發的事件對象;
//$event.target 指代當前觸發的事件對象的dom;
//$event.target.value 就是當前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;
```
**(2)作用在組件上**
在自定義組件中,v-model 默認會利用名為 value 的 prop和名為 input 的事件
**本質是一個父子組件通信的語法糖,通過prop和$.emit實現。**因此父組件 v-model 語法糖本質上可以修改為:
```
<child :value="message" @input="function(e){message = e}"></child>
```
在組件的實現中,可以通過 v-model屬性來配置子組件接收的prop名稱,以及派發的事件名稱。
例子:
```
// 父組件
<aa-input v-model="aa"></aa-input>
// 等價于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>
// 子組件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>
props:{value:aa,}
methods:{
onmessage(e){
$emit('input',e.target.value)
}
}
```
默認情況下,一個組件上的v-model 會把 value 用作 prop且把 input 用作 event。但是一些輸入類型比如單選框和復選框按鈕可能想使用 value prop 來達到不同的目的。使用 model 選項可以回避這些情況產生的沖突。js 監聽input 輸入框輸入數據改變,用oninput,數據改變以后就會立刻出發這個事件。通過input事件把數據$emit 出去,在父組件接受。父組件設置v-model的值為input $emit過來的值。
### 12. v-model 可以被用在自定義組件上嗎?如果可以,如何使用?
可以。v-model 實際上是一個語法糖,如:
```
<input v-model="searchText">
```
相當于:
```
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
```
### 13. data為什么是一個函數而不是對象
Vue組件可能存在多個實例,如果使用對象形式定義data,則會導致它們共用一個data對象,那么狀態變更將會影響所有組件實例,這是不合理的;采用函數形式定義,在initData時會將其作為工廠函數返回全新data對象,有效規避多實例之間狀態污染問題。而在Vue根實例創建過程中則不存在該限制,也是因為根實例只能有一個,不需要擔心這種情況。
### 14. 對keep-alive的理解,它是如何實現的,具體緩存的是什么?
如果需要在組件切換的時候,保存一些組件的狀態防止多次渲染,就可以使用 keep-alive 組件包裹需要保存的組件。
**(1)****keep-alive**
keep-alive有以下三個屬性:
* include 字符串或正則表達式,只有名稱匹配的組件會被匹配;
* exclude 字符串或正則表達式,任何名稱匹配的組件都不會被緩存;
* max 數字,最多可以緩存多少組件實例。
注意:keep-alive 包裹動態組件時,會緩存不活動的組件實例。
**主要流程**
1. 判斷組件 name ,不在 include 或者在 exclude 中,直接返回 vnode,說明該組件不被緩存。
2. 獲取組件實例 key ,如果有獲取實例的 key,否則重新生成。
3. key生成規則,cid +"∶∶"+ tag ,僅靠cid是不夠的,因為相同的構造函數可以注冊為不同的本地組件。
4. 如果緩存對象內存在,則直接從緩存對象中獲取組件實例給 vnode ,不存在則添加到緩存對象中。 5.最大緩存數量,當緩存組件數量超過 max 值時,清除 keys 數組內第一個組件。
**(2)keep-alive 的實現**
```
const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正則,數組
export default {
name: 'keep-alive',
abstract: true, // 抽象組件,是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現在父組件鏈中。
props: {
include: patternTypes, // 匹配的組件,緩存
exclude: patternTypes, // 不去匹配的組件,不緩存
max: [String, Number], // 緩存組件的最大實例數量, 由于緩存的是組件實例(vnode),數量過多的時候,會占用過多的內存,可以用max指定上限
},
created() {
// 用于初始化緩存虛擬DOM數組和vnode的key
this.cache = Object.create(null)
this.keys = []
},
destroyed() {
// 銷毀緩存cache的組件實例
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// prune 削減精簡[v.]
// 去監控include和exclude的改變,根據最新的include和exclude的內容,來實時削減緩存的組件的內容
this.$watch('include', (val) => {
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', (val) => {
pruneCache(this, (name) => !matches(val, name))
})
},
}
```
**render函數:**
1. 會在 keep-alive 組件內部去寫自己的內容,所以可以去獲取默認 slot 的內容,然后根據這個去獲取組件
2. keep-alive 只對第一個組件有效,所以獲取第一個子組件。
3. 和 keep-alive 搭配使用的一般有:動態組件 和router-view
```
render () {
//
function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
const slot = this.$slots.default // 獲取默認插槽
const vnode: VNode = getFirstComponentChild(slot)// 獲取第一個子組件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 組件參數
if (componentOptions) { // 是否有組件參數
// check pattern
const name: ?string = getComponentName(componentOptions) // 獲取組件名
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
// 如果不匹配當前組件的名字和include以及exclude
// 那么直接返回組件的實例
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
if (cache[key]) {
// LRU緩存策略執行
vnode.componentInstance = cache[key].componentInstance // 組件初次渲染的時候componentInstance為undefined
// make current key freshest
remove(keys, key)
keys.push(key)
// 根據LRU緩存策略執行,將key從原來的位置移除,然后將這個key值放到最后面
} else {
// 在緩存列表里面沒有的話,則加入,同時判斷當前加入之后,是否超過了max所設定的范圍,如果是,則去除
// 使用時間間隔最長的一個
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 將組件的keepAlive屬性設置為true
vnode.data.keepAlive = true // 作用:判斷是否要執行組件的created、mounted生命周期函數
}
return vnode || (slot && slot[0])
}
```
keep-alive 具體是通過 cache 數組緩存所有組件的 vnode 實例。當 cache 內原有組件被使用時會將該組件 key 從 keys 數組中刪除,然后 push 到 keys數組最后,以便清除最不常用組件。
**實現步驟:**
1. 獲取 keep-alive 下第一個子組件的實例對象,通過他去獲取這個組件的組件名
2. 通過當前組件名去匹配原來 include 和 exclude,判斷當前組件是否需要緩存,不需要緩存,直接返回當前組件的實例vNode
3. 需要緩存,判斷他當前是否在緩存數組里面:
* 存在,則將他原來位置上的 key 給移除,同時將這個組件的 key 放到數組最后面(LRU)
* 不存在,將組件 key 放入數組,然后判斷當前 key數組是否超過 max 所設置的范圍,超過,那么削減未使用時間最長的一個組件的 key
4. 最后將這個組件的 keepAlive 設置為 true
**(3)keep-alive 本身的創建過程和 patch 過程**
緩存渲染的時候,會根據 vnode.componentInstance(首次渲染 vnode.componentInstance 為 undefined) 和 keepAlive 屬性判斷不會執行組件的 created、mounted 等鉤子函數,而是對緩存的組件執行 patch 過程∶ 直接把緩存的 DOM 對象直接插入到目標元素中,完成了數據更新的情況下的渲染過程。
**首次渲染**
* 組件的首次渲染∶判斷組件的 abstract 屬性,才往父組件里面掛載 DOM
```
// core/instance/lifecycle
function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) { // 判斷組件的abstract屬性,才往父組件里面掛載DOM
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
```
* 判斷當前 keepAlive 和 componentInstance 是否存在來判斷是否要執行組件 prepatch 還是執行創建 componentlnstance
```
// core/vdom/create-component
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) { // componentInstance在初次是undefined!!!
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode) // prepatch函數執行的是組件更新的過程
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
```
prepatch 操作就不會在執行組件的 mounted 和 created 生命周期函數,而是直接將 DOM 插入
**(4)LRU (least recently used)緩存策略**
LRU 緩存策略∶ 從內存中找出最久未使用的數據并置換新的數據。
LRU(Least rencently used)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是**"如果數據最近被訪問過,那么將來被訪問的幾率也更高"**。 最常見的實現是使用一個鏈表保存緩存數據,詳細算法實現如下∶
* 新數據插入到鏈表頭部
* 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部
* 鏈表滿的時候,將鏈表尾部的數據丟棄。
### 15. $nextTick 原理及作用
Vue 的 nextTick 其本質是對 JavaScript 執行原理 EventLoop 的一種應用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法來模擬對應的微/宏任務的實現,本質是為了利用 JavaScript 的這些異步回調任務隊列來實現 Vue 框架中自己的異步回調隊列。
nextTick 不僅是 Vue 內部的異步隊列的調用方法,同時也允許開發者在實際項目中使用這個方法來滿足實際應用中對 DOM 更新數據時機的后續邏輯處理
nextTick 是典型的將底層 JavaScript 執行原理應用到具體案例中的示例,引入異步更新隊列機制的原因∶
* 如果是同步更新,則多次對一個或多個屬性賦值,會頻繁觸發 UI/DOM 的渲染,可以減少一些無用渲染
* 同時由于 VirtualDOM 的引入,每一次狀態發生變化后,狀態變化的信號會發送給組件,組件內部使用 VirtualDOM 進行計算得出需要更新的具體的 DOM 節點,然后對 DOM 進行更新操作,每次更新狀態后的渲染過程需要更多的計算,而這種無用功也將浪費更多的性能,所以異步渲染變得更加至關重要
Vue采用了數據驅動視圖的思想,但是在一些情況下,仍然需要操作DOM。有時候,可能遇到這樣的情況,DOM1的數據發生了變化,而DOM2需要從DOM1中獲取數據,那這時就會發現DOM2的視圖并沒有更新,這時就需要用到了`nextTick`了。
由于Vue的DOM操作是異步的,所以,在上面的情況中,就要將DOM2獲取數據的操作寫在`$nextTick`中。
```
this.$nextTick(() => {
// 獲取數據的操作...
})
```
所以,在以下情況下,會用到nextTick:
* 在數據變化后執行的某個操作,而這個操作需要使用隨數據變化而變化的DOM結構的時候,這個操作就需要方法在`nextTick()`的回調函數中。
* 在vue生命周期中,如果在created()鉤子進行DOM操作,也一定要放在`nextTick()`的回調函數中。
因為在created()鉤子函數中,頁面的DOM還未渲染,這時候也沒辦法操作DOM,所以,此時如果想要操作DOM,必須將操作的代碼放在`nextTick()`的回調函數中。
### 16. Vue中封裝的數組方法有哪些,其如何實現頁面更新
在Vue中,對響應式處理利用的是Object.defineProperty對數據進行攔截,而這個方法并不能監聽到數組內部變化,數組長度變化,數組的截取變化等,所以需要對這些操作進行hack,讓Vue能監聽到其中的變化。

那Vue是如何實現讓這些數組方法實現元素的實時更新的呢,下面是Vue中對這些方法的封裝:
```
// 緩存數組原型
const arrayProto = Array.prototype;
// 實現 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要進行功能拓展的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
// 緩存原生數組方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 執行并緩存原生數組功能
const result = original.apply(this, args);
// 響應式處理
const ob = this.__ob__;
let inserted;
switch (method) {
// push、unshift會新增索引,所以要手動observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果傳入了第三個參數,也會有索引加入,也要手動observer。
case "splice":
inserted = args.slice(2);
break;
}
//
if (inserted) ob.observeArray(inserted);// 獲取插入的值,并設置響應式監聽
// notify change
ob.dep.notify();// 通知依賴更新
// 返回原生數組方法的執行結果
return result;
});
});
```
簡單來說就是,重寫了數組中的那些原生方法,首先獲取到這個數組的\_\_ob\_\_,也就是它的Observer對象,如果有新的值,就調用observeArray繼續對新的值觀察變化(也就是通過`target__proto__ == arrayMethods`來改變了數組實例的型),然后手動調用notify,通知渲染watcher,執行update。
### 17. Vue 單頁應用與多頁應用的區別
**概念:**
* SPA單頁面應用(SinglePage Web Application),指只有一個主頁面的應用,一開始只需要加載一次js、css等相關資源。所有內容都包含在主頁面,對每一個功能模塊組件化。單頁應用跳轉,就是切換相關組件,僅僅刷新局部資源。
* MPA多頁面應用 (MultiPage Application),指有多個獨立頁面的應用,每個頁面必須重復加載js、css等相關資源。多頁應用跳轉,需要整頁資源刷新。
**區別:**

### 18. 子組件可以直接改變父組件的數據嗎?
子組件不可以直接改變父組件的數據。這樣做主要是為了維護父子組件的單向數據流。每次父級組件發生更新時,子組件中所有的 prop 都將會刷新為最新的值。如果這樣做了,Vue 會在瀏覽器的控制臺中發出警告。
Vue提倡單向數據流,即父級 props 的更新會流向子組件,但是反過來則不行。這是為了防止意外的改變父組件狀態,使得應用的數據流變得難以理解,導致數據流混亂。如果破壞了單向數據流,當應用復雜時,debug 的成本會非常高。
**只能通過** `**$emit**` **派發一個自定義事件,父組件接收到后,由父組件修改。**
### 19. Vue是如何收集依賴的?
在初始化 Vue 的每個組件時,會對組件的 data 進行初始化,就會將由普通對象變成響應式對象,在這個過程中便會進行依賴收集的相關邏輯,如下所示∶
```
function defieneReactive (obj, key, val){
const dep = new Dep();
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
if(Dep.target){
dep.depend();
...
}
return val
}
...
})
}
```
以上只保留了關鍵代碼,主要就是 `const dep = new Dep()`實例化一個 Dep 的實例,然后在 get 函數中通過 `dep.depend()` 進行依賴收集。
**(1)Dep**
Dep是整個依賴收集的核心,其關鍵代碼如下:
```
class Dep {
static target;
subs;
constructor () {
...
this.subs = [];
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.sub, sub)
}
depend () {
if(Dep.target){
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subds.slice();
for(let i = 0;i < subs.length; i++){
subs[i].update()
}
}
}
```
Dep 是一個 class ,其中有一個關 鍵的靜態屬性 static,它指向了一個全局唯一 Watcher,保證了同一時間全局只有一個 watcher 被計算,另一個屬性 subs 則是一個 Watcher 的數組,所以 Dep 實際上就是對 Watcher 的管理,再看看 Watcher 的相關代碼∶
**(2)Watcher**
```
class Watcher {
getter;
...
constructor (vm, expression){
...
this.getter = expression;
this.get();
}
get () {
pushTarget(this);
value = this.getter.call(vm, vm)
...
return value
}
addDep (dep){
...
dep.addSub(this)
}
...
}
function pushTarget (_target) {
Dep.target = _target
}
```
Watcher 是一個 class,它定義了一些方法,其中和依賴收集相關的主要有 get、addDep 等。
**(3)過程**
在實例化 Vue 時,依賴收集的相關過程如下∶
初 始 化 狀 態 initState , 這 中 間 便 會 通 過 defineReactive 將數據變成響應式對象,其中的 getter 部分便是用來依賴收集的。
初始化最終會走 mount 過程,其中會實例化 Watcher ,進入 Watcher 中,便會執行 this.get() 方法,
```
updateComponent = () => {
vm._update(vm._render())
}
new Watcher(vm, updateComponent)
```
get 方法中的 pushTarget 實際上就是把 Dep.target 賦值為當前的 watcher。
this.getter.call(vm,vm),這里的 getter 會執行 vm.\_render() 方法,在這個過程中便會觸發數據對象的 getter。那么每個對象值的 getter 都持有一個 dep,在觸發 getter 的時候會調用 dep.depend() 方法,也就會執行 Dep.target.addDep(this)。剛才 Dep.target 已經被賦值為 watcher,于是便會執行 addDep 方法,然后走到 dep.addSub() 方法,便將當前的 watcher 訂閱到這個數據持有的 dep 的 subs 中,這個目的是為后續數據變化時候能通知到哪些 subs 做準備。所以在 vm.\_render() 過程中,會觸發所有數據的 getter,這樣便已經完成了一個依賴收集的過程。
### 20. 對 React 和 Vue 的理解,它們的異同
**相似之處:**
* 都將注意力集中保持在核心庫,而將其他功能如路由和全局狀態管理交給相關的庫;
* 都有自己的構建工具,能讓你得到一個根據最佳實踐設置的項目模板;
* 都使用了Virtual DOM(虛擬DOM)提高重繪性能;
* 都有props的概念,允許組件間的數據傳遞;
* 都鼓勵組件化應用,將應用分拆成一個個功能明確的模塊,提高復用性。
**不同之處 :**
**1)數據流**
Vue默認支持數據雙向綁定,而React一直提倡單向數據流
**2)虛擬DOM**
Vue2.x開始引入"Virtual DOM",消除了和React在這方面的差異,但是在具體的細節還是有各自的特點。
* Vue宣稱可以更快地計算出Virtual DOM的差異,這是由于它在渲染過程中,會跟蹤每一個組件的依賴關系,不需要重新渲染整個組件樹。
* 對于React而言,每當應用的狀態被改變時,全部子組件都會重新渲染。當然,這可以通過 PureComponent/shouldComponentUpdate這個生命周期方法來進行控制,但Vue將此視為默認的優化。
**3)組件化**
React與Vue最大的不同是模板的編寫。
* Vue鼓勵寫近似常規HTML的模板。寫起來很接近標準 HTML元素,只是多了一些屬性。
* React推薦你所有的模板通用JavaScript的語法擴展——JSX書寫。
具體來講:React中render函數是支持閉包特性的,所以import的組件在render中可以直接調用。但是在Vue中,由于模板中使用的數據都必須掛在 this 上進行一次中轉,所以 import 一個組件完了之后,還需要在 components 中再聲明下。
**4)監聽數據變化的實現原理不同**
* Vue 通過 getter/setter 以及一些函數的劫持,能精確知道數據變化,不需要特別的優化就能達到很好的性能
* React 默認是通過比較引用的方式進行的,如果不優化(PureComponent/shouldComponentUpdate)可能導致大量不必要的vDOM的重新渲染。這是因為 Vue 使用的是可變數據,而React更強調數據的不可變。
**5)高階組件**
react可以通過高階組件(HOC)來擴展,而Vue需要通過mixins來擴展。
高階組件就是高階函數,而React的組件本身就是純粹的函數,所以高階函數對React來說易如反掌。相反Vue.js使用HTML模板創建視圖組件,這時模板無法有效的編譯,因此Vue不能采用HOC來實現。
**6)構建工具**
兩者都有自己的構建工具:
* React ==> Create React APP
* Vue ==> vue-cli
**7)跨平臺**
* React ==> React Native
* Vue ==> Weex
### 21. Vue的優點
* 輕量級框架:只關注視圖層,是一個構建數據的視圖集合,大小只有幾十 `kb` ;
* 簡單易學:國人開發,中文文檔,不存在語言障礙 ,易于理解和學習;
* 雙向數據綁定:保留了 `angular` 的特點,在數據操作方面更為簡單;
* 組件化:保留了 `react` 的優點,實現了 `html` 的封裝和重用,在構建單頁面應用方面有著獨特的優勢;
* 視圖,數據,結構分離:使數據的更改更為簡單,不需要進行邏輯代碼的修改,只需要操作數據就能完成相關操作;
* 虛擬DOM:`dom` 操作是非常耗費性能的,不再使用原生的 `dom` 操作節點,極大解放 `dom` 操作,但具體操作的還是 `dom` 不過是換了另一種方式;
* 運行速度更快:相比較于 `react` 而言,同樣是操作虛擬 `dom`,就性能而言, `vue` 存在很大的優勢。
### 22. assets和static的區別
**相同點:** `assets` 和 `static` 兩個都是存放靜態資源文件。項目中所需要的資源文件圖片,字體圖標,樣式文件等都可以放在這兩個文件下,這是相同點
**不相同點:**`assets` 中存放的靜態資源文件在項目打包時,也就是運行 `npm run build` 時會將 `assets` 中放置的靜態資源文件進行打包上傳,所謂打包簡單點可以理解為壓縮體積,代碼格式化。而壓縮后的靜態資源文件最終也都會放置在 `static` 文件中跟著 `index.html` 一同上傳至服務器。`static` 中放置的靜態資源文件就不會要走打包壓縮格式化等流程,而是直接進入打包好的目錄,直接上傳至服務器。因為避免了壓縮直接進行上傳,在打包時會提高一定的效率,但是 `static` 中的資源文件由于沒有進行壓縮等操作,所以文件的體積也就相對于 `assets` 中打包后的文件提交較大點。在服務器中就會占據更大的空間。
**建議:** 將項目中 `template`需要的樣式文件js文件等都可以放置在 `assets` 中,走打包這一流程。減少體積。而項目中引入的第三方的資源文件如`iconfoont.css` 等文件可以放置在 `static` 中,因為這些引入的第三方文件已經經過處理,不再需要處理,直接上傳。
### 23. vue如何監聽對象或者數組某個屬性的變化
當在項目中直接設置數組的某一項的值,或者直接設置對象的某個屬性值,這個時候,你會發現頁面并沒有更新。這是因為Object.defineProperty()限制,監聽不到變化。
解決方式:
* this.$set(你要改變的數組/對象,你要改變的位置/key,你要改成什么value)
```
this.$set(this.arr, 0, "OBKoro1"); // 改變數組
this.$set(this.obj, "c", "OBKoro1"); // 改變對象
```
* 調用以下幾個數組的方法
```
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
```
vue源碼里緩存了array的原型鏈,然后重寫了這幾個方法,觸發這幾個方法的時候會observer數據,意思是使用這些方法不用再進行額外的操作,視圖自動進行更新。 推薦使用splice方法會比較好自定義,因為splice可以在數組的任何位置進行刪除/添加操作
vm.`$set` 的實現原理是:
* 如果目標是數組,直接使用數組的 splice 方法觸發相應式;
* 如果目標是對象,會先判讀屬性是否存在、對象是否是響應式,最終如果要對屬性進行響應式處理,則是通過調用 defineReactive 方法進行響應式處理( defineReactive 方法就是 Vue 在初始化對象時,給對象屬性采用 Object.defineProperty 動態添加 getter 和 setter 的功能所調用的方法)。
### 24. 什么是 mixin ?
* Mixin 使我們能夠為 Vue 組件編寫可插拔和可重用的功能。
* 如果希望在多個組件之間重用一組組件選項,例如生命周期 hook、 方法等,則可以將其編寫為 mixin,并在組件中簡單的引用它。
* 然后將 mixin 的內容合并到組件中。如果你要在 mixin 中定義生命周期 hook,那么它在執行時將優化于組件自已的 hook。
### 25. Vue模版編譯原理
vue中的模板template無法被瀏覽器解析并渲染,因為這不屬于瀏覽器的標準,不是正確的HTML語法,所有需要將template轉化成一個JavaScript函數,這樣瀏覽器就可以執行這一個函數并渲染出對應的HTML元素,就可以讓視圖跑起來了,這一個轉化的過程,就成為模板編譯。模板編譯又分三個階段,解析parse,優化optimize,生成generate,最終生成可執行函數render。
* **解析階段**:使用大量的正則表達式對template字符串進行解析,將標簽、指令、屬性等轉化為抽象語法樹AST。
* **優化階段**:遍歷AST,找到其中的一些靜態節點并進行標記,方便在頁面重渲染的時候進行diff比較時,直接跳過這一些靜態節點,優化runtime的性能。
* **生成階段**:將最終的AST轉化為render函數字符串。
### 26. 對SSR的理解
SSR也就是服務端渲染,也就是將Vue在客戶端把標簽渲染成HTML的工作放在服務端完成,然后再把html直接返回給客戶端
SSR的優勢:
* 更好的SEO
* 首屏加載速度更快
SSR的缺點:
* 開發條件會受到限制,服務器端渲染只支持beforeCreate和created兩個鉤子;
* 當需要一些外部擴展庫時需要特殊處理,服務端渲染應用程序也需要處于Node.js的運行環境;
* 更多的服務端負載。
### 27. Vue的性能優化有哪些
**(1)編碼階段**
* 盡量減少data中的數據,data中的數據都會增加getter和setter,會收集對應的watcher
* v-if和v-for不能連用
* 如果需要使用v-for給每項元素綁定事件時使用事件代理
* SPA 頁面采用keep-alive緩存組件
* 在更多的情況下,使用v-if替代v-show
* key保證唯一
* 使用路由懶加載、異步組件
* 防抖、節流
* 第三方模塊按需導入
* 長列表滾動到可視區域動態加載
* 圖片懶加載
**(2)SEO優化**
* 預渲染
* 服務端渲染SSR
**(3)打包優化**
* 壓縮代碼
* Tree Shaking/Scope Hoisting
* 使用cdn加載第三方模塊
* 多線程打包happypack
* splitChunks抽離公共文件
* sourceMap優化
**(4)用戶體驗**
* 骨架屏
* PWA
* 還可以使用緩存(客戶端緩存、服務端緩存)優化、服務端開啟gzip壓縮等。
### 28. 對 SPA 單頁面的理解,它的優缺點分別是什么?
SPA( single-page application )僅在 Web 頁面初始化時加載相應的 HTML、JavaScript 和 CSS。一旦頁面加載完成,SPA 不會因為用戶的操作而進行頁面的重新加載或跳轉;取而代之的是利用路由機制實現 HTML 內容的變換,UI 與用戶的交互,避免頁面的重新加載。
**優點:**
* 用戶體驗好、快,內容的改變不需要重新加載整個頁面,避免了不必要的跳轉和重復渲染;
* 基于上面一點,SPA 相對對服務器壓力小;
* 前后端職責分離,架構清晰,前端進行交互邏輯,后端負責數據處理;
**缺點:**
* 初次加載耗時多:為實現單頁 Web 應用功能及顯示效果,需要在加載頁面的時候將 JavaScript、CSS 統一加載,部分頁面按需加載;
* 前進后退路由管理:由于單頁應用在一個頁面中顯示所有的內容,所以不能使用瀏覽器的前進后退功能,所有的頁面切換需要自己建立堆棧管理;
* SEO 難度較大:由于所有的內容都在一個頁面中動態替換顯示,所以在 SEO 上其有著天然的弱勢。
### 29. vue初始化頁面閃動問題
使用vue開發時,在vue初始化之前,由于div是不歸vue管的,所以我們寫的代碼在還沒有解析的情況下會容易出現花屏現象,看到類似于{{message}}的字樣,雖然一般情況下這個時間很短暫,但是還是有必要讓解決這個問題的。
首先:在css里加上以下代碼:
```
[v-cloak] {
display: none;
}
```
如果沒有徹底解決問題,則在根元素加上`style="display: none;" :style="{display: 'block'}"`。
### 30. MVVM**的優缺點?
優點:
* 分離視圖(View)和模型(Model),降低代碼耦合,提?視圖或者邏輯的重?性: ?如視圖(View)可以獨?于Model變化和修改,?個ViewModel可以綁定不同的"View"上,當View變化的時候Model不可以不變,當Model變化的時候View也可以不變。你可以把?些視圖邏輯放在?個ViewModel??,讓很多view重?這段視圖邏輯
* 提?可測試性: ViewModel的存在可以幫助開發者更好地編寫測試代碼
* ?動更新dom: 利?雙向綁定,數據更新后視圖?動更新,讓開發者從繁瑣的?動dom中解放
缺點:
* Bug很難被調試: 因為使?雙向綁定的模式,當你看到界?異常了,有可能是你View的代碼有Bug,也可能是Model的代碼有問題。數據綁定使得?個位置的Bug被快速傳遞到別的位置,要定位原始出問題的地?就變得不那么容易了。另外,數據綁定的聲明是指令式地寫在View的模版當中的,這些內容是沒辦法去打斷點debug的
* ?個?的模塊中model也會很?,雖然使??便了也很容易保證了數據的?致性,當時?期持有,不釋放內存就造成了花費更多的內存
* 對于?型的圖形應?程序,視圖狀態較多,ViewModel的構建和維護的成本都會?較?。
### 32. **v-if****和****v-for哪個優先級更高?如果同時出現,應如何優化?**
v-for優先于v-if被解析,如果同時出現,每次渲染都會**先執行循環再判斷條件**,無論如何循環都不可避免,浪費了性能。
要避免出現這種情況,則在外層嵌套template,在這一層進行v-if判斷,然后在內部進行v-for循環。如果條件出現在循環內部,可通過計算屬性提前過濾掉那些不需要顯示的項。
### 32. 對Vue組件化的理解
1. 組件是獨立和可復用的代碼組織單元。組件系統是Vue核心特性之一,它使開發者使用小型、獨立和通常可復用的組件構建大型應用;
2. 組件化開發能大幅提高應用開發效率、測試性、復用性等;
3. 組件使用按分類有:頁面組件、業務組件、通用組件;
4. vue的組件是基于配置的,我們通常編寫的組件是組件配置而非組件,框架后續會生成其構造函數,它們基于VueComponent,擴展于Vue;
5. vue中常見組件化技術有:屬性prop,自定義事件,插槽等,它們主要用于組件通信、擴展等;6.合理的劃分組件,有助于提升應用性能;
6. 組件應該是高內聚、低耦合的;
7. 遵循單向數據流的原則。
### 33. 對vue設計原則的理解
1. **漸進式JavaScript框架**:與其它大型框架不同的是,Vue被設計為可以自底向上逐層應用。Vue的核心庫只關注視圖層,不僅易于上手,還便于與第三方庫或既有項目整合。另一方面,當與現代化的工具鏈以及各種支持類庫結合使用時,Vue也完全能夠為復雜的單頁應用提供驅動。
2. **易用性**:vue提供數據響應式、聲明式模板語法和基于配置的組件系統等核心特性。這些使我們只需要關注應用的核心業務即可,只要會寫js、html和css就能輕松編寫vue應用。
3. **靈活性**:漸進式框架的最大優點就是靈活性,如果應用足夠小,我們可能僅需要vue核心特性即可完成功能;隨著應用規模不斷擴大,我們才可能逐漸引入路由、狀態管理、vue-cli等庫和工具,不管是應用體積還是學習難度都是一個逐漸增加的平和曲線。
4. **高效性:**超快的虛擬DOM和di?算法使我們的應用擁有最佳的性能表現。追求高效的過程還在繼續,vue3中引入Proxy對數據響應式改進以及編譯器中對于靜態內容編譯的改進都會讓vue更加高效。
### 34. 常見的Vue性能優化方法
1. 路由懶加載
2. keep-alive緩存頁面
3. 使用v-show復用DOM
4. v-for?遍歷避免同時使用 v-if
5. 長列表性能優化
```
// 如果列表是純粹的數據展示,不會有任何改變,就不需要做響應化
export default {
data: () => ({
users: []
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
};
// 如果是大數據長列表,可采用虛擬滾動,只渲染少部分區域的內容
<recycle-scroller class="items" :items="items" :item-size="24">
<template v-slot="{ item }">
<FetchItemView :item="item" @vote="voteItem(item)"/>
</template>
</recycle-scroller>
```
6. 事件的銷毀
* Vue 組件銷毀時,會自動解綁它的全部指令及事件監聽器,但是僅限于組件本身的事件。
7. 圖片懶加載
* 對于圖片過多的頁面,為了加速頁面加載速度,所以很多時候我們需要將頁面內未出現在可視區域內的圖片先不做加載, 等到滾動到可視區域后再去加載。
8. 第三方插件按需引入
* 像element-ui這樣的第三方組件庫可以按需引入避免體積太大。
9. 無狀態的組件標記為函數式組件 functional
10. 子組件分隔
11. 變量本地化
12. SSR
## 二、組件通信
### 組件通信的方式如下:
1. props ?/ ? $emit
2. `eventBus`事件總線適用于**父子組件**、**非父子組件**等之間的通信
3. 依賴注入(provide/ inject),這種方式就是Vue中的**依賴注入**,該方法用于**父子組件之間的通信**。當然這里所說的父子不一定是真正的父子,也可以是祖孫組件,在層數很深的情況下,可以使用這種方法來進行傳值。就不用一層一層的傳遞了。
4. ref / $refs,這種方式也是實現**父子組件**之間的通信。
5. $parent / $children
6. $attrs / $listeners
### 通信總結
**(1)父子組件間通信**
* 子組件通過 props 屬性來接受父組件的數據,然后父組件在子組件上注冊監聽事件,子組件通過 emit 觸發事件來向父組件發送數據。
* 通過 ref 屬性給子組件設置一個名字。父組件通過 $refs 組件名來獲得子組件,子組件通過 $parent 獲得父組件,這樣也可以實現通信。
* 使用 provide/inject,在父組件中通過 provide提供變量,在子組件中通過 inject 來將變量注入到組件中。不論子組件有多深,只要調用了 inject 那么就可以注入 provide中的數據。
**(2)兄弟組件間通信**
* 使用 eventBus 的方法,它的本質是通過創建一個空的 Vue 實例來作為消息傳遞的對象,通信的組件引入這個實例,通信的組件通過在這個實例上監聽和觸發事件,來實現消息的傳遞。
* 通過 $parent/$refs 來獲取到兄弟組件,也可以進行通信。
**(3)任意組件之間**
* 使用 eventBus ,其實就是創建一個事件中心,相當于中轉站,可以用它來傳遞事件和接收事件。
如果業務邏輯復雜,很多組件之間需要同時處理一些公共的數據,這個時候采用上面這一些方法可能不利于項目的維護。這個時候可以使用 vuex ,vuex 的思想就是將這一些公共的數據抽離出來,將它作為一個全局的變量來管理,然后其他組件就可以對這個公共數據進行讀寫操作,這樣達到了解耦的目的。
## 三、路由的hash和history模式的區別
Vue-Router有兩種模式:**hash模式**和**history模式**。默認的路由模式是hash模式。
### 1. hash模式
**簡介:** hash模式是開發中默認的模式,它的URL帶著一個#,例如:[http://www.abc.com/#/vue](http://www.abc.com/#/vue),它的hash值就是`#/vue`。
**特點**:hash值會出現在URL里面,但是不會出現在HTTP請求中,對后端完全沒有影響。所以改變hash值,不會重新加載頁面。這種模式的瀏覽器支持度很好,低版本的IE瀏覽器也支持這種模式。hash路由被稱為是前端路由,已經成為SPA(單頁面應用)的標配。
**原理:** hash模式的主要原理就是**onhashchange()事件**:
```
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
```
使用onhashchange()事件的好處就是,在頁面的hash值發生變化時,無需向后端發起請求,window就可以監聽事件的改變,并按規則加載相應的代碼。除此之外,hash值變化對應的URL都會被瀏覽器記錄下來,這樣瀏覽器就能實現頁面的前進和后退。雖然是沒有請求后端服務器,但是頁面的hash值和對應的URL關聯起來了。
### 2. history模式
**簡介:** history模式的URL中沒有#,它使用的是傳統的路由分發模式,即用戶在輸入一個URL時,服務器會接收這個請求,并解析這個URL,然后做出相應的邏輯處理。
**特點:** 當使用history模式時,URL就像這樣:[http://abc.com/user/id](http://abc.com/user/id)。相比hash模式更加好看。但是,history模式需要后臺配置支持。如果后臺沒有正確配置,訪問時會返回404。
**API:** history api可以分為兩大部分,切換歷史狀態和修改歷史狀態:
* **修改歷史狀態**:包括了 HTML5 History Interface 中新增的 `pushState()` 和 `replaceState()` 方法,這兩個方法應用于瀏覽器的歷史記錄棧,提供了對歷史記錄進行修改的功能。只是當他們進行修改時,雖然修改了url,但瀏覽器不會立即向后端發送請求。如果要做到改變url但又不刷新頁面的效果,就需要前端用上這兩個API。
* **切換歷史狀態:** 包括`forward()`、`back()`、`go()`三個方法,對應瀏覽器的前進,后退,跳轉操作。
雖然history模式丟棄了丑陋的#。但是,它也有自己的缺點,就是在刷新頁面的時候,如果沒有相應的路由或資源,就會刷出404來。
如果想要切換到history模式,就要進行以下配置(后端也要進行配置):
```
const router = new VueRouter({
mode: 'history',
routes: [...]
})
```
### 3. 兩種模式對比
調用 history.pushState() 相比于直接修改 hash,存在以下優勢:
* pushState() 設置的新 URL 可以是與當前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能設置與當前 URL 同文檔的 URL;
* pushState() 設置的新 URL 可以與當前 URL 一模一樣,這樣也會把記錄添加到棧中;而 hash 設置的新值必須與原來不一樣才會觸發動作將記錄添加到棧中;
* pushState() 通過 stateObject 參數可以添加任意類型的數據到記錄中;而 hash 只可添加短字符串;
* pushState() 可額外設置 title 屬性供后續使用。
* hash模式下,僅hash符號之前的url會被包含在請求中,后端如果沒有做到對路由的全覆蓋,也不會返回404錯誤;history模式下,前端的url必須和實際向后端發起請求的url一致,如果沒有對用的路由處理,將返回404錯誤。
hash模式和history模式都有各自的優勢和缺陷,還是要根據實際情況選擇性的使用。
### 4. 如何獲取頁面的hash變化
**(1)監聽$route的變化**
```
// 監聽,當路由發生變化的時候執行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度觀察監聽
deep: true
}
},
```
**(2)window.location.hash讀取#值**
window.location.hash 的值可讀可寫,讀取來判斷狀態是否改變,寫入時可以在不重載網頁的前提下,添加一條歷史訪問記錄。
### 4. $route 和$router 的區別
* $route 是“路由信息對象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息參數
* $router 是“路由實例”對象包括了路由的跳轉方法,鉤子函數等。
### 5. 如何定義動態路由?如何獲取傳過來的動態參數?
**(1)param方式**
* 配置路由格式:`/router/:id`
* 傳遞的方式:在path后面跟上對應的值
* 傳遞后形成的路徑:`/router/123`
1)路由定義
```
//在APP.vue中
<router-link :to="'/user/'+userId" replace>用戶</router-link>
//在index.js
{
? ?path: '/user/:userid',
? ?component: User,
},
```
2)路由跳轉
```
// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按鈕</router-link
// 方法2:
this.$router.push({name:'users',params:{uname:wade}})
// 方法3:
this.$router.push('/user/' + wade)
```
3)參數獲取
通過 `$route.params.userid` 獲取傳遞的值
**(2)query方式**
* 配置路由格式:`/router`,也就是普通配置
* 傳遞的方式:對象中使用query的key作為傳遞方式
* 傳遞后形成的路徑:`/route?id=123`
1)路由定義
```
//方式1:直接在router-link 標簽上以對象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">檔案</router-link>
// 方式2:寫成按鈕以點擊事件形式
<button @click='profileClick'>我的</button>
profileClick(){
this.$router.push({
path: "/profile",
query: {
name: "kobi",
age: "28",
height: 198
}
});
}
```
2)跳轉方法
```
// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按鈕</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按鈕</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)
```
3)獲取參數
```
通過$route.query 獲取傳遞的值
```
### 6. Vue-router 路由鉤子在生命周期的體現
一、Vue-Router導航守衛
有的時候,需要通過路由來進行一些操作,比如最常見的登錄權限驗證,當用戶滿足條件時,才讓其進入導航,否則就取消跳轉,并跳到登錄頁面讓其登錄。
為此有很多種方法可以植入路由的導航過程:全局的,單個路由獨享的,或者組件級的
1. 全局路由鉤子
全局有三個路由鉤子;
* router.beforeEach 全局前置守衛 進入路由之前
* router.beforeResolve 全局解析守衛(2.5.0+)在 beforeRouteEnter 調用之后調用
* router.afterEach 全局后置鉤子 進入路由之后
具體使用∶
* beforeEach(判斷是否登錄了,沒登錄就跳轉到登錄頁)
### 7. Vue-router跳轉和location.href有什么區別
* 使用 `location.href= /url?`來跳轉,簡單方便,但是刷新了頁面;
* 使用 `history.pushState( /url )` ,無刷新頁面,靜態跳轉;
* 引進 router ,然后使用 `router.push( /url )` 來跳轉,使用了 `diff` 算法,實現了按需加載,減少了 dom 的消耗。其實使用 router 跳轉和使用 `history.pushState()` 沒什么差別的,因為vue-router就是用了 `history.pushState()` ,尤其是在history模式下。
### 8. params和query的區別
**用法**:query要用path來引入,params要用name來引入,接收參數都是類似的,分別是 `this.$route.query.name` 和 `this.$route.params.name` 。
**url地址顯示**:query更加類似于ajax中get傳參,params則類似于post,說的再簡單一點,前者在瀏覽器地址欄中顯示參數,后者則不顯示
**注意**:query刷新不會丟失query里面的數據 params刷新會丟失 params里面的數據。
### 9. Vue-router 導航守衛有哪些
* 全局前置/鉤子:beforeEach、beforeResolve、afterEach
* 路由獨享的守衛:beforeEnter
* 組件內的守衛:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
### 10. 對前端路由的理解
在前端技術早期,一個 url 對應一個頁面,如果要從 A 頁面切換到 B 頁面,那么必然伴隨著頁面的刷新。這個體驗并不好,不過在最初也是無奈之舉——用戶只有在刷新頁面的情況下,才可以重新去請求數據。
后來,改變發生了——Ajax 出現了,它允許人們在不刷新頁面的情況下發起請求;與之共生的,還有“不刷新頁面即可更新頁面內容”這種需求。在這樣的背景下,出現了 **SPA(單頁面應用**)。
SPA極大地提升了用戶體驗,它允許頁面在不刷新的情況下更新頁面內容,使內容的切換更加流暢。但是在 SPA 誕生之初,人們并沒有考慮到“定位”這個問題——在內容切換前后,頁面的 URL 都是一樣的,這就帶來了兩個問題:
* SPA 其實并不知道當前的頁面“進展到了哪一步”。可能在一個站點下經過了反復的“前進”才終于喚出了某一塊內容,但是此時只要刷新一下頁面,一切就會被清零,必須重復之前的操作、才可以重新對內容進行定位——SPA 并不會“記住”你的操作。
* 由于有且僅有一個 URL 給頁面做映射,這對 SEO 也不夠友好,搜索引擎無法收集全面的信息
為了解決這個問題,前端路由出現了。
前端路由可以幫助我們在僅有一個頁面的情況下,“記住”用戶當前走到了哪一步——為 SPA 中的各個視圖匹配一個唯一標識。這意味著用戶前進、后退觸發的新內容,都會映射到不同的 URL 上去。此時即便他刷新頁面,因為當前的 URL 可以標識出他所處的位置,因此內容也不會丟失。
那么如何實現這個目的呢?首先要解決兩個問題:
* 當用戶刷新頁面時,瀏覽器會默認根據當前 URL 對資源進行重新定位(發送請求)。這個動作對 SPA 是不必要的,因為我們的 SPA 作為單頁面,無論如何也只會有一個資源與之對應。此時若走正常的請求-刷新流程,反而會使用戶的前進后退操作無法被記錄。
* 單頁面應用對服務端來說,就是一個URL、一套資源,那么如何做到用“不同的URL”來映射不同的視圖內容呢?
從這兩個問題來看,服務端已經完全救不了這個場景了。所以要靠咱們前端自力更生,不然怎么叫“前端路由”呢?作為前端,可以提供這樣的解決思路:
* 攔截用戶的刷新操作,避免服務端盲目響應、返回不符合預期的資源內容。把刷新這個動作完全放到前端邏輯里消化掉。
* 感知 URL 的變化。這里不是說要改造 URL、憑空制造出 N 個 URL 來。而是說 URL 還是那個 URL,只不過我們可以給它做一些微小的處理——這些處理并不會影響 URL 本身的性質,不會影響服務器對它的識別,只有我們前端感知的到。一旦我們感知到了,我們就根據這些變化、用 JS 去給它生成不同的內容。
## 四、Vuex
### 1. Vuex 的原理
Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。每一個 Vuex 應用的核心就是 store(倉庫)。“store” 基本上就是一個容器,它包含著你的應用中大部分的狀態 ( state )。
* Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那么相應的組件也會相應地得到高效更新。
* 改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣可以方便地跟蹤每一個狀態的變化。

Vuex為Vue Components建立起了一個完整的生態圈,包括開發中的API調用一環。
**(1)核心流程中的主要功能:**
* Vue Components 是 vue 組件,組件會觸發(dispatch)一些事件或動作,也就是圖中的 Actions;
* 在組件中發出的動作,肯定是想獲取或者改變數據的,但是在 vuex 中,數據是集中管理的,不能直接去更改數據,所以會把這個動作提交(Commit)到 Mutations 中;
* 然后 Mutations 就去改變(Mutate)State 中的數據;
* 當 State 中的數據被改變之后,就會重新渲染(Render)到 Vue Components 中去,組件展示更新后的數據,完成一個流程。
**(2)各模塊在核心流程中的主要功能:**
* `Vue Components`∶ Vue組件。HTML頁面上,負責接收用戶操作等交互行為,執行dispatch方法觸發對應action進行回應。
* `dispatch`∶操作行為觸發方法,是唯一能執行action的方法。
* `actions`∶ 操作行為處理模塊。負責處理Vue Components接收到的所有交互行為。包含同步/異步操作,支持多個同名方法,按照注冊的順序依次觸發。向后臺API請求的操作就在這個模塊中進行,包括觸發其他action以及提交mutation的操作。該模塊提供了Promise的封裝,以支持action的鏈式觸發。
* `commit`∶狀態改變提交操作方法。對mutation進行提交,是唯一能執行mutation的方法。
* `mutations`∶狀態改變操作方法。是Vuex修改state的唯一推薦方法,其他修改方式在嚴格模式下將會報錯。該方法只能進行同步操作,且方法名只能全局唯一。操作之中會有一些hook暴露出來,以進行state的監控等。
* `state`∶ 頁面狀態管理容器對象。集中存儲Vuecomponents中data對象的零散數據,全局唯一,以進行統一的狀態管理。頁面顯示所需的數據從該對象中進行讀取,利用Vue的細粒度數據響應機制來進行高效的狀態更新。
* `getters`∶ state對象讀取方法。圖中沒有單獨列出該模塊,應該被包含在了render中,Vue Components通過該方法讀取全局state對象。
**總結:**
Vuex 實現了一個單向數據流,在全局擁有一個 State 存放數據,當組件要更改 State 中的數據時,必須通過 Mutation 提交修改信息, Mutation 同時提供了訂閱者模式供外部插件調用獲取 State 數據的更新。而當所有異步操作(常見于調用后端接口異步獲取更新數據)或批量的同步操作需要走 Action ,但 Action 也是無法直接修改 State 的,還是需要通過Mutation 來修改State的數據。最后,根據 State 的變化,渲染到視圖上。
### 2\. Vuex中action和mutation的區別
mutation中的操作是一系列的同步函數,用于修改state中的變量的的狀態。當使用vuex時需要通過commit來提交需要操作的內容。mutation 非常類似于事件:每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。這個回調函數就是實際進行狀態更改的地方,并且它會接受 state 作為第一個參數:
```
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
state.count++ // 變更狀態
}
}
})
```
當觸發一個類型為 increment 的 mutation 時,需要調用此函數:`store.commit('increment')`
而Action類似于mutation,不同點在于:
* Action 可以包含任意異步操作。
* Action 提交的是 mutation,而不是直接變更狀態。
```
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
```
Action 函數接受一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調用 context.commit 提交一個 mutation,或者通過 context.state 和 context.getters 來獲取 state 和 getters。
所以,兩者的不同點如下:
* Mutation專注于修改State,理論上是修改State的唯一途徑;Action業務代碼、異步請求。
* Mutation:必須同步執行;Action:可以異步,但不能直接操作State。
* 在視圖更新時,先觸發actions,actions再觸發mutation
* mutation的參數是state,它包含store中的數據;store的參數是context,它是 state 的父級,包含 state、getters
### 3\. Vuex 和 localStorage 的區別
**(1)最重要的區別**
* vuex存儲在內存中
* localstorage 則以文件的方式存儲在本地,只能存儲字符串類型的數據,存儲對象需要 JSON的stringify和parse方法進行處理。 讀取內存比讀取硬盤速度要快
**(2)應用場景**
* Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,并以相應的規則保證狀態以一種可預測的方式發生變化。vuex用于組件之間的傳值。
* localstorage是本地存儲,是將數據存儲到瀏覽器的方法,一般是在跨頁面傳遞數據時使用 。
* Vuex能做到數據的響應式,localstorage不能
**(3)永久性**
刷新頁面時vuex存儲的值會丟失,localstorage不會。
**注意:**對于不變的數據確實可以用localstorage可以代替vuex,但是當兩個組件共用一個數據源(對象或數組)時,如果其中一個組件改變了該數據源,希望另一個組件響應該變化時,localstorage無法做到,原因就是區別1。
### 5. 為什么要用 Vuex
由于傳參的方法對于多層嵌套的組件將會非常繁瑣,并且對于兄弟組件間的狀態傳遞無能為力。我們經常會采用父子組件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致代碼無法維護。
所以需要把組件的共享狀態抽取出來,以一個全局單例模式管理。在這種模式下,組件樹構成了一個巨大的"視圖",不管在樹的哪個位置,任何組件都能獲取狀態或者觸發行為。
另外,通過定義和隔離狀態管理中的各種概念并強制遵守一定的規則,代碼將會變得更結構化且易維護。
### 6\. Vuex有哪幾種屬性?
有五種,分別是 State、 Getter、Mutation 、Action、 Module
* state => 基本數據(數據源存放地)
* getters => 從基本數據派生出來的數據
* mutations => 提交更改數據的方法,同步
* actions => 像一個裝飾器,包裹mutations,使之可以異步。
* modules => 模塊化Vuex
### 7\. Vuex和單純的全局對象有什么區別?
* Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那么相應的組件也會相應地得到高效更新。
* 不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣可以方便地跟蹤每一個狀態的變化,從而能夠實現一些工具幫助更好地了解我們的應用。
### 8\. 為什么 Vuex 的 mutation 中不能做異步操作?
* Vuex中所有的狀態更新的唯一途徑都是mutation,異步操作通過 Action 來提交 mutation實現,這樣可以方便地跟蹤每一個狀態的變化,從而能夠實現一些工具幫助更好地了解我們的應用。
* 每個mutation執行完成后都會對應到一個新的狀態變更,這樣devtools就可以打個快照存下來,然后就可以實現 time-travel 了。如果mutation支持異步操作,就沒有辦法知道狀態是何時更新的,無法很好的進行狀態的追蹤,給調試帶來困難。
## 五、Vue 3.0
### 1\. Vue3.0有什么更新
**(1)監測機制的改變**
* 3.0 將帶來基于代理 Proxy的 observer 實現,提供全語言覆蓋的反應性跟蹤。
* 消除了 Vue 2 當中基于 Object.defineProperty 的實現所存在的很多限制:
**(2)只能監測屬性,不能監測對象**
* 檢測屬性的添加和刪除;
* 檢測數組索引和長度的變更;
* 支持 Map、Set、WeakMap 和 WeakSet。
**(3)模板**
* 作用域插槽,2.x 的機制導致作用域插槽變了,父組件會重新渲染,而 3.0 把作用域插槽改成了函數的方式,這樣只會影響子組件的重新渲染,提升了渲染的性能。
* 同時,對于 render 函數的方面,vue3.0 也會進行一系列更改來方便習慣直接使用 api 來生成 vdom 。
**(4)對象式的組件聲明方式**
* vue2.x 中的組件是通過聲明的方式傳入一系列 option,和 TypeScript 的結合需要通過一些裝飾器的方式來做,雖然能實現功能,但是比較麻煩。
* 3.0 修改了組件的聲明方式,改成了類式的寫法,這樣使得和 TypeScript 的結合變得很容易
**(5)其它方面的更改**
* 支持自定義渲染器,從而使得 weex 可以通過自定義渲染器的方式來擴展,而不是直接 fork 源碼來改的方式。
* 支持 Fragment(多個根節點)和 Protal(在 dom 其他部分渲染組建內容)組件,針對一些特殊的場景做了處理。
* 基于 tree shaking 優化,提供了更多的內置功能。
### 2\. defineProperty和proxy的區別
Vue 在實例初始化時遍歷 data 中的所有屬性,并使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。這樣當追蹤數據發生變化時,setter 會被自動調用。
Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。
但是這樣做有以下問題:
1. 添加或刪除對象的屬性時,Vue 檢測不到。因為添加或刪除的對象沒有在初始化進行響應式處理,只能通過`$set` 來調用`Object.defineProperty()`處理。
2. 無法監控到數組下標和長度的變化。
Vue3 使用 Proxy 來監控數據的變化。Proxy 是 ES6 中提供的功能,其作用為:用于定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數調用等)。相對于`Object.defineProperty()`,其有以下特點:
1. Proxy 直接代理整個對象而非對象屬性,這樣只需做一層代理就可以監聽同級結構下的所有屬性變化,包括新增屬性和刪除屬性。
2. Proxy 可以監聽數組的變化。
### 3\. Vue3.0 為什么要用 proxy?
在 Vue2 中, 0bject.defineProperty 會改變原始數據,而 Proxy 是創建對象的虛擬表示,并提供 set 、get 和 deleteProperty 等處理器,這些處理器可在訪問或修改原始對象上的屬性時進行攔截,有以下特點∶
* 不需用使用 `Vue.$set` 或 `Vue.$delete` 觸發響應式。
* 全方位的數組變化檢測,消除了Vue2 無效的邊界情況。
* 支持 Map,Set,WeakMap 和 WeakSet。
Proxy 實現的響應式原理與 Vue2的實現原理相同,實現方式大同小異∶
* get 收集依賴
* Set、delete 等觸發依賴
* 對于集合類型,就是對集合對象的方法做一層包裝:原方法執行后執行依賴相關的收集或觸發邏輯。
### 4\. Vue 3.0 中的 Vue Composition API?
在 Vue2 中,代碼是 Options API 風格的,也就是通過填充 (option) data、methods、computed 等屬性來完成一個 Vue 組件。這種風格使得 Vue 相對于 React極為容易上手,同時也造成了幾個問題:
1. 由于 Options API 不夠靈活的開發方式,使得Vue開發缺乏優雅的方法來在組件間共用代碼。
2. Vue 組件過于依賴`this`上下文,Vue 背后的一些小技巧使得 Vue 組件的開發看起來與 JavaScript 的開發原則相悖,比如在`methods` 中的`this`竟然指向組件實例來不指向`methods`所在的對象。這也使得 TypeScript 在Vue2 中很不好用。
于是在 Vue3 中,舍棄了 Options API,轉而投向 Composition API。Composition API本質上是將 Options API 背后的機制暴露給用戶直接使用,這樣用戶就擁有了更多的靈活性,也使得 Vue3 更適合于 TypeScript 結合。
如下,是一個使用了 Vue Composition API 的 Vue3 組件:
```
<template>
<button @click="increment">
Count: {{ count }}
</button>
</template>
<script>
// Composition API 將組件屬性暴露為函數,因此第一步是導入所需的函數
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// 使用 ref 函數聲明了稱為 count 的響應屬性,對應于Vue2中的data函數
const count = ref(0)
// Vue2中需要在methods option中聲明的函數,現在直接聲明
function increment() {
count.value++
}
// 對應于Vue2中的mounted聲明周期
onMounted(() => console.log('component mounted!'))
return {
count,
increment
}
}
}
</script>
```
顯而易見,Vue Composition API 使得 Vue3 的開發風格更接近于原生 JavaScript,帶給開發者更多地靈活性
## 六、虛擬DOM
### 1\. 對虛擬DOM的理解?
從本質上來說,Virtual Dom是一個JavaScript對象,通過對象的方式來表示DOM結構。將頁面的狀態抽象為JS對象的形式,配合不同的渲染工具,使跨平臺渲染成為可能。通過事務處理機制,將多次DOM修改的結果一次性的更新到頁面上,從而有效的減少頁面渲染的次數,減少修改DOM的重繪重排次數,提高渲染性能。
虛擬DOM是對DOM的抽象,這個對象是更加輕量級的對 DOM的描述。它設計的最初目的,就是更好的跨平臺,比如Node.js就沒有DOM,如果想實現SSR,那么一個方式就是借助虛擬DOM,因為虛擬DOM本身是js對象。 在代碼渲染到頁面之前,vue會把代碼轉換成一個對象(虛擬 DOM)。以對象的形式來描述真實DOM結構,最終渲染到頁面。在每次數據發生變化前,虛擬DOM都會緩存一份,變化之時,現在的虛擬DOM會與緩存的虛擬DOM進行比較。在vue內部封裝了diff算法,通過這個算法來進行比較,渲染時修改改變的變化,原先沒有發生改變的通過原先的數據進行渲染。
另外現代前端框架的一個基本要求就是無須手動操作DOM,一方面是因為手動操作DOM無法保證程序性能,多人協作的項目中如果review不嚴格,可能會有開發者寫出性能較低的代碼,另一方面更重要的是省略手動DOM操作可以大大提高開發效率。
### 2\. 虛擬DOM的解析過程
虛擬DOM的解析過程:
* 首先對將要插入到文檔中的 DOM 樹結構進行分析,使用 js 對象將其表示出來,比如一個元素對象,包含 TagName、props 和 Children 這些屬性。然后將這個 js 對象樹給保存下來,最后再將 DOM 片段插入到文檔中。
* 當頁面的狀態發生改變,需要對頁面的 DOM 的結構進行調整的時候,首先根據變更的狀態,重新構建起一棵對象樹,然后將這棵新的對象樹和舊的對象樹進行比較,記錄下兩棵樹的的差異。
* 最后將記錄的有差異的地方應用到真正的 DOM 樹中去,這樣視圖就更新了。
### 3\. 為什么要用虛擬DOM
**(1)保證性能下限,在不進行手動優化的情況下,提供過得去的性能**
看一下頁面渲染的流程:**解析HTML -> 生成DOM** **\->** **生成 CSSOM** **\->** **Layout** **\->** **Paint** **\->** **Compiler**
下面對比一下修改DOM時真實DOM操作和Virtual DOM的過程,來看一下它們重排重繪的性能消耗∶
* 真實DOM∶ 生成HTML字符串+重建所有的DOM元素
* 虛擬DOM∶ 生成vNode+ DOMDiff+必要的dom更新
Virtual DOM的更新DOM的準備工作耗費更多的時間,也就是JS層面,相比于更多的DOM操作它的消費是極其便宜的。尤雨溪在社區論壇中說道∶ 框架給你的保證是,你不需要手動優化的情況下,依然可以給你提供過得去的性能。
**(2)跨平臺**
Virtual DOM本質上是JavaScript的對象,它可以很方便的跨平臺操作,比如服務端渲染、uniapp等。
### 4\. 虛擬DOM真的比真實DOM性能好嗎
* 首次渲染大量DOM時,由于多了一層虛擬DOM的計算,會比innerHTML插入慢。
* 正如它能保證性能下限,在真實DOM操作的時候進行針對性的優化時,還是更快的。
### 5\. DIFF算法的原理
在新老虛擬DOM對比時:
* 首先,對比節點本身,判斷是否為同一節點,如果不為相同節點,則刪除該節點重新創建節點進行替換
* 如果為相同節點,進行patchVnode,判斷如何對該節點的子節點進行處理,先判斷一方有子節點一方沒有子節點的情況(如果新的children沒有子節點,將舊的子節點移除)
* 比較如果都有子節點,則進行updateChildren,判斷如何對這些新老節點的子節點進行操作(diff核心)。
* 匹配時,找到相同的子節點,遞歸比較子節點
在diff中,只對同層的子節點進行比較,放棄跨級的節點比較,使得時間復雜從O(n3)降低值O(n),也就是說,只有當新舊children都為多個子節點時才需要用核心的Diff算法進行同層級比較。
### 6\. Vue中key的作用
vue 中 key 值的作用可以分為兩種情況來考慮:
* 第一種情況是 v-if 中使用 key。由于 Vue 會盡可能高效地渲染元素,通常會復用已有元素而不是從頭開始渲染。因此當使用 v-if 來實現元素切換的時候,如果切換前后含有相同類型的元素,那么這個元素就會被復用。如果是相同的 input 元素,那么切換前后用戶的輸入不會被清除掉,這樣是不符合需求的。因此可以通過使用 key 來唯一的標識一個元素,這個情況下,使用 key 的元素不會被復用。這個時候 key 的作用是用來標識一個獨立的元素。
* 第二種情況是 v-for 中使用 key。用 v-for 更新已渲染過的元素列表時,它默認使用“就地復用”的策略。如果數據項的順序發生了改變,Vue 不會移動 DOM 元素來匹配數據項的順序,而是簡單復用此處的每個元素。因此通過為每個列表項提供一個 key 值,來以便 Vue 跟蹤元素的身份,從而高效的實現復用。這個時候 key 的作用是為了高效的更新渲染虛擬 DOM。
key 是為 Vue 中 vnode 的唯一標記,通過這個 key,diff 操作可以更準確、更快速
* 更準確:因為帶 key 就不是就地復用了,在 sameNode 函數a.key === b.key對比中可以避免就地復用的情況。所以會更加準確。
* 更快速:利用 key 的唯一性生成 map 對象來獲取對應節點,比遍歷方式更快
### 7\. 為什么不建議用index作為key?
使用index 作為 key和沒寫基本上沒區別,因為不管數組的順序怎么顛倒,index 都是 0, 1, 2...這樣排列,導致 Vue 會復用錯誤的舊子節點,做很多額外的工作。
- 首頁
- 2021年
- 基礎知識
- 同源策略
- 跨域
- css
- less
- scss
- reset
- 超出文本顯示省略號
- 默認滾動條
- 清除浮動
- line-height與vertical-align
- box-sizing
- 動畫
- 布局
- JavaScript
- 設計模式
- 深淺拷貝
- 排序
- canvas
- 防抖節流
- 獲取屏幕/可視區域寬高
- 正則
- 重繪重排
- rem換算
- 手寫算法
- apply、call和bind原理與實現
- this的理解-普通函數、箭頭函數
- node
- nodejs
- express
- koa
- egg
- 基于nodeJS的全棧項目
- 小程序
- 常見問題
- ec-canvas之橫豎屏切換重繪
- 公眾號后臺基本配置
- 小程序發布協議更新
- 小程序引入iconfont字體
- Uni-app
- 環境搭建
- 項目搭建
- 數據庫
- MySQL數據庫安裝
- 數據庫圖形化界面常用命令行
- cmd命令行操作數據庫
- Redis安裝
- APP
- 控制縮放meta
- GIT
- 常用命令
- vsCode
- 常用插件
- Ajax
- axios-services
- 文章
- 如何讓代碼更加優雅
- 虛擬滾動
- 網站收藏
- 防抖節流之定時器清除問題
- 號稱破解全網會員的腳本
- 資料筆記
- 資料筆記2
- 公司面試題
- 服務器相關
- 前端自動化部署-jenkins
- nginx.conf配置
- https添加證書
- shell基本命令
- 微型ssh-deploy前端部署插件
- webpack
- 深入理解loader
- 深入理解plugin
- webpack注意事項
- vite和webpack區別
- React
- react+antd搭建
- Vue
- vue-cli
- vue.config.js
- 面板分割左右拖動
- vvmily-admin-template
- v-if與v-for那個優先級高?
- 下載excel
- 導入excel
- Echart-China-Map
- vue-xlsx(解析excel)
- 給elementUI的el-table添加骨架
- cdn引入配置
- Vue2.x之defineProperty應用
- 徹底弄懂diff算法的key作用
- 復制模板內容
- 表格操作按鈕太多
- element常用組件二次封裝
- Vue3.x
- Vue3快速上手(第一天)
- Vue3.x快速上手(第二天)
- Vue3.x快速上手(第三天)
- vue3+element-plus搭建項目
- vue3
- 腳手架
- vvmily-cli
- TS
- ts筆記
- common
- Date
- utils
- axios封裝
- 2022年
- HTML
- CSS基礎
- JavaScript 基礎
- 前端框架Vue
- 計算機網絡
- 瀏覽器相關
- 性能優化
- js手寫代碼
- 前端安全
- 前端算法
- 前端構建與編譯
- 操作系統
- Node.js
- 一些開放問題、智力題