vue初始化頁面閃動問題
使用vue開發時,在vue初始化之前,由于div是不歸vue管的,所以我們寫的代碼在還沒有解析的情況下會容易出現花屏現象,看到類似于{{message}}的字樣,雖然一般情況下這個時間很短暫,但是還是有必要讓解決這個問題的。
首先:在css里加上以下代碼:
[v-cloak] { display: none;}
復制代碼
如果沒有徹底解決問題,則在根元素加上style="display: none;" :style="{display: 'block'}"
Vue模版編譯原理知道嗎,能簡單說一下嗎?
簡單說,Vue的編譯過程就是將template
轉化為render
函數的過程。會經歷以下階段:
- 生成AST樹
- 優化
- codegen
首先解析模版,生成AST語法樹
(一種用JavaScript對象的形式來描述整個模板)。 使用大量的正則表達式對模板進行解析,遇到標簽、文本的時候都會執行對應的鉤子進行相關處理。
Vue的數據是響應式的,但其實模板中并不是所有的數據都是響應式的。有一些數據首次渲染后就不會再變化,對應的DOM也不會變化。那么優化過程就是深度遍歷AST樹,按照相關條件對樹節點進行標記。這些被標記的節點(靜態節點)我們就可以跳過對它們的比對
,對運行時的模板起到很大的優化作用。
編譯的最后一步是將優化后的AST樹轉換為可執行的代碼
。
生命周期鉤子是如何實現的
Vue 的生命周期鉤子核心實現是利用發布訂閱模式先把用戶傳入的的生命周期鉤子訂閱好(內部采用數組的方式存儲)然后在創建組件實例的過程中會一次執行對應的鉤子方法(發布)
相關代碼如下
export function callHook(vm, hook) {
// 依次執行生命周期對應的方法
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); //生命周期里面的this指向當前實例
}
}
}
// 調用的時候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); //初始化數據之前
// 初始化狀態
initState(vm);
callHook(vm, "created"); //初始化數據之后
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
Vue為什么沒有類似于React中shouldComponentUpdate的生命周期?
考點: Vue的變化偵測原理
前置知識: 依賴收集、虛擬DOM、響應式系統
根本原因是Vue與React的變化偵測方式有所不同
React是pull的方式偵測變化,當React知道發生變化后,會使用Virtual Dom Diff進行差異檢測,但是很多組件實際上是肯定不會發生變化的,這個時候需要用shouldComponentUpdate進行手動操作來減少diff,從而提高程序整體的性能.
Vue是pull+push的方式偵測變化的,在一開始就知道那個組件發生了變化,因此在push的階段并不需要手動控制diff,而組件內部采用的diff方式實際上是可以引入類似于shouldComponentUpdate相關生命周期的,但是通常合理大小的組件不會有過量的diff,手動優化的價值有限,因此目前Vue并沒有考慮引入shouldComponentUpdate這種手動優化的生命周期.
了解nextTick嗎?
異步方法,異步渲染最后一步,與JS事件循環聯系緊密。主要使用了宏任務微任務(setTimeout
、promise
那些),定義了一個異步方法,多次調用nextTick
會將方法存入隊列,通過異步方法清空當前隊列。
Vue 組件通訊有哪幾種方式
- props 和
emit 觸發事件來做到的
-
children 獲取當前組件的父組件和當前組件的子組件
-
listeners A->B->C。Vue 2.4 開始提供了
listeners 來解決這個問題
- 父組件中通過 provide 來提供變量,然后在子組件中通過 inject 來注入變量。(官方不推薦在實際業務中使用,但是寫組件庫時很常用)
- $refs 獲取組件實例
- envetBus 兄弟組件數據傳遞 這種情況下可以使用事件總線的方式
- vuex 狀態管理
談一下對 vuex 的個人理解
vuex 是專門為 vue 提供的全局狀態管理系統,用于多個組件中數據共享、數據緩存等。(無法持久化、內部核心原理是通過創造一個全局實例 new Vue)
主要包括以下幾個模塊:
- State:定義了應用狀態的數據結構,可以在這里設置默認的初始狀態。
- Getter:允許組件從 Store 中獲取數據,mapGetters 輔助函數僅僅是將 store 中的 getter 映射到局部計算屬性。
- Mutation:是唯一更改 store 中狀態的方法,且必須是同步函數。
- Action:用于提交 mutation,而不是直接變更狀態,可以包含任意異步操作。
- Module:允許將單一的 Store 拆分為多個 store 且同時保存在單一的狀態樹中。
了解nextTick嗎?
異步方法,異步渲染最后一步,與JS事件循環聯系緊密。主要使用了宏任務微任務(setTimeout
、promise
那些),定義了一個異步方法,多次調用nextTick
會將方法存入隊列,通過異步方法清空當前隊列。
說一下Vue的生命周期
Vue 實例有?個完整的?命周期,也就是從開始創建、初始化數據、編譯模版、掛載Dom -> 渲染、更新 -> 渲染、卸載 等?系列過程,稱這是Vue的?命周期。
- beforeCreate(創建前):數據觀測和初始化事件還未開始,此時 data 的響應式追蹤、event/watcher 都還沒有被設置,也就是說不能訪問到data、computed、watch、methods上的方法和數據。
-
created(創建后) :實例創建完成,實例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此時渲染得節點還未掛載到 DOM,所以不能訪問到
$el
屬性。 - beforeMount(掛載前):在掛載開始之前被調用,相關的render函數首次被調用。實例已完成以下的配置:編譯模板,把data里面的數據和模板生成html。此時還沒有掛載html到頁面上。
- mounted(掛載后):在el被新創建的 vm.$el 替換,并掛載到實例上去之后調用。實例已完成以下的配置:用上面編譯好的html內容替換el屬性指向的DOM對象。完成模板中的html渲染到html 頁面中。此過程中進行ajax交互。
- beforeUpdate(更新前):響應式數據更新時調用,此時雖然響應式數據更新了,但是對應的真實 DOM 還沒有被渲染。
- updated(更新后) :在由于數據更改導致的虛擬DOM重新渲染和打補丁之后調用。此時 DOM 已經根據響應式數據的變化更新了。調用時,組件 DOM已經更新,所以可以執行依賴于DOM的操作。然而在大多數情況下,應該避免在此期間更改狀態,因為這可能會導致更新無限循環。該鉤子在服務器端渲染期間不被調用。
-
beforeDestroy(銷毀前):實例銷毀之前調用。這一步,實例仍然完全可用,
this
仍能獲取到實例。 - destroyed(銷毀后):實例銷毀后調用,調用后,Vue 實例指示的所有東西都會解綁定,所有的事件監聽器會被移除,所有的子實例也會被銷毀。該鉤子在服務端渲染期間不被調用。
另外還有 keep-alive
獨有的生命周期,分別為 activated
和 deactivated
。用 keep-alive
包裹的組件在切換時不會進行銷毀,而是緩存到內存中并執行 deactivated
鉤子函數,命中緩存渲染后會執行 activated
鉤子函數。
Vuex的嚴格模式是什么,有什么作用,如何開啟?
在嚴格模式下,無論何時發生了狀態變更且不是由mutation函數引起的,將會拋出錯誤。這能保證所有的狀態變更都能被調試工具跟蹤到。
在Vuex.Store 構造器選項中開啟,如下
const store = new Vuex.Store({
strict:true,
})
復制代碼
Redux 和 Vuex 有什么區別,它們的共同思想
(1)Redux 和 Vuex區別
- Vuex改進了Redux中的Action和Reducer函數,以mutations變化函數取代Reducer,無需switch,只需在對應的mutation函數里改變state值即可
- Vuex由于Vue自動重新渲染的特性,無需訂閱重新渲染函數,只要生成新的State即可
- Vuex數據流的順序是∶View調用store.commit提交對應的請求到Store中對應的mutation函數->store改變(vue檢測到數據變化自動渲染)
通俗點理解就是,vuex 弱化 dispatch,通過commit進行 store狀態的一次更變;取消了action概念,不必傳入特定的 action形式進行指定變更;弱化reducer,基于commit參數直接對數據進行轉變,使得框架更加簡易;
(2)共同思想
- 單—的數據源
- 變化可以預測
本質上:redux與vuex都是對mvvm思想的服務,將數據從視圖中抽離的一種方案;
形式上:vuex借鑒了redux,將store作為全局的數據中心,進行mode管理;
DIFF算法的原理
在新老虛擬DOM對比時:
- 首先,對比節點本身,判斷是否為同一節點,如果不為相同節點,則刪除該節點重新創建節點進行替換
- 如果為相同節點,進行patchVnode,判斷如何對該節點的子節點進行處理,先判斷一方有子節點一方沒有子節點的情況(如果新的children沒有子節點,將舊的子節點移除)
- 比較如果都有子節點,則進行updateChildren,判斷如何對這些新老節點的子節點進行操作(diff核心)。
- 匹配時,找到相同的子節點,遞歸比較子節點
在diff中,只對同層的子節點進行比較,放棄跨級的節點比較,使得時間復雜從O(n3)降低值O(n),也就是說,只有當新舊children都為多個子節點時才需要用核心的Diff算法進行同層級比較。
nextTick 使用場景和原理
nextTick 中的回調是在下次 DOM 更新循環結束之后執行的延遲回調。在修改數據之后立即使用這個方法,獲取更新后的 DOM。主要思路就是采用微任務優先的方式調用異步方法去執行 nextTick 包裝的方法
相關代碼如下
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把標志還原為false
// 依次執行回調
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定義異步方法 采用優雅降級
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是監聽dom變化 也是一個異步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判斷setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降級采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 還有用戶自己手動調用的nextTick 一起被收集到數組
callbacks.push(cb);
if (!pending) {
// 如果多次調用nextTick 只會執行一次異步 等異步隊列清空之后再把標志變為false
pending = true;
timerFunc();
}
}
對keep-alive的理解,它是如何實現的,具體緩存的是什么?
如果需要在組件切換的時候,保存一些組件的狀態防止多次渲染,就可以使用 keep-alive 組件包裹需要保存的組件。
(1)keep-alive
keep-alive有以下三個屬性:
- include 字符串或正則表達式,只有名稱匹配的組件會被匹配;
- exclude 字符串或正則表達式,任何名稱匹配的組件都不會被緩存;
- max 數字,最多可以緩存多少組件實例。
注意:keep-alive 包裹動態組件時,會緩存不活動的組件實例。
主要流程
- 判斷組件 name ,不在 include 或者在 exclude 中,直接返回 vnode,說明該組件不被緩存。
- 獲取組件實例 key ,如果有獲取實例的 key,否則重新生成。
- key生成規則,cid +"∶∶"+ tag ,僅靠cid是不夠的,因為相同的構造函數可以注冊為不同的本地組件。
- 如果緩存對象內存在,則直接從緩存對象中獲取組件實例給 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函數:
- 會在 keep-alive 組件內部去寫自己的內容,所以可以去獲取默認 slot 的內容,然后根據這個去獲取組件
- keep-alive 只對第一個組件有效,所以獲取第一個子組件。
- 和 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數組最后,以便清除最不常用組件。
實現步驟:
獲取 keep-alive 下第一個子組件的實例對象,通過他去獲取這個組件的組件名
通過當前組件名去匹配原來 include 和 exclude,判斷當前組件是否需要緩存,不需要緩存,直接返回當前組件的實例vNode
需要緩存,判斷他當前是否在緩存數組里面:
存在,則將他原來位置上的 key 給移除,同時將這個組件的 key 放到數組最后面(LRU)
不存在,將組件 key 放入數組,然后判斷當前 key數組是否超過 max 所設置的范圍,超過,那么削減未使用時間最長的一個組件的 key
- 最后將這個組件的 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)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是 "如果數據最近被訪問過,那么將來被訪問的幾率也更高"。 最常見的實現是使用一個鏈表保存緩存數據,詳細算法實現如下∶
- 新數據插入到鏈表頭部
- 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部
- 鏈表滿的時候,將鏈表尾部的數據丟棄。
computed 的實現原理
computed 本質是一個惰性求值的觀察者。
computed 內部實現了一個惰性的 watcher,也就是 computed watcher,computed watcher 不會立刻求值,同時持有一個 dep 實例。
其內部通過 this.dirty 屬性標記計算屬性是否需要重新求值。
當 computed 的依賴狀態發生改變時,就會通知這個惰性的 watcher,
computed watcher 通過 this.dep.subs.length 判斷有沒有訂閱者,
有的話,會重新計算,然后對比新舊值,如果變化了,會重新渲染。 (Vue 想確保不僅僅是計算屬性依賴的值發生變化,而是當計算屬性最終計算的值發生變化時才會觸發渲染 watcher 重新渲染,本質上是一種優化。)
沒有的話,僅僅把 this.dirty = true。 (當計算屬性依賴于其他數據時,屬性并不會立即重新計算,只有之后其他地方需要讀取屬性的時候,它才會真正計算,即具備 lazy(懶計算)特性。)
keep-alive 使用場景和原理
keep-alive 是 Vue 內置的一個組件,可以實現組件緩存,當組件切換時不會對當前組件進行卸載。
- 常用的兩個屬性 include/exclude,允許組件有條件的進行緩存。
- 兩個生命周期 activated/deactivated,用來得知當前組件是否處于活躍狀態。
- keep-alive 的中還運用了 LRU(最近最少使用) 算法,選擇最近最久未使用的組件予以淘汰。