Vue3.0新特性

WindrunnerMax發表於2021-02-09

Vue3.0新特性

Vue3.0的設計目標可以概括為體積更小、速度更快、加強TypeScript支援、加強API設計一致性、提高自身可維護性、開放更多底層功能。

描述

Vue2Vue3在一些比較重要的方面的詳細對比。

生命週期的變化

  • Vue2 -> Vue3
  • beforeCreate -> setup
  • created -> setup
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • activated -> onActivated
  • deactivated -> onDeactivated
  • errorCaptured -> onErrorCaptured
  • renderTracked -> onRenderTracked
  • renderTriggered -> onRenderTriggered

在這裡主要是增加了setup這個生命週期,而其他的生命週期都是以API的形式呼叫,實際上隨著Composition API的引入,我們訪問這些鉤子函式的方式已經改變,我們所有的生命週期都應該寫在setup中,此方法我們應該實現大多陣列件程式碼,並處理響應式,生命週期鉤子函式等。

import { onBeforeMount, 
    onMounted, 
    onBeforeUpdate, 
    onUpdated, 
    onBeforeUnmount, 
    onUnmounted, 
    onActivated, 
    onDeactivated, 
    onErrorCaptured, 
    onRenderTracked,  
    onRenderTriggered
} from "vue";

export default {
    setup() {
        onBeforeMount(() => {
            // ... 
        })
        onMounted(() => {
            // ... 
        })
        onBeforeUpdate(() => {
            // ... 
        })
        onUpdated(() => {
            // ... 
        })
        onBeforeUnmount(() => {
            // ... 
        })
        onUnmounted(() => {
            // ... 
        })
        onActivated(() => {
            // ... 
        })
        onDeactivated(() => {
            // ... 
        })
        onErrorCaptured(() => {
            // ... 
        })
        onRenderTracked(() => {
            // ... 
        })
        onRenderTriggered(() => {
            // ... 
        })
    }
}

使用proxy代替defineProperty

Vue2是通過資料劫持的方式來實現響應式的,其中最核心的方法便是通過Object.defineProperty()來實現對屬性的劫持,該方法允許精確地新增或修改物件的屬性,對資料新增屬性描述符中的gettersetter存取描述符實現劫持。Vue2之所以只能相容到IE8主要就是因為defineProperty無法相容IE8,其他瀏覽器也會存在輕微相容問題。

var obj = { __x: 1 };
Object.defineProperty(obj, "x", {
    set: function(x){ console.log("watch"); this.__x = x; },
    get: function(){ return this.__x; }
});
obj.x = 11; // watch
console.log(obj.x); // 11

Vue3使用Proxy實現資料劫持,Object.defineProperty只能監聽屬性,而Proxy能監聽整個物件,通過呼叫new Proxy(),可以建立一個代理用來替代另一個物件被稱為目標,這個代理對目標物件進行了虛擬,因此該代理與該目標物件表面上可以被當作同一個物件來對待。代理允許攔截在目標物件上的底層操作,而這原本是Js引擎的內部能力,攔截行為使用了一個能夠響應特定操作的函式,即通過Proxy去對一個物件進行代理之後,我們將得到一個和被代理物件幾乎完全一樣的物件,並且可以從底層實現對這個物件進行完全的監控。Proxy物件是ES6引入的新特性,Vue3放棄使用了Object.defineProperty,而選擇了使用更快的原生Proxy,即是在相容性方面更偏向於現代瀏覽器。

var target = {a: 1};
var proxy = new Proxy(target, {
    set: function(target, key, value, receiver){ 
        console.log("watch");
        return Reflect.set(target, key, value, receiver);
    },
    get: function(target, key, receiver){ 
        return target[key];
    }
});
proxy.a = 11; // watch
console.log(target); // { a: 11 }

diff演算法的提升

diff演算法的基礎是Virtual DOMVirtual DOM是一棵以JavaScript物件作為基礎的樹,每一個節點稱為VNode,用物件屬性來描述節點,實際上它是一層對真實DOM的抽象,最終可以通過渲染操作使這棵樹對映到真實環境上,簡單來說Virtual DOM就是一個Js物件,用以描述整個文件。
Vue2框架通過深度遞迴遍歷新舊兩個虛擬DOM樹,並比較每個節點上的每個屬性,來確定實際DOM的哪些部分需要更新,由於現代JavaScript引擎執行的高階優化,這種有點暴力的演算法通常非常快速,但是DOM的更新仍然涉及許多不必要的CPU工作。
在這裡引用尤大的描述,為了實現這一點,編譯器和執行時需要協同工作:編譯器分析模板並生成帶有優化提示的程式碼,而執行時儘可能獲取提示並採用快速路徑,這裡有三個主要的優化:

  • 首先,在DOM樹級別,我們注意到,在沒有動態改變節點結構的模板指令(例如v-ifv-for)的情況下,節點結構保持完全靜態,如果我們將一個模板分成由這些結構指令分隔的巢狀塊,則每個塊中的節點結構將再次完全靜態,當我們更新塊中的節點時,我們不再需要遞迴遍歷DOM樹,該塊內的動態繫結可以在一個平面陣列中跟蹤,這種優化通過將需要執行的樹遍歷量減少一個數量級來規避虛擬DOM的大部分開銷。
  • 其次,編譯器積極地檢測模板中的靜態節點、子樹甚至資料物件,並在生成的程式碼中將它們提升到渲染函式之外,這樣可以避免在每次渲染時重新建立這些物件,從而大大提高記憶體使用率並減少垃圾回收的頻率。
  • 第三,在元素級別,編譯器還根據需要執行的更新型別,為每個具有動態繫結的元素生成一個優化標誌,例如具有動態類繫結和許多靜態屬性的元素將收到一個標誌,提示只需要進行類檢查,執行時將獲取這些提示並採用專用的快速路徑。

