天天看點

全面總結 Vue 3.0 的新特性 Vue 3.0 新特性及使用方法

全面總結 Vue 3.0 的新特性 Vue 3.0 新特性及使用方法

Vue3.0從20年九月釋出第一個One Piece版本,到現在一直在更新優化;中文版的官方文檔也已經放出;那麼作為終端使用者的我們來看下Vue3新增了哪些功能和特性。

尤大大在B站直播時分享了Vue3.0的幾個亮點:

  • Performance:性能優化
  • Tree-shaking support:支援搖樹優化
  • Composition API:組合API
  • Fragment,Teleport,Suspense:新增的元件
  • Better TypeScript support:更好的TypeScript支援
  • Custom Renderer API:自定義渲染器

在性能方面,對比Vue2.x,性能提升了1.3~2倍左右;打包後的體積也更小了,如果單單寫一個HelloWorld進行打包,隻有13.5kb;加上所有運作時特性,也不過22.5kb。

那麼作為終端使用者的我們,在開發時,和Vue2.x有什麼不同呢?

Talk is cheap

,我們還是來看代碼。

Tree-shaking

Vue3最重要的變化之一就是引入了Tree-Shaking,Tree-Shaking帶來的bundle體積更小是顯而易見的。在2.x版本中,很多函數都挂載在全局Vue對象上,比如nextTick、nextTick、nextTick、set等函數,是以雖然我們可能用不到,但打包時隻要引入了vue這些全局函數仍然會打包進bundle中。

而在Vue3中,所有的API都通過ES6子產品化的方式引入,這樣就能讓webpack或rollup等打包工具在打包時對沒有用到API進行剔除,最小化bundle體積;我們在main.js中就能發現這樣的變化:

  1. //src/main.js
  2. import { createApp } from "vue";
  3. import App from "./App.vue";
  4. import router from "./router";
  5. const app = createApp(App);
  6. app.use(router).mount("#app");

建立app執行個體方式從原來的

new Vue()

變為通過createApp函數進行建立;不過一些核心的功能比如virtualDOM更新算法和響應式系統無論如何都是會被打包的;這樣帶來的變化就是以前在全局配置的元件(Vue.component)、指令(Vue.directive)、混入(Vue.mixin)和插件(Vue.use)等變為直接挂載在執行個體上的方法;我們通過建立的執行個體來調用,帶來的好處就是一個應用可以有多個Vue執行個體,不同執行個體之間的配置也不會互相影響:

  1. const app = createApp(App)
  2. app.use(/* ... */)
  3. app.mixin(/* ... */)
  4. app.component(/* ... */)
  5. app.directive(/* ... */)

是以Vue2.x的以下全局API也需要改為ES6子產品化引入:

  • Vue.nextTick
  • Vue.observable不再支援,改為

    reactive

  • Vue.version
  • Vue.compile (僅全建構)
  • Vue.set (僅相容建構)
  • Vue.delete (僅相容建構)

