vue3組合式API介紹

2023-04-23 06:01:11

為什麼要使用Composition API?

根據官方的說法,vue3.0的變化包括效能上的改進、更小的 bundle 體積、對 TypeScript 更好的支援、用於處理大規模用例的全新 API,全新的api指的就是本文主要要說的組合式api。

在 vue3 版本之前,我們複用元件(或者提取和重用多個元件之間的邏輯),通常有以下幾種方式:

  • Mixin:名稱空間衝突 & 渲染上下文中暴露的 property 來源不清晰。例如在閱讀一個運用了多個 mixin 的模板時,很難看出某個 property 是從哪一個 mixin 中注入的。
  • Renderless Component:無渲染元件需要額外的有狀態的元件範例,從而使得效能有所損耗
  • Vuex:就會變得更加複雜,需要去定義 Mutations 也需要去定義 Actions

上述提到的幾種方式,也是我們專案中正在使用的方式。對於提取和重用多個元件之間的邏輯似乎並不簡單。我們甚至採用了 extend 來做到最大化利用已有元件邏輯,因此使得程式碼邏輯依賴嚴重,難以閱讀和理解。
Vue3 中的 Composition API 便是解決這一問題;且完美支援型別推導,不再是依靠一個簡單的 this 上下文來暴露 property(比如 methods 選項下的函數的 this 是指向元件範例的,而不是這個 methods 物件)。其是一組低侵入式的、函數式的 API,使得我們能夠更靈活地「組合」元件的邏輯。

業務實踐

組合式api的出現就能解決以上兩個問題,此外,它也對TypeScript型別推導更加友好。
在具體使用上,對vue單檔案來說,模板部分和樣式部分基本和以前沒有區別,組合式api主要影響的是邏輯部分。下面是一個經典的vue2的計數器案例.:

vue2 實現

//Counter.vue
export default {
  data: () => ({
    count: 0
  }),
  methods: {
    increment() {
      this.count++;
    }
  },
  computed: {
    double () {
      return this.count * 2;
    }
  }
}

vue3 composition api

當我們在元件間提取並複用邏輯時,組合式API 是十分靈活的。一個組合函數僅依賴它的引數和 Vue 全域性匯出的 API,而不是依賴其微妙的 this 上下文。你可以將元件內的任何一段邏輯匯出為函數以複用它。

  • 基於響應式
  • 提供 vue 的生命週期勾點
  • 元件銷燬時自動銷燬依賴監聽
  • 可複用的邏輯
// Counter.vue
import { ref, computed } from "vue";

export default {
  setup() {
    const count = ref(0);
    const double = computed(() => count * 2)
    function increment() {
      count.value++;
    }
    return {
      count,
      double,
      increment
    }
  }
}

程式碼提取

Composition API的第一個明顯優點是提取邏輯很容易。使用Composition提取上面Counter.vue元件程式碼。

//useCounter.js 組合函數
import { ref, computed } from "vue";

export default function () {
  const count = ref(0);
  const double = computed(() => count * 2)
  function increment() {
    count.value++;
  }
  return {
    count,
    double,
    increment
  }
}

程式碼重用

要在元件中使用該函數,我們只需將模組匯入元件檔案並呼叫它(注意匯入是一個函數)。這將返回我們定義的變數,隨後我們可以從 setup 函數中返回它們。

// MyComponent.js
import useCounter from "./useCounter.js";

export default {
  setup() {
    const { count, double, increment } = useCounter();
    return {
      count,
      double,
      increment
    }
  }
} 

相比而言,組合式 API:

  • 暴露給模板的 property 來源十分清晰,因為它們都是被組合邏輯函數返回的值
  • 不存在名稱空間衝突,可以通過解構任意命名
  • 不再需要僅為邏輯複用而建立新的元件範例


常用api介紹

setup

export default {
  setup(props, context) {
    console.log(context); // { attrs, slots, emit }
    //context.emit('emitFun', {emit: true})
    return { privateMsg: props.msg };
  }
}

setup函數是元件內使用 component API 的入口。是在元件範例被建立時, 初始化了 props 之後呼叫,處於 created 前。還有以下特點:
1.可以返回一個物件或函數,物件的屬性會合併到模板渲染的上下文中;
2.第一個引數是響應式的props物件,注意不能解構 props 物件,會使其失去響應性。 **
也不可直接修改 props,會觸發警告
3.第二個引數是一個上下文物件,暴露了 attrs,slots,emit 物件
4.
this 在 setup 函數中不可用。**因為它不會找到元件範例。setup 的呼叫發生在 data、computed 和 methods 被解析之前,所以它們無法在 setup 中被獲取。

props與上下文物件attrs的區別:
1、props 要先宣告才能取值,attrs 不用先宣告
2、props 宣告過的屬性,attrs 裡不會再出現
3、props 不包含事件,attrs 包含。vue2中的$listeners 被整合到 $attrs

reactive

<template>
  <div>
    <p>{{data.msg}}</p>
    <button @click="updateData">更新資料</button>
  </div>
</template>

<script>
import { reactive } from "vue";

export default {
  name: "ReactiveObject",
  setup() {
    const data = reactive({ msg: "hello world" });
    const updateData = () => {
      data.msg= "hello world " + new Date().getTime();
    };
    return { data, updateData };
  },
};
</script>

reactive函數接收一個普通物件然後返回物件的響應式代理,同 Vue.observable。
原理:通過proxy對資料進行封裝,當資料變化時,觸發模板等內容的更新。

ref

<template>
  <div>
    <p>{{msg}}</p>
    <button @click="updateMessage">更新資料</button>
  </div>
