v-show 和 v-if 區別
- v-show 通過 CSS display 控制顯示和隱藏
- v-if 通過判斷組件真實渲染和銷毀,而不是顯示和隱藏
- 頻繁切換顯示狀態用 v-show,否則用 v-if
v-if
- 當 v-if 與 v-for 一起使用時,v-for 具有比 v-if 更高的優先級,意味著:v-if 將分別重復運行于每個 v-for 循環中,會造成性能問題。所以,不推薦 v-if 和 v-for 同時使用
const compiler = require('vue-template-compiler')const res = compiler.compile(`<div v-if="true" v-for="i in 3">{{message}}</div>`)with (this) {return renderList(3, function(i) {return true ? createElement('div', [createTextVNode(toString(message))]) : createEmptyVNode()})
}
v-show
const compiler = require('vue-template-compiler')const res = compiler.compile(`<p v-show="flag === 'a'">A</p>`)with (this) {return createElement('p',{directives: [{ name: 'show', rawName: 'v-show', value: flag === 'a', expression: "flag === 'a'" },],},[createTextVNode('A')])
}// v-show 操作的是樣式 https://github.com/vuejs/vue/blob/dev/src/platforms/web/runtime/directives/show.js
bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {vnode = locateNode(vnode)const transition = vnode.data && vnode.data.transitionconst originalDisplay = el.__vOriginalDisplay =el.style.display === 'none' ? '' : el.style.displayif (value && transition) {vnode.data.show = trueenter(vnode, () => {el.style.display = originalDisplay})} else {el.style.display = value ? originalDisplay : 'none'}
}
為何在 v-for 中用 key
- 必須用 key,且不能是 index 和 random
- diff 算法中通過 tag 和 key 來判斷,是否是 sameNode
- 減少渲染次數,提升渲染性能
描述 Vue 組件生命周期(父子組件)
- beforeCreate 在初始化事件生命周期之后,數據被觀測(observer)之前調用
- created 實例已經創建完成之后被調用
- 可以進行一些數據、資源請求。在這個階段無法與 DOM 進行交互,如果非想要,可以通過 $nextTick 訪問
- beforeMount 在 DOM 掛載之前被調用,相關的 render 函數首次被調用(如果有 template 會轉換成 render 函數)
- 在此時也可以對數據進行更改,不會觸發 updated
- mounted 創建 vm.$el 并替換 el,并在掛載之后調用該鉤子
- 可以訪問到 DOM 節點,使用 $refs 屬性對 DOM 進行操作,也可以像后臺發送請求,拿到返回數據
- beforeUpdate 數據更新時調用,發生在虛擬 DOM 重新渲染和和打補丁之前
- 可以在這個鉤子中進一步地更改狀態,這不會觸發附加的重新渲染
- updated 由于數據更改導致虛擬 DOM 重新渲染和打補丁,在這之后會調用該鉤子
- 避免在此期間更改狀態,可能會導致更新無限循環
- beforeDestroy 實例銷毀之前調用
- destroyed Vue 實例銷毀后調用,調用后,Vue 實例所有東西都會解綁定,所有事件監聽會被移除,所有子實例也會被銷毀可以執行一些優化操作,清除定時器,解除綁定事件
注意:除了 beforeCreate 和 created 鉤子之外,其他鉤子均在服務器端渲染期間不調用
Vue.prototype._init = function (options?: Object) {// ...initLifecycle(vm)initEvents(vm) // 初始化事件相關的屬性initRender(vm) // vm 添加了一些虛擬 dom、slot 等相關的屬性和方法callHook(vm, 'beforeCreate')initInjections(vm)initState(vm) // props、methods、data、watch、computed等數據初始化initProvide(vm)callHook(vm, 'created')
}
mounted (渲染完成)執行順序是先子后父
function patch(oldVnode, vnode, hydrating, removeOnly) {// 定義收集所有組件的insert hook方法的數組const insertedVnodeQueue = []if (isUndef(oldVnode)) {createElm(vnode, insertedVnodeQueue)}
}
function createElm(vnode, insertedVnodeQueue, ...) {// createChildren會遞歸創建子組件(遞歸createElm)createChildren(vnode, children, insertedVnodeQueue)if (isDef(data)) {// 執行所有的create鉤子invokeCreateHooks(vnode, insertedVnodeQueue)}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {// 把vnode push到insertedVnodeQueueif (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}// 調用insert方法把DOM插入到父節點,因為是遞歸調用,子元素會優先調用insert
insert(parentElm, vnode.elm, refElm)
function invokeInsertHook(vnode, queue, initial) {// 依次調用insert方法queue[i].data.hook.insert(queue[i])
}
const componentVNodeHooks = {// 依次執行 mounted 方法insert(vnode: MountedComponentVNode) {callHook(componentInstance, 'mounted')},
}
$destroy (銷毀完成)執行順序是先子后父
Vue.prototype.$destroy = function() {callHook(vm, 'beforeDestroy')// 遞歸觸發子組件銷毀鉤子函數vm.__patch__(vm._vnode, null)callHook(vm, 'destroyed')
}
Vue 組件如何通訊
- 父 -> 子通過 props,子 -> 父通過 $on $emit
- 在父組件中提供數據子組件進行消費 provide、inject
- ref 獲取實例的方式調用組件的屬性或方法
- 自定義事件 event.$on、event.$off、event.$emit
- vuex 狀態管理實現通信
描述組件渲染和更新過程
- 生成 render 函數,其生成一個 vnode,它會 touch 觸發 getter 進行收集依賴
- 在模板中哪個被引用了就會將其用 Watcher 觀察起來,發生了 setter 也會將其 Watcher 起來
- 如果之前已經被 Watcher 觀察起來,發生更新進行重新渲染
雙向數據綁定 v-model 的實現原理
v-model本質上是語法糖,v-model 在內部為不同的輸入元素使用不同的屬性并拋出不同的事件
- text 和 textarea 元素使用 value 屬性和 input 事件
- checkbox 和 radio 使用 checked 屬性和 change 事件
- select 字段將 value 作為 prop 并將 change 作為事件
const compiler = require('vue-template-compiler')const res = compiler.compile(`<input v-model="name" type="text" />`)with (this) {return createElement('input', {directives: [{ name: 'model', rawName: 'v-model', value: name, expression: 'name' }],attrs: { type: 'text' },domProps: { value: name },on: {input: function($event) {if ($event.target.composing) returnname = $event.target.value},},})
}
對 MVVM 的理解
- Model:代表數據模型,也可以在 Model 中定義數據修改和操作的業務邏輯。我們可以把 Model 稱為數據層,因為它僅僅關注數據本身,不關心任何行為
- View:用戶操作界面。當 ViewModel 對 Model 進行更新的時候,會通過數據綁定更新到 View
- ViewModel:業務邏輯層,View 需要什么數據,ViewModel 要提供這個數據;View 有某些操作,ViewModel 就要響應這些操作
總結: MVVM模式簡化了界面與業務的依賴,解決了數據頻繁更新。MVVM 在使用當中,利用雙向綁定技術,使得 Model 變化時,ViewModel 會自動更新,而 ViewModel 變化時,View 也會自動變化
computed 和 watch 的區別
computed:
- computed 具有緩存性,computed 的值在 getter 執行后是會緩存的,只有它依賴的屬性值改變之后,下一次獲取 computed 的值時才會重新調用對應的 getter 來計算
- computed 適用于比較消耗性能的計算場景,可以提高性能
watch:
- 更多的是觀察作用,類似于數據監聽的回調函數,用于觀察 props、$emit 或本組件的值,當數據變化時來執行回調進行后續操作
- 無緩存性,頁面重新渲染時值不變化也會執行
computed 和 watch 都支持對象的寫法
vm.$watch('obj', {deep: true, // 深度遍歷immediate: true, // 立即觸發handler: function(val, oldVal) {}, // 執行的函數
})
var vm = new Vue({data: { a: 1 },computed: {aPlus: {// this.aPlus 時觸發get: function() {return this.a + 1},// this.aPlus = 1 時觸發set: function(v) {this.a = v - 1},},},
})
為何組件 data 必須是一個函數
- 一個組件被復用多次的話,也就是創建多個實例。本質上,這些實例用的都是同一個構造函數,如果 data 是對象的話(引用數據類型),會影響到所有實例
- 為了組件不同實例 data 不沖突,data 必須是一個函數
自定義 v-model
v-model 可以看成是 value + input 方法的語法糖
- 自定義:自己寫 model 屬性,里面放上 prop 和 event
<template><input type="text" :value="text" @input="$emit('change', $event.target.value)" />
</template><script>
export default {model: {prop: 'text',event: 'change',},props: {text: String,},
}
</script>
相同邏輯如何抽離
- Vue.mixin ,給組件每個生命周期、函數都混入一些公共邏輯
- mixin 混入的鉤子函數會先于組件內的鉤子函數執行,并且在遇到同名選項時也會有選擇性進行合并
何時使用異步組件
核心就是把組件變成一個函數,依賴 import() 語法,可以實現文件的分割加載
- 加載大組件
- 路由異步加載
何時使用 keep-alive
常用的兩個屬性:include、exclude,允許組件有條件的進行緩存
兩個生命周期:activated、deactivated,用來得知當前組件是否處于活躍狀態
- 緩存組件實例,用于保留組件狀態或避免重復渲染
- 多個靜態 Tab 頁的切換時,來優化性能
何時需要使用 beforeDestory
- 解綁自定義事件 event.$off
- 清除定時器
- 解綁自定義的 DOM 事件,如:window、scroll 等
action 和 mutation 有何區別
- action 中可以處理異步,mutation 中不可以
- mutation 做的是原子操作,action 可以整合多個 mutation
vue-router 常用的路由模式
hash 路由
- hash 變化會觸發網頁跳轉,即瀏覽器的前進、后退
- hash 變化不會刷新頁面,SPA 必需的特點
- hash 永遠不會提交到 server 端(前端自生自滅)
history 路由
- 用 url 規范的路由,但跳轉時不刷新頁面
- 需要 server 端配合,可參考:后端配置例子
- pushState 不會觸發 hashchange 事件,popstate 事件只會在瀏覽器某些行為下觸發,比如點擊后退、前進按鈕
vnode 描述一個 DOM 結構
- Vue 中的真實 DOM
<div id="div1" class="container"><p>vdom</p><ul style="font-size: 20px;"><li>a</li></ul>
</div>
- Vue 中的虛擬 DOM
{tag: 'div',props: {className: 'container',id: 'div1',},children: [{tag: 'p',children: 'dom',},{tag: 'ul',props: { style: 'font-size: 20px' },children: [{ tag: 'li', children: 'a' }],},],
}
數據響應式原理
核心 API:Object.defineProperty
- 存在一些問題,Vue 3.0 啟動 Proxy
- Proxy 可以原生支持監聽數組變化
- 但是 Proxy 兼容性不好,且無法 polyfill
問題
- 深度監聽,需要遞歸到底,一次性計算量大
- 無法監聽新增屬性/刪除屬性(Vue.set、Vue.delete)
- 不能監聽數組變化(重新定義原型,重寫 push、pop 等方法)
簡單實現 Vue 中的 defineReactive
// 觸發更新視圖
function updateView() {console.log('視圖更新')
}// 重新定義數組原型
const arrayProto = Array.prototype
// 創建新對象,原型指向 arrayProto ,再擴展新的方法不會影響原型
const arrayMethods = Object.create(arrayProto)
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {arrayMethods[method] = function() {arrayProto[method].call(this, ...arguments) // 原始操作updateView() // 觸發視圖更新}
})// 重新定義屬性,監聽起來
function defineReactive(target, key, value) {observer(value) // 進行監聽// 不可枚舉的不用監聽const property = Object.getOwnPropertyDescriptor(target, key)if (property && property.configurable === false) returnObject.defineProperty(target, key, {get() {return value},set(newValue) {if (newValue !== value) {observer(newValue) // 值修改后進行監聽// value 一直在閉包中,此處設置完之后,再 get 時也是會獲取最新的值value = newValueupdateView() // 觸發更新視圖}},})
}// 監測數據的變化
function observer(target) {// 不是對象或數組(vue 是判斷是否是數組、純粹對象、可擴展對象)if (typeof target !== 'object' || target === null) {return target}// 不要這樣寫,污染全局的 Array 原型/* Array.prototype.push = function () {updateView()} */if (Array.isArray(target)) {target.__proto__ = arrayMethods}// 重新定義各個屬性(for in 也可以遍歷數組)for (let key in target) {if (!Object.hasOwnProperty.call(target, key)) returndefineReactive(target, key, target[key])}
}const data = {name: 'zhangsan',age: 20,info: {address: '北京', // 需要深度監聽},nums: [10, 20, 30],
}observer(data)data.name = 'lisi'
data.age = 21
data.info.address = '上海' // 深度監聽
// data.x = '100' // 新增屬性,監聽不到 —— 所以有 Vue.set
// delete data.name // 刪除屬性,監聽不到 —— 所有已 Vue.delete
data.nums.push(4) // 監聽數組
diff 算法
diff 算法過程:
- 同級元素進行比較,再比較子節點
- 先判斷一方有子節點,一方沒有子節點情況(如果新的 children 沒有子節點,將舊的子節點移除)
- 之后比較都有子節點的情況(核心 diff),遞歸比較子節點
正常 diff 兩個樹的時間復雜度是 O(n^3),但實際情況我們很少會跨級移動 DOM。所以,只有當新舊 children 都為多個子節點時才需要核心的 diff 算法進行同層級比較
- Vue2 核心 diff 算法采用了雙端比較的算法,同時從新舊 children 的兩端開始比較,借助 key 值找到可復用的節點,再進行相關操作。相比 React 的 diff 算法,同樣情況可以減少移動節點次數,減少不必要的性能損耗
- Vue3 核心 diff 算法采用了最長遞增子序列
雙端比較算法
- 使用 舊列表 的頭一個節點 oldStartNode 與 新列表 的頭一個節點 newStartNode 對比
- 使用 舊列表 的最后一個節點 oldEndNode 與 新列表 的最后一個節點 newEndNode 對比
- 使用 舊列表 的頭一個節點 oldStartNode 與 新列表 的最后一個節點 newEndNode 對比
- 使用 舊列表 的最后一個節點 oldEndNode 與 新列表 的頭一個節點 newStartNode 對比
樹 diff 的時間復雜度 O(n^3)
- 對于舊樹上的點 E 來說,它要和新樹上的所有點比較,復雜度為 O(n)
- 點 E 在新樹上沒有找到,點 E 會被刪除,然后遍歷新樹上的所有點找到對應點(X)去填空,復雜度增加到 O(n^2)
- 這樣的操作會在舊樹的每個點進行,最終復雜度為 O(n^3),1000 個節點,要計算 1 億次,算法不可用
優化時間復雜度到 O(n)
- 只比較同一層級,不跨級比較
- tag 不相同,則直接刪掉重建,不再深度比較
- tag 和 key,兩者都相同,則認為是相同節點,不再深度比較
簡述 diff 算法過程
在 Vue 中,主要是 patch()、patchVnode() 和 updateChildren 這三個方法來實現 Diff 的
- 當 Vue 中的響應式數據發生變化時,就會觸發 updateCompoent()
- updateComponent() 會調用 patch() 方法,在該方法中進行比較,調用 sameVnode 判斷是否為相同節點(判斷 key、tag 等靜態屬性),如果是相同節點的話執行 patchVnode 方法,開始比較節點差異,如果不是相同節點的話,則進行替換操作? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??patch() 接收新舊虛擬 DOM,即 oldVnode、vnode
- 首先判斷 vnode 是否存在,如果不存在,刪除舊節點
- 如果 vnode 存在,再判斷 oldVnode,如果不存在,只需新增整個 vnode 即可
- 如果 vnode 和 oldVnode 都存在,判斷兩者是不是相同節點,如果是,調用 patchVnode() ,對兩個節點進行詳細比較
- 如果兩者不是相同節點,只需將 vnode 轉換為真實 DOM 替換 oldVnode
- patchVnode 同樣接收新舊虛擬 DOM,即 oldVnode、vnode
- 首先判斷兩個虛擬 DOM 是不是全等,即沒有任何變動,是的話直接結束函數,否者繼續執行
- 其次更新節點的屬性,接著判斷 vnode.text 是否存在,存在的話只需更新節點文本即可,否則繼續執行
- 判斷 vnode 和 oldVnode 是否有孩子節點
- 如果兩者都有孩子節點,執行 updateChildren() ,進行比較更新
- 如果 vnode 有孩子,oldVnode 沒有,則直接刪除所有孩子節點,并將該文本屬性設為空
- 如果 oldVnode 有孩子,vnode 沒有,則直接刪除所有孩子節點
- 如果兩者都沒有孩子節點,就判斷 oldVnode.text 是否有內容,有的話情況內容即可
- updateChildren 接收三個參數:parentElm 父級真實節點、oldCh 為 oldVnode 的孩子節點、newCh 為 Vnode 的孩子節點oldCh 和 newCh 都是一個數組。正常我們想到的方法就是對這兩個數組一一比較,時間復雜度為 O(NM)。Vue 中是通過四個指針實現的
- 首先是 oldStartVnode 和 newStartVnode 進行比較(兩頭比較),如果比較相同的話,就可以執行 patchVnode
- -如果 oldStartVnode 和 newStartVnode 匹配不上的話,接下來就是 oldEndVnode 和 newEndVnode 做比較了(兩尾比較)
- 如果兩頭和兩尾比較都不是相同節點的話,就開始交叉比較,首先是 oldStartVnode 和 newEndVnode 做比較(頭尾比較)
- 如果 oldStartVnode 和 newEndVnode 匹配不上的話,就 oldEndVnode 和 newStartVnode 進行比較(尾頭比較)如果這四種比較方法都匹配不到相同節點,才是用暴力解法,針對 newStartVnode 去遍歷 oldCh 中剩余的節點,一一匹配
圖片來源:圖文并茂地來詳細講講Vue Diff算法
?
Vue 為何是異步渲染,$nextTick 何用
- 因為如果不采用異步更新,那么每次更新數據都會對當前組件進行重新渲染。異步渲染(合并 data 修改),再更新視圖,可以提高渲染性能
class Watcher {update() {if (this.computed) {// ...} else if (this.sync) {// ...} else {// 當數據發生變化時會將watcher放到一個隊列中批量更新queueWatcher(this)}}
}const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
// 在派發更新時并不會每次修改都觸發watcher的回調
export function queueWatcher(watcher: Watcher) {const id = watcher.id// has對象保證同一個watcher只添加一次if (has[id] == null) {has[id] = trueif (!flushing) {queue.push(watcher)} else {let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}queue.splice(i + 1, 0, watcher)}// 通過waiting保證nextTick的調用邏輯只有一次if (!waiting) {waiting = true// 調用nextTick方法 批量的進行更新nextTick(flushSchedulerQueue)}}
}function flushSchedulerQueue () {// 1.因為父組件的創建過程是先于子的,所以watcher的創建也是先父后子,執行順序也是先父后子// 2.用戶自定義watcher要優先于渲染watcher執行// 3.如果一個組件在父組件watcher執行期間被銷毀,那么它對應的watcher執行都可以被跳過queue.sort((a, b) => a.id - b.id) // 由小到大
}
- $nextTick 在 DOM 更新完之后,觸發回調,用于獲得更新后的 DOM
- $nextTick 方法主要是使用了 宏任務 和 微任務,定義一個異步方法,多次調用 nextTick 會將方法存入隊列中,通過這個異步方法清空當前隊列
Vue 2.4 之前都是使用微任務,但是微任務的優先級過高,有些情況下可能會出現比事件冒泡更快的情況,但如果都是用宏任務,有可能會出現渲染的性能問題
新版,默認使用微任務,但在特殊情況下會使用宏任務,比如:v-on
- 對于實現宏任務,會先判斷是否能用 setImmediate,不能的話降級為 MessageChannel ,以上都不行的話就是用 setTimeout
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {macroTimerFunc = () => {setImmediate(flushCallbacks)}
} else if (typeof MessageChannel !== 'undefined' &&(isNative(MessageChannel) ||// PhantomJSMessageChannel.toString() === '[object MessageChannelConstructor]')
) {const channel = new MessageChannel()const port = channel.port2channel.port1.onmessage = flushCallbacksmacroTimerFunc = () => {port.postMessage(1)}
} else {macroTimerFunc = () => {setTimeout(flushCallbacks, 0)}
}
Vue 常見性能優化方式
- 合理使用 v-show 和 v-if
- 合理使用 computed
- v-for 時加 key(Vue 會進行復用),以及避免和 v-if 同時使用
- 自定義事件、DOM 事件及時銷毀
- 合理使用異步組件、路由懶加載
- 合理使用 keep-alive(SPA 頁面)
- data 層級不要太深,不要講所有數據都放在 data 中(會增加 getter 和 setter,收集對應 watcher)
- 使用 vue-loader 在開發環境做模板編譯(預編譯)
- webpack 層面的優化
- 前端通用的性能優化,如圖片懶加載、防抖、節流
- 使用 SSR
釋義參數 vue-template-compiler
render 中的參數釋義
_c = createElement
function installRenderHelpers(target) {target._o = markOncetarget._n = toNumbertarget._s = toStringtarget._l = renderListtarget._t = renderSlottarget._q = looseEqualtarget._i = looseIndexOftarget._m = renderStatictarget._f = resolveFiltertarget._k = checkKeyCodestarget._b = bindObjectPropstarget._v = createTextVNodetarget._e = createEmptyVNodetarget._u = resolveScopedSlotstarget._g = bindObjectListenerstarget._d = bindDynamicKeystarget._p = prependModifier
}