TypeScript的支援

Vue2中使用的都是Js,其本身並沒有型別系統這個概念,現如今TypeScript異常火爆,對於規模很大的專案,沒有型別宣告,後期維護和程式碼的閱讀都是頭疼的事情,雖然Vue2實際上也是可以使用TS的,但是支援並不算特別完美,此外Vue2的原始碼也是使用FacebookFlow做型別檢查。
最終Vue3借鑑了React Hook實現了更自由的程式設計方式,提出了Composition APIComposition API不需要通過指定一長串選項來定義元件,而是允許使用者像編寫函式一樣自由地表達、組合和重用有狀態的元件邏輯,同時提供出色的TypeScript支援。

打包體積變化

Vue2官方說明執行時打包23k,但這只是沒安裝依賴的時候,隨著依賴包和框架特性的增多,有時候不必要的,未使用的程式碼檔案都被打包了進去,所以後期專案大了,打包檔案會特別多還很大。
Vue3中,通過將大多數全域性API和內部幫助程式移動到JavaScriptmodule.exports屬性上實現這一點,這允許現代模式下的module bundler能夠靜態地分析模組依賴關係,並刪除與未使用的module.exports屬性相關的程式碼,模板編譯器還生成了對Tree Shaking搖樹優化友好的程式碼,只有在模板中實際使用某個特性時,該程式碼才匯入該特性的幫助程式,儘管增加了許多新特性,但Vue3被壓縮後的基線大小約為10KB,不到Vue2的一半。

非相容變更

Vue3相對於Vue2的非相容的變更概括,詳情可以查閱https://v3.cn.vuejs.org/guide/migration/introduction.html

全域性API

  • 全域性Vue API已更改為使用應用程式例項。
  • 全域性和內部API已經被重構為可tree-shakable

模板指令

  • 元件上v-model用法已更改,替換v-bind.sync
  • <template v-for>和非v-for節點上key用法已更改。
  • 在同一元素上使用的v-ifv-for優先順序已更改。
  • v-bind="object"現在排序敏感。
  • v-on:event.native修飾符已移除。
  • v-for中的ref不再註冊ref陣列。

元件

  • 只能使用普通函式建立功能元件。
  • functional屬性在SFC單檔案元件<template>functional元件選項被拋棄。
  • 非同步元件現在需要defineAsyncComponent方法來建立。
  • 元件事件現在需要在emits選項中宣告。

渲染函式

  • 渲染函式API改變。
  • $scopedSlots property已刪除,所有插槽都通過$slots作為函式暴露。
  • $listeners被移除或整合到$attrs
  • $attrs現在包含class and style attribute

自定義元素

  • 自定義元素白名單現在已經在編譯時執行。
  • 對特殊的is prop的使用只嚴格限制在被保留的<component>標記中。

其他小改變

  • destroyed生命週期選項被重新命名為unmounted
  • beforeDestroy生命週期選項被重新命名為beforeUnmount
  • default prop工廠函式不再可以訪問this上下文。
  • 自定義指令API已更改為與元件生命週期一致。
  • data選項應始終被宣告為一個函式。
  • 來自mixindata選項現在為淺合併。
  • Attribute強制策略已更改。
  • 一些過渡class被重新命名。
  • <TransitionGroup>不再預設渲染包裹元素。
  • 當偵聽一個陣列時,只有當陣列被替換時,回撥才會觸發,如果需要在變更時觸發,則需要指定deep選項。
  • 沒有特殊指令的標記v-if/else-if/elsev-forv-slot<template>現在被視為普通元素,並將生成原生的<template>元素,而不是渲染其內部內容。
  • Vue2中,應用根容器的outerHTML將替換為根元件模板,如果根元件沒有模板/渲染選項,則最終編譯為模板,Vue3現在使用應用容器的innerHTML,這意味著容器本身不再被視為模板的一部分。

移除API

  • keyCode支援作為v-on的修飾符。
  • $on$off$once例項方法
  • 過濾器方法,建議用計算屬性或方法代替過濾器。
  • 內聯模板attribute
  • $children例項property
  • $destroy例項方法,使用者不應再手動管理單個Vue元件的生命週期。

示例

Vue3的元件簡單示例,可檢視線上示例https://codesandbox.io/s/c1437?file=/src/App.vue

<template>
  <div>
    <div>{{ msg }}</div>
    <div>count: {{ count }}</div>
    <div>double-count: {{ doubleCount }}</div>
    <button @click="multCount(3)">count => count*3</button>
  </div>
</template>

<script>
import { onMounted, reactive, computed } from "vue";
export default {
  name: "App",
  components: {},
  setup: () => {
    /* 定義data */
    const data = reactive({
      msg: "Hello World",
      count: 1,
    });

    /* 處理生命週期 */
    onMounted(() => {
      console.log("Mounted");
    });

    /* 處理computed */
    const computeds = {
      doubleCount: computed(() => data.count * 2),
    };

    /* 定義methods */
    const methods = {
      multCount: function (n) {
        data.count = data.count * n;
      },
    };

    /* 返回資料 */
    return Object.assign(data, computeds, methods);
  },
};
</script>

<style>
</style>

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://zhuanlan.zhihu.com/p/92143274
https://www.jianshu.com/p/9d3ddaec9134
https://zhuanlan.zhihu.com/p/257044300
https://juejin.cn/post/6867123074148335624
https://juejin.cn/post/6892295955844956167
https://segmentfault.com/a/1190000024580501
https://v3.cn.vuejs.org/guide/migration/introduction.html