前言
Composition API(組合式 API) 是 Vue 3 的最大特點,為 Vue 元件的另一種編寫方式。
首先,來探討 Options API 遇到的問題。
Options API 結構:
1
2
3
4
5
6
7
8
9
|
export default {
props: {},
data() {
return {};
},
watch: {},
computed: {},
methods: {},
};
|
隨著專案複雜度提高,程式碼會不斷膨脹,後續的維護成本會逐漸增加,因此我們會想要將功能切割,提升程式碼的複用性及維護性。但 Options API 形成了一種強制的約定,導致邏輯分散在各個選項中,造成程式碼難以閱讀理解及分割,雖然有 mixins
,但大量使用時,容易出現命名衝突、來源不清晰等問題。
Composition API 改善的問題:
- 程式能依功能分類使用,增加可讀性;
- 封裝功能,可跨元件使用,增加複用性;
- 提供更好的 TypeScript 支持。
入口函式 Setup
全新的 setup
選項為一個函式,它會在元件實體尚未被建立之前執行,是使用 Composition API 實際位置。
setup
函式可以回傳一個物件,物件中的內容都將暴露給元件的其餘部分(計算屬性、方法、生命週期鉤子等等)以及元件的模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<script>
export default {
setup() {
const msg = 'hello!';
const handler = () => {
alert(msg);
};
return { msg, handler };
},
};
</script>
<template>
<div>
<p>{{ msg }}</p>
<button @click="handler">click</button>
</div>
</template>
|
CodePen Demo:Vue 3.0 - setup 選項
Composition API 可以選擇想要暴露的 property,不同於 Options API 全部都暴露給實體。另外,還可以透過解構重新命名,避免命名衝突的發生。
建立響應式資料
在 Vue 3 中,可以透過 reactive
方法建立響應式物件,或者透過 ref
方法建立響應式變數。
1. Reactive
reactive()
接受一個普通物件作為參數,回傳一個響應式的物件狀態。該響應式轉換是「深度轉換」,會影響巢狀物件傳遞的所有 property。
Vue 3 的響應式的物件是基於 ES6 的 Proxy 實現。而 Vue 2 是透過 ES5 的 Object.defineProperty()
實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<script>
import { reactive } from 'vue';
export default {
setup() {
const data = reactive({
msg: 'hello!',
});
const handler = () => {
alert(data.msg);
};
return { data, handler };
},
};
</script>
<template>
<div>
<p>{{ msg }}</p>
<div>
<input type="text" v-model="msg">
</div>
<button @click="handler">click</button>
</div>
</template>
|
CodePen Demo:Vue 3.0 - reactive 響應式物件
reactive()
相當於 Vue 2.6 新增的 Vue.observable()
API,為避免與 RxJS 中的 observables 混淆因此對其重新命名。
另外,data
選項回傳一個物件時,在內部也是透過 reactive()
使其成為響應式物件的。
2. Ref
ref()
接受一個任何型別的參數,回傳一個響應式且可變的 Ref 物件。Ref 物件只包含一個名為 value
的屬性。
若傳入物件型別,內部會呼叫 reactive()
將其轉成響應式物件。
從 setup
回傳的 Ref 物件在模板中訪問時是被自動解開的,因此不需要在模板中使用 .value
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<script>
import { ref } from 'vue';
export default {
setup() {
const msg = ref('hello!');
const handler = () => {
alert(msg.value); // 透過 .value 訪問值
};
return { msg, handler };
},
};
</script>
<template>
<div>
<p>{{ msg }}</p>
<div>
<input type="text" v-model="msg">
</div>
<button @click="handler">click</button>
</div>
</template>
|
CodePen Demo:Vue 3.0 - 帶 ref 的響應式變數
3. Ref 自動解構
如果將 Ref 物件分配給響應式物件 的 property 時,Ref 物件會自動解構。
1
2
3
4
5
6
7
8
9
10
11
|
import { ref, reactive } from 'vue';
const count = ref(1);
const data = reactive({});
data.count = count;
console.log(data.count); // 1
const obj = ref({});
obj.value.count = count;
console.log(obj.value.count); // 1
|
但要注意,只有物件會解構 Ref 物件,陣列不會。陣列給值仍然需要加 .value
。
1
2
3
4
5
6
7
8
|
import { ref, reactive } from 'vue';
const arr = ref([]);
arr.value[0] = count;
console.log(arr.value[0]); // Ref{}
console.log(arr.value[0].value); // 1
arr.value[1] = count.value;
console.log(arr.value[1]); // 1
|
CodePen Demo:Vue 3.0 - 自動解構
4. Reactive vs Ref
基本型別,肯定只能使用 ref()
宣告,那麼複雜型別呢?究竟是要使用 ref()
還是 reactive()
?
可以參考 VUE CONF 21 - VueUse 作者 Anthony Fu 分享可組合的 Vue。
Setup 參數
執行 setup
函式時,元件實體尚未被建立,在 setup
函式內的 this
不是實體的引用,若要訪問 props
、attrs
、slots
、emit
可以透過 setup
函式提供的兩個參數來訪問:
1. Props
props
為響應式物件,可以用來在 setup
函式中取得父元件傳遞的 prop。
1
2
3
4
5
6
7
|
export default {
props: ['msg'],
setup(props) {
console.log(props.msg);
},
// ...
};
|
toRefs
注意,如果想要使用 ES6 的解構 const { msg } = props
,會失去響應性。
因此 Vue 3.x 提供了 toRefs
方法。它會將響應式物件轉成普通物件並將每個 property 轉成指向原始物件相應 property 的 Ref 物件。
1
2
3
4
5
6
7
8
9
10
|
import { toRef } from 'vue';
export default {
props: ['msg', 'name'],
setup(props) {
const { name, msg } = toRefs(props);
console.log(name.value, msg.value);
},
// ...
};
|
CodePen Demo:Vue 3.x - toRefs
toRef
如果 prop 是可選的,那麼傳入的 props
可能會沒有不存在,使用 toRefs()
將無法得到 Ref 物件。這種情況下,可以改使用 toRef
方法處理。
toRef()
與 ref()
雖然都是用建立響應式資料,但 ref
會複製一份新的資料,而 toRef()
會保持對其源 property 的響應式連接。即使源 property 不存在,toRef()
也會回傳一個可用的 Ref 物件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import { toRef, toRefs } from 'vue';
export default {
props: {
msg: {
type: String,
},
name: {
type: String,
},
},
setup(props) {
const msg = toRef(props, 'msg');
console.log(msg); // Ref {}
const { name } = toRefs(props);
console.log(name); // undefined
},
// ...
};
|
CodePen Demo:Vue 3.x - toRef
2. Context
context
是一個普通的物件,它暴露三個元件的 property:
attrs
Attribute
slots
插槽
emit
觸發事件
context
不是響應式物件,可以安全地使用 ES6 解構:
1
2
3
4
5
|
export default {
setup(props, { attrs, slots, emit }) {
// ...
},
};
|
注意,attrs
和 slots
是有狀態的物件,會隨元件本身的更新而更新,要避免對它們進行解構。
計算 & 監聽
1. computed
基本用法
computed
方法接受一個 getter 函式,會回傳一個唯讀的響應式 Ref 物件。
1
2
3
4
5
6
7
8
|
import { ref, computed } from 'vue';
const count = ref(1);
const plusOne = computed(() => count.value + 1);
console.log(plusOne.value); // 2
plusOne.value++; // 錯誤
|
CodePen Demo:Vue 3.0 - computed
setter
或者,使用具有 get
和 set
函式的物件,並回傳一個可寫的響應式 Ref 物件。
1
2
3
4
5
6
7
8
9
10
11
12
|
import { computed } from 'vue';
const count = ref(1);
const plusOne = computed({
get: () => count.value + 1,
set: (newVal) => {
count.value = newVal - 1;
},
});
plusOne.value = 1;
console.log(count.value); // 0
|
CodePen Demo:Vue 3.0 - computed - setter 應用
2. watch
watch
API 與實體方法 $watch
完全等效。
1
|
watch(source, callback, [options])
|
基本用法
資料源可以是一個 getter 函式或 Ref 物件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { watch } form 'vue';
// 監聽一個 getter 函式
const state = reactive({ count: 0 });
watch(
() => state.count,
(newVal, oldVal) => {
// ...
},
);
// 監聽一個 ref
const count = ref(0);
watch(count, (newVal, oldVal) => {
// ...
});
|
第一種用法可以監聽響應式物件的 property,不需要監聽整個物件。
CodePen Demo:Vue 3.0 - watch
監聽多個資料源
還可以使用陣列同時監聽多個資料源:
1
2
3
4
5
6
7
8
|
import { watch } form 'vue';
const foo = ref('');
const bar = ref('');
watch([foo, bar], ([newFoo, newBar], [oldFoo, oldBar]) => {
// ...
});
|
選項
deep
:深度監聽物件或陣列。
immediate
:立即觸發 callback。
flush
:控制 callback 的時間。
'pre'
:預設值,渲染被呼叫
'post'
:渲染後呼叫
'sync'
:一旦值發生改變,callback 將同步呼叫。
3. watchEffect
watchEffect
是 Vue 3.x 新增的,比起 watch
它不需要指定監聽資料源,會自動收集依賴。watchEffect
初始化會執行一次用以收集依賴。
1
2
3
4
5
|
const count = ref(0);
watchEffect(() => {
console.log(count.value);
});
|
watchEffect
會回傳一個 WachStopHandle
函式,用來停止 watchEffect
。
1
2
3
4
5
|
const stop = watchEffect(() => {
// ...
});
stop();
|
生命週期鉤子
在 setup
函式中執行的程式,等同在 beforeCreate
和 created
階段執行,直接寫在 setup
函式內即可,其它生命週期鉤子需要透過 on
前綴的方法訪問。
Options API |
Hook inside setup |
beforeCreate |
- - - |
created |
- - - |
beforeMount |
onBeforeMount |
mounted |
onMounted |
beforeUpdate |
onBeforeUpdate |
updated |
onUpdated |
beforeUnmount |
onBeforeUnmount |
unmounted |
onUnmounted |
errorCaptured |
onErrorCaptured |
renderTracked |
onRenderTracked |
renderTriggered |
onRenderTriggered |
生命週期函式接收一個 callback
作為參數,當鉤子被元件呼叫時,callback 就會被執行。
1
2
3
4
5
6
7
8
9
10
|
import { onMounted } from 'vue';
export default {
setup() {
// mounted
onMounted(() => {
// ...
});
},
};
|
Provide / Inject
在 Composition API 中使用 Provide / Inject 依賴注入。
1. Provide
在 setup()
中使用 provide
時,需要使用到 provide
方法。
provide()
接受兩個參數,用來定義要傳遞的資料:
1
2
3
4
5
6
7
|
import { provide } from 'vue';
export default {
setup() {
provide('count', 1);
},
};
|
2. Inject
在 setup()
中使用 inject
時,需要使用到 inject
方法。
provide()
接受兩個參數,回傳注入的資料:
name
資料名稱(字串)
value
預設值(可選)
1
2
3
4
5
6
7
8
|
import { inject } from 'vue';
export default {
setup() {
const count = inject('count', 0);
return { count };
},
};
|
3. 響應性
provide/inject
綁定的值預設並不是響應式的。我們可以通過傳遞響應式資料來處理 provide/inject
之間的響應性。
可以使用 reactive
傳遞響應式物件,或者使用 ref
傳遞 Ref 物件。
1
2
3
4
5
6
7
8
|
import { ref, provide } from 'vue';
export default {
setup() {
const count = ref(1);
provide('count', count);
},
};
|
4. 操作響應式
provide/inject
不像 Vuex 可以追蹤狀態改變,如果資料狀態操作分散在多個後代元件中,會很難維護。
因此,建議盡可能在定義 provide
的元件上操作 響應式的 provide/inject
資料。
若一定要在注入資料的元件內操作。這種情況下,建議多定義一個負責操作的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import { ref, provide } from 'vue';
export default {
setup() {
const count = ref(1);
const addCount = ()=> {
count.value += 1;
};
provide('count', count);
provide('addCount', addCount);
},
};
|
1
2
3
4
5
6
7
8
9
|
import { inject } from 'vue';
export default {
setup() {
const count = inject('count', 0);
const addCount = inject('addCount');
return { count, addCount };
},
};
|
如果要確保 provide
傳遞的資料不被後代元件更改,可以使用 readonly
或者 computed
方法建立唯讀資料。
模板引用
1. Composition API
在 Composition API 中,只需要使用 ref()
宣告一個與 ref
attribute 相同名稱的變數、內容值為 null
,並將它回傳,即可獲取 DOM。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import { ref, onMounted } from 'vue';
export default {
setup() {
const input = ref(null);
onMounted(()=>{
const dom = input.value;
dom.focus();
});
return { input };
},
};
|
CodePen Demo:Vue 3.x - Composition API 模板引用
2. v-for 的處理
v-for
模板引用 Vue 2.x 與 Vue 3.x 的差異可以參考 模板指令:v-for 中的 Ref 陣列。
Composition API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<template>
<div v-for="n of nums" :ref="setDoms">{{ n }}</div>
<button type="button" @click="nums.push(nums.length + 1)">+</button>
</template>
<script>
console.clear();
import { ref, onMounted, onBeforeUpdate, onUpdated } from 'vue'
export default {
setup() {
const nums = ref([1, 2, 3]);
const doms = ref([]);
const setDoms = (el) => {
if (el) {
doms.value.push(el);
}
};
onMounted(() => {
console.log(doms.value);
});
onBeforeUpdate(() => {
doms.value = [];
});
onUpdated(() => {
console.log(doms.value);
});
return { nums, setDoms };
},
};
</script>
|
CodePen Demo:Vue 3.0 - 模板引用 v-for 的處理
邏輯拆分
1. 應用範例
Composition API 最主要的目的就是將邏輯功能拆分出來,並在入口函式 setup
中組合使用。
假設要實現一個色系切換的功能
透過 JavaScript 判斷目前系統主題色是否為深色,需要用到 Window.matchMedia()
和 prefers-color-scheme
:
1
2
3
4
5
6
7
8
9
|
// 判斷當前系統主題色是否為深色
const prefersColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
let preferredDark = prefersColorSchemeMedia.matches;
// 監聽系統主題色是否有改變
const handler = (event) => {
preferredDark = event.matches;
};
prefersColorSchemeMedia.addEventListener('change', handler);
|
Vue Options API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
export default {
data() {
return {
preferredDark: false,
prefersColorSchemeMedia: undefined,
};
},
created() {
this.prefersColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
this.preferredDark = this.prefersColorSchemeMedia.matches;
this.prefersColorSchemeMedia.addEventListener('change', this.prefersColorSchemeUpdate);
},
unmounted() {
this.prefersColorSchemeMedia.removeEventListener('change', this.prefersColorSchemeUpdate);
},
methods: {
prefersColorSchemeUpdate(event) {
this.preferredDark = event.matches;
},
},
};
|
Options API 若需要對其進行抽離,會使用 mixins 方式,但 mixins 的缺點很明顯,就是如果引入大量的 mixins 會不知道變數來自哪個,且存在命名稱衝突的問題。
Vue Composition API,可以讓我們將功能脫離元件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// usePreferredDark.js
import { ref, onUnmounted } from 'vue';
export default function usePreferredDark() {
const preferredDark = ref(false);
const prefersColorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
preferredDark.value = prefersColorSchemeMedia.matches;
const handler = (event) => {
preferredDark.value = event.matches;
};
prefersColorSchemeMedia.addEventListener('change', handler);
onUnmounted(() => {
prefersColorSchemeMedia.removeEventListener('change', handler);
});
return preferredDark;
}
|
在元件中使用:
1
2
3
4
5
6
7
|
import usePreferredDark from './usePreferredDark.js';
export default {
setup() {
const isDark = usePreferredDark();
},
};
|
2. Composition API 的實用工具集合:VueUse
VueUse
VueUse 是一個 Vue Composition API 的實用工具集合。
本文撰寫時,github 上已擁有 3.7k 以上的 star。
特點:
- 互動式的 Docs 和 Demos。
- Vue 2 和 Vue 3 都支持。
- tree shaking 結構,只會打包引入的程式碼。
- 使用 TypeScript 編寫,帶有 TS 檔案。
- 可通過 CDN 使用。
- 可配置事件過濾器和目標。
- 支持各種套件,例如 Router、Firebase、RxJS 等。
CDN:
1
2
3
|
<!-- shared 一定要在前-->
<script src="https://unpkg.com/@vueuse/shared"></script>
<script src="https://unpkg.com/@vueuse/core"></script>
|
使用 window.VueUse
:
1
2
3
4
5
6
7
8
9
10
|
const { useMouse } = VueUse;
const app = Vue.createApp({
setup() {
// tracks mouse position
const { x, y } = useMouse();
return { x, y };
},
});
app.mount('#app');
|
CodePen Demo:VueUse Demo - Vue3
安裝:
# npm
npm i @vueuse/core
# yarn
yarn add @vueuse/core
從 @vueuse/core
引入:
1
2
3
4
5
6
7
8
9
10
|
import { useMouse } from '@vueuse/core';
export default {
name: 'App',
setup() {
// tracks mouse position
const { x, y } = useMouse();
return { x, y };
},
};
|
核心功能:
- Animation 動畫
- Browser 瀏覽器
- Component 元件
- Formatters 格式化
- Misc 其他
- Sensors 感測器
- State 狀態
- Utilities 實用工具
- Watch 監聽
官方文件有完整說明與範例