面試官:vue2和vue3的區別有哪些?
一、Vue3 與 Vue2 區別詳述
1. 生命週期
對於生命週期來說,整體上變化不大,只是大部分生命週期鉤子名稱上 + “on”,功能上是類似的。不過有一點需要注意,Vue3 在組合式API(Composition API,下面展開)中使用生命週期鉤子時需要先引入,而 Vue2 在選項API(Options API)中可以直接呼叫生命週期鉤子,如下所示。
```javascript // vue3
// vue2
```
常用生命週期對比如下表所示。
| vue2 | vue3 | | ------------- | --------------- | | beforeCreate | | | created | | | beforeMount | onBeforeMount | | mounted | onMounted | | beforeUpdate | onBeforeUpdate | | updated | onUpdated | | beforeDestroy | onBeforeUnmount | | destroyed | onUnmounted |
Tips: setup 是圍繞 beforeCreate 和 created 生命週期鉤子執行的,所以不需要顯式地去定義。
2. 多根節點
熟悉 Vue2 的朋友應該清楚,在模板中如果使用多個根節點時會報錯,如下所示。
```javascript // vue2中在template裡存在多個根節點會報錯
// 只能存在一個根節點,需要用一個
但是,Vue3 支援多個根節點,也就是 fragment。即以下多根節點的寫法是被允許的。
```javascript ```
3. Composition API
Vue2 是選項API(Options API),一個邏輯會散亂在檔案不同位置(data、props、computed、watch、生命週期鉤子等),導致程式碼的可讀性變差。當需要修改某個邏輯時,需要上下來回跳轉檔案位置。
Vue3 組合式API(Composition API)則很好地解決了這個問題,可將同一邏輯的內容寫到一起,增強了程式碼的可讀性、內聚性,其還提供了較為完美的邏輯複用性方案。
4. 非同步元件(Suspense)
Vue3 提供 Suspense 元件,允許程式在等待非同步元件載入完成前渲染兜底的內容,如 loading ,使使用者的體驗更平滑。使用它,需在模板中宣告,幷包括兩個命名插槽:default 和 fallback。Suspense 確保載入完非同步內容時顯示預設插槽,並將 fallback 插槽用作載入狀態。參考 前端進階面試題詳細解答
javascript
<tempalte>
<suspense>
<template #default>
<List />
</template>
<template #fallback>
<div>
Loading... </div>
</template>
</suspense>
</template>
在 List 元件(有可能是非同步元件,也有可能是元件內部處理邏輯或查詢操作過多導致載入過慢等)未載入完成前,顯示 Loading...(即 fallback 插槽內容),載入完成時顯示自身(即 default 插槽內容)。
5. Teleport
Vue3 提供 Teleport 元件可將部分 DOM 移動到 Vue app 之外的位置。比如專案中常見的 Dialog 彈窗。
```javascript
```
6. 響應式原理
Vue2 響應式原理基礎是 Object.defineProperty;Vue3 響應式原理基礎是 Proxy。
- Object.defineProperty 基本用法:直接在一個物件上定義新的屬性或修改現有的屬性,並返回物件。
javascript
let obj = {};
let name = 'leo';
Object.defineProperty(obj, 'name', {
enumerable: true, // 可列舉(是否可通過 for...in 或 Object.keys() 進行訪問)
configurable: true, // 可配置(是否可使用 delete 刪除,是否可再次設定屬性)
// value: '', // 任意型別的值,預設undefined
// writable: true, // 可重寫
get() {
return name;
},
set(value) {
name = value;
}
});
Tips:
writable
和value
與getter
和setter
不共存。
搬運 Vue2 核心原始碼,略刪減。
```javascript 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個新物件,新增則合併新屬性和舊物件;刪除則將刪除屬性後的物件深拷貝給新物件。
- Proxy Proxy 是 ES6 新特性,通過第2個引數 handler 攔截目標物件的行為。相較於 Object.defineProperty 提供語言全範圍的響應能力,消除了侷限性。
侷限性:
(1)、物件/陣列的新增、刪除
(2)、監測 .length 修改
(3)、Map、Set、WeakMap、WeakSet 的支援
基本用法:建立物件的代理,從而實現基本操作的攔截和自定義操作。
javascript
let handler = {
get(obj, prop) {
return prop in obj ? obj[prop] : '';
},
set() {
// ...
},
...
};
搬運 vue3 的原始碼 reactive.ts 檔案。
javascript
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
}
7. 虛擬DOM
Vue3 相比於 Vue2,虛擬DOM上增加 patchFlag 欄位。我們藉助Vue3 Template Explorer來看。
```javascript
vue3虛擬DOM講解
今天天氣真不錯
```
渲染函式如下所示。
```javascript 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, vue3虛擬DOM講解, -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等。除此之外,發現所有的靜態節點(HOISTED 為 -1),都儲存為一個變數進行靜態提升,可在重新渲染時直接引用,無需重新建立。
javascript
// patchFlags 欄位型別列舉
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 應該結束
}
8. 事件快取
Vue3 的cacheHandler
可在第一次渲染後快取我們的事件。相比於 Vue2 無需每次渲染都傳遞一個新函式。加一個 click 事件。
```javascript
vue3事件快取講解
今天天氣真不錯
```
渲染函式如下所示。
```javascript 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, vue3事件快取講解, -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 ])) } ```
觀察以上渲染函式,你會發現 click 事件節點為靜態節點(HOISTED 為 -1),即不需要每次重新渲染。
9. Diff演算法優化
搬運 Vue3 patchChildren 原始碼。結合上文與原始碼,patchFlag 幫助 diff 時區分靜態節點,以及不同型別的動態節點。一定程度地減少節點本身及其屬性的比對。
```javascript 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 原始碼如下所示。
```javascript 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() }
} ```
10. 打包優化
Tree-shaking:模組打包 webpack、rollup 等中的概念。移除 JavaScript 上下文中未引用的程式碼。主要依賴於 import 和 export 語句,用來檢測程式碼模組是否被匯出、匯入,且被 JavaScript 檔案使用。
以 nextTick 為例子,在 Vue2 中,全域性API暴露在Vue例項上,即使未使用,也無法通過 tree-shaking 進行消除。
```javascript import Vue from 'vue';
Vue.nextTick(() => { // 一些和DOM有關的東西 }); ```
Vue3 中針對全域性和內部的API進行了重構,並考慮到 tree-shaking 的支援。因此,全域性API現在只能作為ES模組構建的命名匯出進行訪問。
```javascript 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 輕量很多。
11. TypeScript支援
Vue3 由 TypeScript 重寫,相對於 Vue2 有更好的 TypeScript 支援。
-
Vue2 Options API 中 option 是個簡單物件,而 TypeScript 是一種型別系統,面向物件的語法,不是特別匹配。
-
Vue2 需要vue-class-component強化vue原生元件,也需要vue-property-decorator增加更多結合Vue特性的裝飾器,寫法比較繁瑣。
二、Options API 與 Composition API
Vue 元件可以用兩種不同的 API 風格編寫:Options API 和 Composition API。
1. Options API
使用 Options API,我們使用選項物件定義元件的邏輯,例如data、methods和mounted。由選項定義的屬性在 this 內部函式中公開,指向元件例項,如下所示。
```javascript
```
2. Composition API
使用 Composition API,我們使用匯入的 API 函式定義元件的邏輯。在 SFC 中,Composition API 通常使用
```javascript
```