</template>

<script>
import { ref } from "vue";

export default {
  name: "ReactiveSingleValue",
  setup() {
    const msg= ref("hello world");
    const updateMessage = () => {
      msg.value = "hello world " + new Date().getTime();
    };
    return { msg, updateMessage };
  },
};
</script>

ref和reactive存在一定的相似性,所以需要完全理解它們才能高效的在各種場景下選擇不同的方式,它們之間最明顯的區別是ref使用的時候需要通過.value來取值,reactive不用。ref是property而reactive是proxy,reactive能夠深度監聽各種型別物件的變化,ref是處理諸如number,string之類的基本資料型別。
它們的區別也可以這麼理解,ref是使某一個資料提供響應能力,而reactive是為包含該資料的一整個物件提供響應能力。
在模板裡使用ref和巢狀在響應式物件裡時不需要通過.value,會自己解開:

除了響應式ref還有一個參照DOM元素的ref,2.x裡面是通過this.$refs.xxx來參照,但是在setup裡面沒有this,所以也是通過建立一個ref來使用:

<template>
    <div ref="node"></div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
    setup() {
        const node = ref(null)
        onMounted(() => {
            console.log(node.value)  // 此處就是dom元素 <div ref="node"></div>
        })
        return {
            node
        }
    }
}
</script>

computed

傳入一個 getter 函數,返回一個預設不可修改的 ref 物件,同 vue 2.x 中的計算屬性 computed

const count = ref(0)
const sum = computed(() => count.value + 1)
console.log(sum.value) // 1
sum.value = 3 // 錯誤

也可傳入一個 get 和 set 函數物件,建立一個可修改的計算狀態

const count = ref(0)

const sum = computed({
  get: () => count.value + 1,
  set: (value) => {
    count.value = value - 1
  }
})

sum.value = 55
console.log(sum, count) // 1, 54

watchEffect

import { reactive, watchEffect } from "vue";
export default {
  name: "WatchEffect",
  setup() {
    const data = reactive({ count: 1 });
    watchEffect(() => console.log(`偵聽器:${data.count}`));
    setInterval(() => {
      data.count++;
    }, 1000);
    return { data };
  },
};

watchEffect用來監聽資料的變化,它會立即執行一次,之後會追蹤函數裡面用到的所有響應式狀態,當變化後會重新執行該回撥函數。

watch

完全等效於 2.x 中 watch 選項,對比 watchEffect,watch 允許我們:

  • 懶執行副作用;
  • 更明確哪些狀態的改變會觸發偵聽器重新執行副作用;
  • 存取偵聽狀態變化前後的值。
// 監聽一個 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    console.log(count, prevCount)
  }
)

// 直接監聽一個 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  console.log(count, prevCount)
}, {
  deep: true, // 深度監聽
  immediate: true // 初始化執行一次
})

// 監聽多個資料
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  console.log([foo, bar], [prevFoo, prevBar])
})

toRefs

把一個響應式物件轉換成普通物件,該普通物件的每個 property 都是一個 ref,和響應式物件 property 一一對應。可以被解構且保持響應性

<template>
  <div>
    <h1>解構響應式物件資料</h1>
    <p>Username: {{username}}</p>
    <p>Age: {{age}}</p>
  </div>
</template>

<script>
import { reactive, toRefs } from "vue";
export default {
  name: "DestructReactiveObject",
  setup() {
    const user = reactive({
      username: "haihong",
      age: 10000,
    });
    return { ...toRefs(user) };
  },
};
</script>

toRef

toRef 可以用來為一個 reactive 物件的屬性建立一個 ref。這個 ref 可以被傳遞並且能夠保持響應性。

setup() {
  const user = reactive({ age: 1 });
  const age = toRef(user, "age");

  age.value++;
  console.log(user.age); // 2

  user.age++;
  console.log(age.value); // 3
}

Provide/Inject

為了增加 provide 值和 inject 值之間的響應性,我們可以在 provide 值時使用 refreactive
當使用響應式 provide / inject 值時,建議儘可能將對響應式 property 的所有修改限制在定義 provide 的元件內部。然而,有時我們需要在注入資料的元件內部更新 inject 的資料。在這種情況下,我們建議 provide 一個方法來負責改變響應式 property。
最後,如果要確保通過 provide 傳遞的資料不會被 inject 的元件更改,我們建議對提供者的 property 使用 readonly。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', readonly(location))
    provide('geolocation', readonly(geolocation))
    provide('updateLocation', updateLocation)
  }
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    const updateUserLocation = inject('updateLocation')

    return {
      userLocation,
      userGeolocation,
      updateUserLocation
    }
  }
}
</script>

生命週期函數

與 2.x 版本生命週期相對應的組合式 API
~~beforeCreate~~ -> 使用 setup()
~~created~~ -> 使用 setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured

只需要將之前的生命週期改成onXXX的形式即可,需要注意的是created、beforeCreate兩個勾點被刪除了,生命週期函數只能在setup函數裡使用。

總結

使用組合式api還是需要一點時間來適應的,首先需要能區分ref和reactive,不要在基本型別和參照型別、響應式和非響應式物件之間搞混,其次就是如何拆分好每一個use函數,組合式api帶來了更好的程式碼組織方式,但也更容易把程式碼寫的更難以維護,比如setup函數巨長。

簡單總結一下升級思路,data選項裡的資料通過reactive進行宣告,通過...toRefs()返回;computed、mounted等選項通過對應的computed、onMounted等函數來進行替換;methods裡的函數隨便在哪宣告,只要在setup函數裡返回即可。