希望本篇文章能幫你加深對 Vue 的理解,能信誓旦旦地說自己熟練Vue2/3。除此之外,也希望路過的朋友可以幫助我查漏補缺?。
區別
生命週期的變化
整體來看,變化不大,只是名字大部分需要 + on
,功能上類似。使用上 Vue3 組合式 API 需要先引入;Vue2 選項 API 則可直接呼叫,如下所示。
// vue3
<script setup>
import { onMounted } from vue
onMounted(() => {
...
})
// 可將不同的邏輯拆開成多個onMounted,依然按順序執行,不被覆蓋
onMounted(() => {
...
})
</script>
// vue2
<script>
export default {
mounted() {
...
},
}
</script>
常用生命週期表格如下所示。
Vue2.x | Vue3 |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
Tips: setup
是圍繞beforeCreate
和created
生命週期鉤子執行的,所以不需要顯式地去定義。
多根節點
Vue3 支援了多根節點元件,也就是fragment
。
Vue2中,編寫頁面的時候,我們需要去將元件包裹在<div>
中,否則報錯警告。
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
Vue3,我們可以元件包含多個根節點,可以少寫一層,niceeee !
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
非同步元件
Vue3 提供 Suspense
元件,允許程式在等待非同步元件時渲染兜底的內容,如 loading ,使使用者體驗更平滑。使用它,需在模板中宣告,幷包括兩個命名插槽:default
和fallback
。Suspense
確保載入完非同步內容時顯示預設插槽,並將fallback
插槽用作載入狀態。
<tempalte>
<suspense>
<template #default>
<todo-list />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</suspense>
</template>
真實的專案中踩過坑,若想在 setup 中呼叫非同步請求,需在 setup 前加async
關鍵字。這時,會受到警告async setup() is used without a suspense boundary
。
解決方案:在父頁面呼叫當前元件外包裹一層Suspense
元件。
Teleport
Vue3 提供Teleport
元件可將部分DOM移動到 Vue app之外的位置。比如專案中常見的Dialog
元件。
<button @click=dialogVisible = true>點選</button>
<teleport to=body>
<div class=dialog v-if=dialogVisible>
</div>
</teleport>
組合式API
Vue2 是 選項式API(Option API)
,一個邏輯會散亂在檔案不同位置(data、props、computed、watch、生命週期函式等),導致程式碼的可讀性變差,需要上下來回跳轉檔案位置。Vue3 組合式API(Composition API)
則很好地解決了這個問題,可將同一邏輯的內容寫到一起。
除了增強了程式碼的可讀性、內聚性,組合式API 還提供了較為完美的邏輯複用性方案,舉個?,如下所示公用滑鼠座標案例。
// main.vue <template> <span>mouse position {{x}} {{y}}</span> </template> <script setup> import { ref } from vue import useMousePosition from ./useMousePosition const {x, y} = useMousePosition() } </script>
// useMousePosition.js import { ref, onMounted, onUnmounted } from vue function useMousePosition() { let x = ref(0) let y = ref(0) function update(e) { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener( mousemove , update) }) onUnmounted(() => { window.removeEventListener( mousemove , update) }) return { x, y } } </script>
解決了 Vue2 Mixin
的存在的命名衝突隱患,依賴關係不明確,不同元件間配置化使用不夠靈活。
響應式原理
Vue2 響應式原理基礎是Object.defineProperty
;Vue3 響應式原理基礎是Proxy
。
Object.defineProperty
基本用法:直接在一個物件上定義新的屬性或修改現有的屬性,並返回物件。
Tips: writable
和 value
與 getter
和 setter
不共存。
let obj = {} let name = 瑾行 Object.defineProperty(obj, name , { enumerable: true, // 可列舉(是否可通過for...in 或 Object.keys()進行訪問) configurable: true, // 可配置(是否可使用delete刪除,是否可再次設定屬性) // value: , // 任意型別的值,預設undefined // writable: true, // 可重寫 get: function() { return name }, set: function(value) { name = value } })
搬運 Vue2 核心原始碼,略刪減。
function defineReactive(obj, key, val) { // 一 key 一個 dep const dep = new Dep() // 獲取 key 的屬性描述符,發現它是不可配置物件的話直接 return const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 獲取 getter 和 setter,並獲取 val 值 const getter = property && property.get const setter = property && property.set if((!getter || setter) && arguments.length === 2) { val = obj[key] } // 遞迴處理,保證物件中所有 key 被觀察 let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, // get 劫持 obj[key] 的 進行依賴收集 get: function reactiveGetter() { const value = getter ? getter.call(obj) : val if(Dep.target) { // 依賴收集 dep.depend() if(childOb) { // 針對巢狀物件,依賴收集 childOb.dep.depend() // 觸發陣列響應式 if(Array.isArray(value)) { dependArray(value) } } } } return value }) // set 派發更新 obj[key] set: function reactiveSetter(newVal) { ... if(setter) { setter.call(obj, newVal) } else { val = newVal } // 新值設定響應式 childOb = observe(val) // 依賴通知更新 dep.notify() } }
那 Vue3 為何會拋棄它呢?那肯定是有一些缺陷的。
主要原因:無法監聽物件或陣列新增、刪除的元素。
Vue2 方案:針對常用陣列原型方法push
、pop
、shift
、unshift
、splice
、sort
、reverse
進行了hack處理;提供Vue.set
監聽物件/陣列新增屬性。物件的新增/刪除響應,還可以new
個新物件,新增則合併新屬性和舊物件;刪除則將刪除屬性後的物件深拷貝給新物件。
Tips: Object.defineOProperty
是可以監聽陣列已有元素,但 Vue2 沒有提供的原因是效能
問題,具體可看見參考第二篇 ~。
Proxy
Proxy
是ES6新特性,通過第2個引數handler
攔截目標物件的行為。相較於Object.defineProperty
提供語言全範圍的響應能力,消除了侷限性。但在相容性上放棄了(IE11以下)
侷限性
- 物件/陣列的新增、刪除。
- 監測.length修改。
- Map、Set、WeakMap、WeakSet的支援。
基本用法:建立物件的代理,從而實現基本操作的攔截和自定義操作。
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] :
},
set: function() {},
...
}
搬運 Vue3 的原始碼 reactive.ts 檔案
function createReactiveObject(target, isReadOnly, baseHandlers, collectionHandlers, proxyMap) {
...
// collectionHandlers: 處理Map、Set、WeakMap、WeakSet
// baseHandlers: 處理陣列、物件
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
以 baseHandlers.ts 為例,使用Reflect.get而不是target[key]
的原因是receiver引數可以把this指向getter呼叫時,而非Proxy構造時的物件。
// 依賴收集 function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { ... // 陣列型別 const targetIsArray = isArray(target) if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } // 非陣列型別 const res = Reflect.get(target, key, receiver); // 物件遞迴呼叫 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) } return res } } // 派發更新 function createSetter() { return function set(target: Target, key: string | symbol, value: unknown, receiver: Object) { value = toRaw(value) oldValue = target[key] // 因 ref 資料在 set value 時就已 trigger 依賴了,所以直接賦值 return 即可 if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } // 物件是否有 key 有 key set,無 key add const hadKey = hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) if (target === toRaw(receiver)) { if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } }
虛擬DOM
Vue3 相比於 Vue2 虛擬DOM 上增加patchFlag
欄位。我們藉助Vue3 Template Explorer
來看。
<div id=app>
<h1>技術摸魚</h1>
<p>今天天氣真不錯</p>
<div>{{name}}</div>
</div>
渲染函式如下。
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n) const _hoisted_1 = { id: app } const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, 技術摸魚, -1 /* HOISTED */)) const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天氣真不錯, -1 /* HOISTED */)) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(div, _hoisted_1, [ _hoisted_2, _hoisted_3, _createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */) ])) }
注意第 3 個_createElementVNode
的第 4 個引數即patchFlag
欄位型別,欄位型別情況如下所示。1 代表節點為動態文字節點,那在 diff 過程中,只需比對文字對容,無需關注 class、style等。除此之外,發現所有的靜態節點,都儲存為一個變數進行靜態提升
,可在重新渲染時直接引用,無需重新建立。
export const enum PatchFlags { TEXT = 1, // 動態文字內容 CLASS = 1 << 1, // 動態類名 STYLE = 1 << 2, // 動態樣式 PROPS = 1 << 3, // 動態屬性,不包含類名和樣式 FULL_PROPS = 1 << 4, // 具有動態 key 屬性,當 key 改變,需要進行完整的 diff 比較 HYDRATE_EVENTS = 1 << 5, // 帶有監聽事件的節點 STABLE_FRAGMENT = 1 << 6, // 不會改變子節點順序的 fragment KEYED_FRAGMENT = 1 << 7, // 帶有 key 屬性的 fragment 或部分子節點 UNKEYED_FRAGMENT = 1 << 8, // 子節點沒有 key 的fragment NEED_PATCH = 1 << 9, // 只會進行非 props 的比較 DYNAMIC_SLOTS = 1 << 10, // 動態的插槽 HOISTED = -1, // 靜態節點,diff階段忽略其子節點 BAIL = -2 // 代表 diff 應該結束 }
事件快取
Vue3 的 cacheHandler
可在第一次渲染後快取我們的事件。相比於 Vue2 無需每次渲染都傳遞一個新函式。加一個click
事件。
<div id=app>
<h1>技術摸魚</h1>
<p>今天天氣真不錯</p>
<div>{{name}}</div>
<span onCLick=() => {}><span>
</div>
渲染函式如下
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from vue const _withScopeId = n => (_pushScopeId(scope-id),n=n(),_popScopeId(),n) const _hoisted_1 = { id: app } const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(h1, null, 技術摸魚, -1 /* HOISTED */)) const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(p, null, 今天天氣真不錯, -1 /* HOISTED */)) const _hoisted_4 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode(span, { onCLick: () => {} }, [ /*#__PURE__*/_createElementVNode(span) ], -1 /* HOISTED */)) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(div, _hoisted_1, [ _hoisted_2, _hoisted_3, _createElementVNode(div, null, _toDisplayString(_ctx.name), 1 /* TEXT */), _hoisted_4 ])) }
Diff 優化
搬運 Vue3 patchChildren 原始碼。結合上文與原始碼,patchFlag幫助 diff 時區分靜態節點,以及不同型別的動態節點。一定程度地減少節點本身及其屬性的比對。
function patchChildren(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) { // 獲取新老孩子節點 const c1 = n1 && n1.children const c2 = n2.children const prevShapeFlag = n1 ? n1.shapeFlag : 0 const { patchFlag, shapeFlag } = n2 // 處理 patchFlag 大於 0 if(patchFlag > 0) { if(patchFlag && PatchFlags.KEYED_FRAGMENT) { // 存在 key patchKeyedChildren() return } els if(patchFlag && PatchFlags.UNKEYED_FRAGMENT) { // 不存在 key patchUnkeyedChildren() return } } // 匹配是文字節點(靜態):移除老節點,設定文字節點 if(shapeFlag && ShapeFlags.TEXT_CHILDREN) { if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { unmountChildren(c1 as VNode[], parentComponent, parentSuspense) } if (c2 !== c1) { hostSetElementText(container, c2 as string) } } else { // 匹配新老 Vnode 是陣列,則全量比較;否則移除當前所有的節點 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense,...) } else { unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true) } } else { if(prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(container, ) } if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren(c2 as VNodeArrayChildren, container,anchor,parentComponent,...) } } } }
patchUnkeyedChildren 原始碼如下。
function patchUnkeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) { c1 = c1 || EMPTY_ARR c2 = c2 || EMPTY_ARR const oldLength = c1.length const newLength = c2.length const commonLength = Math.min(oldLength, newLength) let i for(i = 0; i < commonLength; i++) { // 如果新 Vnode 已經掛載,則直接 clone 一份,否則新建一個節點 const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as Vnode)) : normalizeVnode(c2[i]) patch() } if(oldLength > newLength) { // 移除多餘的節點 unmountedChildren() } else { // 建立新的節點 mountChildren() } }
patchKeyedChildren原始碼如下,有運用最長遞增序列的演算法思想。
function patchKeyedChildren(c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) { let i = 0; const e1 = c1.length - 1 const e2 = c2.length - 1 const l2 = c2.length // 從頭開始遍歷,若新老節點是同一節點,執行 patch 更新差異;否則,跳出迴圈 while(i <= e1 && i <= e2) { const n1 = c1[i] const n2 = c2[i] if(isSameVnodeType) { patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSvg, optimized) } else { break } i++ } // 從尾開始遍歷,若新老節點是同一節點,執行 patch 更新差異;否則,跳出迴圈 while(i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = c2[e2] if(isSameVnodeType) { patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSvg, optimized) } else { break } e1-- e2-- } // 僅存在需要新增的節點 if(i > e1) { if(i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? c2[nextPos] : parentAnchor while(i <= e2) { patch(null, c2[i], container, parentAnchor, parentComponent, parentSuspense, isSvg, optimized) } } } // 僅存在需要刪除的節點 else if(i > e2) { while(i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) } } // 新舊節點均未遍歷完 // [i ... e1 + 1]: a b [c d e] f g // [i ... e2 + 1]: a b [e d c h] f g // i = 2, e1 = 4, e2 = 5 else { const s1 = i const s2 = i // 快取新 Vnode 剩餘節點 上例即{e: 2, d: 3, c: 4, h: 5} const keyToNewIndexMap = new Map() for (i = s2; i <= e2; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (nextChild.key != null) { if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) { warn( `Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.` ) } keyToNewIndexMap.set(nextChild.key, i) } } } let j = 0 // 記錄即將 patch 的 新 Vnode 數量 let patched = 0 // 新 Vnode 剩餘節點長度 const toBePatched = e2 - s2 + 1 // 是否移動標識 let moved = false let maxNewindexSoFar = 0 // 初始化 新老節點的對應關係(用於後續最大遞增序列演算法) const newIndexToOldIndexMap = new Array(toBePatched) for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 // 遍歷老 Vnode 剩餘節點 for (i = s1; i <= e1; i++) { const prevChild = c1[i] // 代表當前新 Vnode 都已patch,剩餘舊 Vnode 移除即可 if (patched >= toBePatched) { unmount(prevChild, parentComponent, parentSuspense, true) continue } let newIndex // 舊 Vnode 存在 key,則從 keyToNewIndexMap 獲取 if (prevChild.key != null) { newIndex = keyToNewIndexMap.get(prevChild.key) // 舊 Vnode 不存在 key,則遍歷新 Vnode 獲取 } else { for (j = s2; j <= e2; j++) { if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode)){ newIndex = j break } } } // 刪除、更新節點 // 新 Vnode 沒有 當前節點,移除 if (newIndex === undefined) { unmount(prevChild, parentComponent, parentSuspense, true) } else { // 舊 Vnode 的下標位置 + 1,儲存到對應 新 Vnode 的 Map 中 // + 1 處理是為了防止陣列首位下標是 0 的情況,因為這裡的 0 代表需建立新節點 newIndexToOldIndexMap[newIndex - s2] = i + 1 // 若不是連續遞增,則代表需要移動 if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { moved = true } patch(prevChild,c2[newIndex],...) patched++ } } // 遍歷結束,newIndexToOldIndexMap = {0:5, 1:4, 2:3, 3:0} // 新建、移動節點 const increasingNewIndexSequence = moved // 獲取最長遞增序列 ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR j = increasingNewIndexSequence.length - 1 for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i const nextChild = c2[nextIndex] as VNode const anchor = extIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor // 0 新建 Vnode if (newIndexToOldIndexMap[i] === 0) { patch(null,nextChild,...) } else if (moved) { // 移動節點 if (j < 0 || i !== increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) } else { j-- } } } }
打包優化
tree-shaking:模組打包
webpack
、rollup
等中的概念。移除 JavaScript 上下文中未引用的程式碼。主要依賴於import
和export
語句,用來檢測程式碼模組是否被匯出、匯入,且被 JavaScript 檔案使用。
以nextTick
為例子,在 Vue2 中,全域性 API 暴露在 Vue 例項上,即使未使用,也無法通過tree-shaking
進行消除。
import Vue from vue
Vue.nextTick(() => {
// 一些和DOM有關的東西
})
Vue3 中針對全域性 和內部的API進行了重構,並考慮到tree-shaking
的支援。因此,全域性 API 現在只能作為ES模組構建的命名匯出進行訪問。
import { nextTick } from vue
nextTick(() => {
// 一些和DOM有關的東西
})
通過這一更改,只要模組繫結器支援tree-shaking
,則 Vue 應用程式中未使用的api將從最終的捆綁包中消除,獲得最佳檔案大小。受此更改影響的全域性API有如下。
- Vue.nextTick
- Vue.observable (用 Vue.reactive 替換)
- Vue.version
- Vue.compile (僅全構建)
- Vue.set (僅相容構建)
- Vue.delete (僅相容構建)
內部 API 也有諸如 transition、v-model等標籤或者指令被命名匯出。只有在程式真正使用才會被捆綁打包。
根據 尤大 直播可以知道如今 Vue3 將所有執行功能打包也只有22.5kb
,比 Vue2 輕量很多。
自定義渲染API
Vue3 提供的createApp
預設是將 template 對映成 html。但若想生成canvas
時,就需要使用custom renderer api
自定義render生成函式。
// 自定義runtime-render函式
import { createApp } from ./runtime-render
import App from ./src/App
createApp(App).mount( #app )
TypeScript 支援
Vue3 由TS重寫,相對於 Vue2 有更好地TypeScript
支援。
- Vue2
Option API
中 option 是個簡單物件,而TS是一種型別系統,物件導向的語法,不是特別匹配。 - Vue2 需要
vue-class-component
強化vue原生元件,也需要vue-property-decorator
增加更多結合Vue特性的裝飾器,寫法比較繁瑣。