面試官:vue2和vue3的區別有哪些?

語言: CN / TW / HK

一、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

我是彈窗,我直接移動到了body標籤下

```

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: writablevaluegettersetter 不共存。

搬運 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講解

今天天氣真不錯

{{name}}

```

渲染函式如下所示。

```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事件快取講解

今天天氣真不錯

{{name}}
{}>

```

渲染函式如下所示。

```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

```