根據官方的說法,vue3.0的變化包括效能上的改進、更小的 bundle 體積、對 TypeScript 更好的支援、用於處理大規模用例的全新 API,全新的api指的就是本文主要要說的組合式api。
在 vue3 版本之前,我們複用元件(或者提取和重用多個元件之間的邏輯),通常有以下幾種方式:
上述提到的幾種方式,也是我們專案中正在使用的方式。對於提取和重用多個元件之間的邏輯似乎並不簡單。我們甚至採用了 extend 來做到最大化利用已有元件邏輯,因此使得程式碼邏輯依賴嚴重,難以閱讀和理解。
Vue3 中的 Composition API 便是解決這一問題;且完美支援型別推導,不再是依靠一個簡單的 this 上下文來暴露 property(比如 methods 選項下的函數的 this 是指向元件範例的,而不是這個 methods 物件)。其是一組低侵入式的、函數式的 API,使得我們能夠更靈活地「組合」元件的邏輯。
組合式api的出現就能解決以上兩個問題,此外,它也對TypeScript型別推導更加友好。
在具體使用上,對vue單檔案來說,模板部分和樣式部分基本和以前沒有區別,組合式api主要影響的是邏輯部分。下面是一個經典的vue2的計數器案例.:
//Counter.vue
export default {
data: () => ({
count: 0
}),
methods: {
increment() {
this.count++;
}
},
computed: {
double () {
return this.count * 2;
}
}
}
當我們在元件間提取並複用邏輯時,組合式API 是十分靈活的。一個組合函數僅依賴它的引數和 Vue 全域性匯出的 API,而不是依賴其微妙的 this 上下文。你可以將元件內的任何一段邏輯匯出為函數以複用它。
// 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:
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
<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對資料進行封裝,當資料變化時,觸發模板等內容的更新。
<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>
傳入一個 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
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用來監聽資料的變化,它會立即執行一次,之後會追蹤函數裡面用到的所有響應式狀態,當變化後會重新執行該回撥函數。
完全等效於 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])
})
把一個響應式物件轉換成普通物件,該普通物件的每個 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 可以用來為一個 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 值時使用 ref 或 reactive。
當使用響應式 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函數裡返回即可。