除此之外,vuex和vue-router也都使用了Tree-Shaking進行了改進,不過api的文法改動不大:

  1. //src/store/index.js
  2. import { createStore } from "vuex";
  3. export default createStore({
  4.   state: {},
  5.   mutations: {},
  6.   actions: {},
  7.   modules: {},
  8. });
  9. //src/router/index.js
  10. import { createRouter, createWebHistory } from "vue-router";
  11. const router = createRouter({
  12.   history: createWebHistory(process.env.BASE_URL),
  13.   routes,

生命周期函數

我們都知道,在Vue2.x中有8個生命周期函數:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed

在vue3中,新增了一個

setup

生命周期函數,setup執行的時機是在

beforeCreate

生命函數之前執行,是以在這個函數中是不能通過

this

來擷取執行個體的;同時為了命名的統一,将

beforeDestroy

改名為

beforeUnmount

destroyed

unmounted

,是以vue3有以下生命周期函數:

  • beforeCreate(建議使用setup代替)
  • created(建議使用setup代替)
  • setup
  • beforeUnmount
  • unmounted

同時,vue3新增了生命周期鈎子,我們可以通過在生命周期函數前加

on

來通路元件的生命周期,我們可以使用以下生命周期鈎子:

  • onBeforeMount
  • onMounted
  • onBeforeUpdate
  • onUpdated
  • onBeforeUnmount
  • onUnmounted
  • onErrorCaptured
  • onRenderTracked
  • onRenderTriggered

那麼這些鈎子函數如何來進行調用呢?我們在setup中挂載生命周期鈎子,當執行到對應的生命周期時,就調用對應的鈎子函數:

  1. import { onBeforeMount, onMounted } from "vue";
  2. export default {
  3.   setup() {
  4.     console.log("----setup----");
  5.     onBeforeMount(() => {
  6.       // beforeMount代碼執行
  7.     });
  8.     onMounted(() => {
  9.       // mounted代碼執行
  10.   },
  11. }

新增的功能

說完生命周期,下面就是我們期待的Vue3新增加的那些功能。

響應式API

我們可以使用

reactive

來為JS對象建立響應式狀态:

  1. import { reactive, toRefs } from "vue";
  2. const user = reactive({
  3.   name: 'Vue2',
  4.   age: 18,
  5. user.name = 'Vue3'

reactive相當于Vue2.x中的

Vue.observable

reactive函數隻接收object和array等複雜資料類型。

對于一些基本資料類型,比如字元串和數值等,我們想要讓它變成響應式,我們當然也可以通過reactive函數建立對象的方式,但是Vue3提供了另一個函數

ref

  1. import { ref } from "vue";
  2. const num = ref(0);
  3. const str = ref("");
  4. const male = ref(true);
  5. num.value++;
  6. console.log(num.value);
  7. str.value = "new val";
  8. console.log(str.value);
  9. male.value = false;
  10. console.log(male.value);

ref傳回的響應式對象是隻包含一個名為value參數的RefImpl對象,在js中擷取和修改都是通過它的value屬性;但是在模闆中被渲染時,自動展開内部的值,是以不需要在模闆中追加

.value

  1. <template>
  2. <div>
  3. <span>{{ count }}</span>
  4. <button @click="count ++">Increment count</button>
  5. </div>
  6. </template>
  7. <script>
  8. import { ref } from 'vue'
  9. export default {
  10. setup() {
  11. const count = ref(0)
  12. return {
  13. count
  14. </script>

reactive主要負責複雜資料結構,而ref主要處理基本資料結構;但是很多童鞋就會誤解ref隻能處理基本資料,ref本身也是能處理對象和數組的:

  1. const obj = ref({
  2.   name: "qwe",
  3.   age: 1,
  4. setTimeout(() => {
  5.   obj.value.name = "asd";
  6. }, 1000);
  7. const list = ref([1, 2, 3, 4, 6]);
  8.   list.value.push(7);
  9. }, 2000);

當我們處理一些大型響應式對象的property時,我們很希望使用ES6的解構來擷取我們想要的值:

  1. let book = reactive({
  2.   name: 'Learn Vue',
  3.   year: 2020,
  4.   title: 'Chapter one'
  5. })
  6. let {
  7.   name,
  8. } = book
  9. name = 'new Learn'
  10. // Learn Vue
  11. console.log(book.name);

但是很遺憾,這樣會消除它的響應式;對于這種情況,我們可以将響應式對象轉換為一組ref,這些ref将保留與源對象的響應式關聯:

  1. } = toRefs(book)
  2. // 注意這裡解構出來的name是ref對象
  3. // 需要通過value來取值指派
  4. name.value = 'new Learn'
  5. // new Learn

對于一些隻讀資料,我們希望防止它發生任何改變,可以通過

readonly

來建立一個隻讀的對象:

  1. import { reactive, readonly } from "vue";
  2. const copy = readonly(book);
  3. //Set operation on key "name" failed: target is readonly.
  4. copy.name = "new copy";

有時我們需要的值依賴于其他值的狀态,在vue2.x中我們使用

computed函數

來進行計算屬性,在vue3中将computed功能進行了抽離,它接受一個getter函數,并為getter傳回的值建立了一個不可變的響應式ref對象:

  1. const double = computed(() => num.value * 2);
  2. // 2
  3. console.log(double.value);
  4. // Warning: computed value is readonly
  5. double.value = 4

或者我們也可以使用get和set函數建立一個可讀寫的ref對象:

  1. const double = computed({
  2.   get: () => num.value * 2,
  3.   set: (val) => (num.value = val / 2),
  4. double.value = 8
  5. // 4

響應式偵聽

和computed相對應的就是watch,computed是多對一的關系,而watch則是一對多的關系;vue3也提供了兩個函數來偵聽資料源的變化:watch和watchEffect。

我們先來看下watch,它的用法群組件的watch選項用法完全相同,它需要監聽某個資料源,然後執行具體的回調函數,我們首先看下它監聽單個資料源的用法:

  1. import { reactive, ref, watch } from "vue";
  2. const state = reactive({
  3.   count: 0,
  4. //偵聽時傳回值得getter函數
  5. watch(
  6.   () => state.count,
  7.   (count, prevCount) => {
  8.     // 1 0
  9.     console.log(count, prevCount);
  10.   }
  11. );
  12. state.count++;
  13. const count = ref(0);
  14. //直接偵聽ref
  15. watch(count, (count, prevCount) => {
  16.   // 2 0
  17.   console.log(count, prevCount, "watch");
  18. count.value = 2;

我們也可以把多個值放在一個數組中進行偵聽,最後的值也以數組形式傳回:

  1.   count: 1,
  2. const count = ref(2);
  3. watch([() => state.count, count], (newVal, oldVal) => {
  4.   //[3, 2]  [1, 2]
  5.   //[3, 4]  [3, 2]
  6.   console.log(newVal, oldVal);
  7. state.count = 3;
  8. count.value = 4;

如果我們來偵聽一個深度嵌套的對象屬性變化時,需要設定

deep:true

  1. const deepObj = reactive({
  2.   a: {
  3.     b: {
  4.       c: "hello",
  5.     },
  6.   () => deepObj,
  7.   (val, old) => {
  8.     // new hello new hello
  9.     console.log(val.a.b.c, old.a.b.c);
  10.   { deep: true }
  11. deepObj.a.b.c = "new hello";

最後的列印結果可以發現都是改變後的值,這是因為偵聽一個響應式對象始終傳回該對象的引用,是以我們需要對值進行深拷貝:

  1. import _ from "lodash";
  2.   () => _.cloneDeep(deepObj),
  3.     // new hello hello

一般偵聽都會在元件銷毀時自動停止,但是有時候我們想在元件銷毀前手動的方式進行停止,可以調用watch傳回的stop函數進行停止:

  1. const stop = watch(count, (count, prevCount) => {
  2.   // 不執行
  3.   console.log(count, prevCount);
  4. setTimeout(()=>{
  5.   count.value = 2;
  6. // 停止watch
  7. stop();

還有一個函數watchEffect也可以用來進行偵聽,但是都已經有watch了,這個watchEffect和watch有什麼差別呢?他們的用法主要有以下幾點不同:

  1. watchEffect不需要手動傳入依賴
               
  2. 每次初始化時watchEffect都會執行一次回調函數來自動擷取依賴
               
  3. watchEffect無法擷取到原值,隻能得到變化後的值
               
  1. import { reactive, ref, watch, watchEffect } from "vue";
  2.   year: 2021,
  3. watchEffect(() => {
  4.   console.log(count.value);
  5.   console.log(state.year);
  6. setInterval(() => {
  7.   count.value++;
  8.   state.year++;

watchEffect會在頁面加載時自動執行一次,追蹤響應式依賴;在加載後定時器每隔1s執行時,watchEffect都會監聽到資料的變化自動執行,每次執行都是擷取到變化後的值。

組合API

Composition API(組合API)也是Vue3中最重要的一個功能了,之前的2.x版本采用的是

Options API

(選項API),即官方定義好了寫法:data、computed、methods,需要在哪裡寫就在哪裡寫,這樣帶來的問題就是随着功能增加,代碼也越來複雜,我們看代碼需要上下反複橫跳:

全面總結 Vue 3.0 的新特性 Vue 3.0 新特性及使用方法

Composition API對比

上圖中,一種顔色代表一個功能,我們可以看到

Options API

的功能代碼比較分散;

Composition API

則可以将同一個功能的邏輯,組織在一個函數内部,利于維護。

我們首先來看下之前Options API的寫法:

  1.   components: {},
  2.   data() {},
  3.   computed: {},
  4.   watch: {},
  5.   mounted() {},

Options API

就是将同一類型的東西放在同一個選項中,當我們的資料比較少的時候,這樣的組織方式是比較清晰的;但是随着資料增多,我們維護的功能點會涉及到多個data和methods,但是我們無法感覺哪些data和methods是需要涉及到的,經常需要來回切換查找,甚至是需要了解其他功能的邏輯,這也導緻了元件難以了解和閱讀。

Composition API

做的就是把同一功能的代碼放到一起維護,這樣我們需要維護一個功能點的時候,不用去關心其他的邏輯,隻關注目前的功能;

Composition API

通過

setup

選項來組織代碼:

  1.   setup(props, context) {}
  2. };

我們看到這裡它接收了兩個參數props和context,props就是父元件傳入的一些資料,context是一個上下文對象,是從2.x暴露出來的一些屬性:

  • attrs
  • slots
  • emit
注:props的資料也需要通過toRefs解構,否則響應式資料會失效。

我們通過一個Button按鈕來看下setup具體的用法:

全面總結 Vue 3.0 的新特性 Vue 3.0 新特性及使用方法

舉個栗子

  1. <div>{{ state.count }} * 2 = {{ double }}</div>
  2. <div>{{ num }}</div>
  3. <div @click="add">Add</div>
  4. import { reactive, computed, ref } from "vue";
  5. name: "Button",
  6. const state = reactive({
  7. count: 1,
  8. const num = ref(2);
  9. function add() {
  10. num.value += 10;
  11. const double = computed(() => state.count * 2);
  12. state,
  13. double,
  14. num,
  15. add,
  16. },

很多童鞋可能就有疑惑了,這跟我在data和methods中寫沒什麼差別麼,不就是把他們放到一起麼?我們可以将

setup

中的功能進行提取分割成一個一個獨立函數,每個函數還可以在不同的元件中進行邏輯複用:

  1.     const { networkState } = useNetworkState();
  2.     const { user } = userDeatil();
  3.     const { list } = tableData();
  4.     return {
  5.       networkState,
  6.       user,
  7.       list,
  8.     };
  9. function useNetworkState() {}
  10. function userDeatil() {}
  11. function tableData() {}

Fragment

所謂的Fragment,就是片段;在vue2.x中,要求每個模闆必須有一個根節點,是以我們代碼要這樣寫:

  1.   <div>
  2.     <span></span>
  3.   </div>

或者在Vue2.x中還可以引入

vue-fragments

庫,用一個虛拟的fragment代替div;在React中,解決方法是通過的一個

React.Fragment

标簽建立一個虛拟元素;在Vue3中我們可以直接不需要根節點:

  1.     <span>hello</span>
  2.     <span>world</span>

這樣就少了很多沒有意義的div元素。

Teleport

Teleport翻譯過來就是傳送、遠距離傳送的意思;顧名思義,它可以将插槽中的元素或者元件傳送到頁面的其他位置:

全面總結 Vue 3.0 的新特性 Vue 3.0 新特性及使用方法

傳送門遊戲

在React中可以通過

createPortal

函數來建立需要傳送的節點;本來尤大大想起名叫

Portal

,但是H5原生的

Portal标簽

也在計劃中,雖然有一些安全問題,但是為了避免重名,是以改成

Teleport

Teleport一個常見的使用場景,就是在一些嵌套比較深的元件來轉移模态框的位置。雖然在邏輯上模态框是屬于該元件的,但是在樣式和DOM結構上,嵌套層級後較深後不利于進行維護(z-index等問題);是以我們需要将其進行剝離出來:

  1. <button @click="showDialog = true">打開模态框</button>
  2. <teleport to="body">
  3. <div class="modal" v-if="showDialog" style="position: fixed">
  4. 我是一個模态框
  5. <button @click="showDialog = false">關閉</button>
  6. <child-component :msg="msg"></child-component>
  7. </teleport>
  8. data() {
  9. showDialog: false,
  10. msg: "hello"

這裡的Teleport中的modal div就被傳送到了body的底部;雖然在不同的地方進行渲染,但是Teleport中的元素群組件還是屬于父元件的邏輯子元件,還是可以和父元件進行資料通信。Teleport接收兩個參數

to

disabled

  • to - string:必須是有效的查詢選擇器或 HTMLElement,可以id或者class選擇器等。
  • disabled - boolean:如果是true表示禁用teleport的功能,其插槽内容将不會移動到任何位置,預設false不禁用。

Suspense

Suspense是Vue3推出的一個内置元件,它允許我們的程式在等待異步元件時渲染一些後備的内容,可以讓我們建立一個平滑的使用者體驗;Vue中加載異步元件其實在Vue2.x中已經有了,我們用的vue-router中加載的路由元件其實也是一個異步元件:

  1.   name: "Home",
  2.   components: {
  3.     AsyncButton: () => import("../components/AsyncButton"),

在Vue3中重新定義,異步元件需要通過

defineAsyncComponent

來進行顯示的定義:

  1. // 全局定義異步元件
  2. import { defineAsyncComponent } from "vue";
  3. const AsyncButton = defineAsyncComponent(() =>
  4.   import("./components/AsyncButton.vue")
  5. app.component("AsyncButton", AsyncButton);
  6. // 元件内定義異步元件
  7. // src/views/Home.vue
  8.     AsyncButton: defineAsyncComponent(() =>
  9.       import("../components/AsyncButton")
  10.     ),

同時對異步元件的可以進行更精細的管理:

  1.     AsyncButton: defineAsyncComponent({
  2.       delay: 100,
  3.       timeout: 3000,
  4.       loader: () => import("../components/AsyncButton"),
  5.       errorComponent: ErrorComponent,
  6.       onError(error, retry, fail, attempts) {
  7.         if (attempts <= 3) {
  8.           retry();
  9.         } else {
  10.           fail();
  11.         }
  12.       },
  13.     }),

這樣我們對異步元件加載情況就能掌控,在加載失敗也能重新加載或者展示異常的狀态:

全面總結 Vue 3.0 的新特性 Vue 3.0 新特性及使用方法

異步元件加載失敗

我們回到Suspense,上面說到它主要是在元件加載時渲染一些後備的内容,它提供了兩個slot插槽,一個

default

預設,一個

fallback

加載中的狀态:

  1. <button @click="showButton">展示異步元件</button>
  2. <template v-if="isShowButton">
  3. <Suspense>
  4. <template #default>
  5. <AsyncButton></AsyncButton>
  6. <template #fallback>
  7. <div>元件加載中...</div>
  8. </Suspense>
  9. const isShowButton = ref(false);
  10. function showButton() {
  11. isShowButton.value = true;
  12. isShowButton,
  13. showButton,
全面總結 Vue 3.0 的新特性 Vue 3.0 新特性及使用方法

異步元件加載顯示占位

非相容的功能

非相容的功能主要是一些和Vue2.x版本改動較大的文法,已經在Vue3上可能存在相容問題了。

data、mixin和filter

在Vue2.x中,我們可以定義data為

object

或者

function

,但是我們知道在元件中如果data是object的話會出現資料互相影響,因為object是引用資料類型;

在Vue3中,data隻接受

function

類型,通過

function

傳回對象;同時

Mixin

的合并行為也發生了改變,當mixin和基類中data合并時,會執行淺拷貝合并:

  1. const Mixin = {
  2.   data() {
  3.       user: {
  4.         name: 'Jack',
  5.         id: 1,
  6.         address: {
  7.           prov: 2,
  8.           city: 3,
  9.         },
  10.       }
  11.     }
  12. const Component = {
  13.   mixins: [Mixin],
  14.         id: 2,
  15.           prov: 4,
  16. // vue2結果:
  17. {
  18.   id: 2,
  19.   name: 'Jack',
  20.   address: {
  21.     prov: 4,
  22.     city: 3
  23. // vue3結果:
  24. user: {

我們看到最後合并的結果,vue2.x會進行深拷貝,對data中的資料向下深入合并拷貝;而vue3隻進行淺層拷貝,對data中資料發現已存在就不合并拷貝。

在vue2.x中,我們還可以通過

過濾器filter

來處理一些文本内容的展示:

  1. <div>{{ status | statusText }}</div>
  2. props: {
  3. status: {
  4. type: Number,
  5. default: 1
  6. filters: {
  7. statusText(value){
  8. if(value === 1){
  9. return '訂單未下單'
  10. } else if(value === 2){
  11. return '訂單待支付'
  12. } else if(value === 3){
  13. return '訂單已完成'

最常見的就是處理一些訂單的文案展示等;然而在vue3中,過濾器filter已經删除,不再支援了,官方建議使用方法調用或者

計算屬性computed

來進行代替。

v-model

在Vue2.x中,

v-model

相當于綁定

value

屬性和

input

事件,它本質也是一個文法糖:

  1. <child-component v-model="msg"></child-component>
  2. <!-- 相當于 -->
  3. <child-component :value="msg" @input="msg=$event"></child-component>

在某些情況下,我們需要對多個值進行雙向綁定,其他的值就需要顯示的使用回調函數來改變了:

  1. <child-component
  2.     v-model="msg"
  3.     :msg1="msg1"
  4.     @change1="msg1=$event"
  5.     :msg2="msg2"
  6.     @change2="msg2=$event">
  7. </child-component>

在vue2.3.0+版本引入了

.sync

修飾符,其本質也是文法糖,是在元件上綁定

@update:propName

回調,文法更簡潔:

  1. <child-component 
  2.     :msg1.sync="msg1"
  3.     :msg2.sync="msg2">
  4.     @update:msg1="msg1=$event"
  5.     @update:msg2="msg2=$event">

Vue3中将

v-model

.sync

進行了功能的整合,抛棄了.sync,表示:多個雙向綁定value值直接用多個v-model傳就好了;同時也将v-model預設傳的prop名稱由value改成了modelValue:

  1.     v-model="msg">
  2.   :modelValue="msg"
  3.   @update:modelValue="msg = $event">

如果我們想通過v-model傳遞多個值,可以将一個

argument

傳遞給v-model:

  1.     v-model.msg1="msg1"
  2.     v-model.msg2="msg2">

v-for和key

在Vue2.x中,我們都知道v-for每次循環都需要給每個子節點一個唯一的key,還不能綁定在template标簽上,

  1. <template v-for="item in list">
  2.   <div :key="item.id">...</div>
  3.   <span :key="item.id">...</span>

而在Vue3中,key值應該被放置在template标簽上,這樣我們就不用為每個子節點設一遍:

  1. <template v-for="item in list" :key="item.id">
  2.   <div>...</div>
  3.   <span>...</span>

v-bind合并

在vue2.x中,如果一個元素同時定義了

v-bind="object"

和一個相同的單獨的屬性,那麼這個單獨的屬性會覆寫

object

中的綁定:

  1. <div id="red" v-bind="{ id: 'blue' }"></div>
  2. <div v-bind="{ id: 'blue' }" id="red"></div>
  3. <!-- 最後結果都相同 -->
  4. <div id="red"></div>

然而在vue3中,如果一個元素同時定義了

v-bind="object"

和一個相同的單獨的屬性,那麼聲明綁定的順序決定了最後的結果(後者覆寫前者):

  1. <!-- template -->
  2. <!-- result -->
  3. <div id="blue"></div>

v-for中ref

vue2.x中,在v-for上使用

ref

屬性,通過

this.$refs

會得到一個數組:

  1. <template
  2. <div v-for="item in list" :ref="setItemRef"></div>
  3. data(){
  4. list: [1, 2]
  5. mounted () {
  6. // [div, div]
  7. console.log(this.$refs.setItemRef)

但是這樣可能不是我們想要的結果;是以vue3不再自動建立數組,而是将ref的處理方式變為了函數,該函數預設傳入該節點:

  1. <div v-for="item in 3" :ref="setItemRef"></div>
  2. import { reactive, onUpdated } from 'vue'
  3. let itemRefs = reactive([])
  4. const setItemRef = el => {
  5. itemRefs.push(el)
  6. onUpdated(() => {
  7. console.log(itemRefs)
  8. itemRefs,
  9. setItemRef

v-for和v-if優先級

在vue2.x中,在一個元素上同時使用v-for和v-if,

v-for

有更高的優先級,是以在vue2.x中做性能優化,有一個重要的點就是v-for和v-if不能放在同一個元素上。

  1. <div v-for="item in list" v-if="item % 2 === 0" :key="item">{{ item }}</div>
  2. list: [1, 2, 3, 4, 5],

總結

繼續閱讀