請啟用 Javascript 查看內容

Vue 3 - Composition API

 ·  ☕ 9 分鐘

前言

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 函式提供的兩個參數來訪問:

  • props
  • context

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() 接受兩個參數,用來定義要傳遞的資料:

  • name 定義名稱(字串)
  • value 定義值
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 監聽

官方文件有完整說明與範例


竹白
作者
竹白
前端筆記

文章目錄