請啟用 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 不是實體的引用,若要訪問 propsattrsslotsemit 可以透過 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 }) {
    // ...
  },
};

注意,attrsslots 是有狀態的物件,會隨元件本身的更新而更新,要避免對它們進行解構。

計算 & 監聽

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

或者,使用具有 getset 函式的物件,並回傳一個可寫的響應式 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 函式中執行的程式,等同在 beforeCreatecreated 階段執行,直接寫在 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 監聽

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


竹白
作者
竹白
前端筆記

文章目錄