[TOC]
> [@vue/composition-api](https://github.com/vuejs/composition-api)
# 為什么使用組合API(Composition API)
在使用 Vue 2 時可能遇到三個限制:
* 隨著您的組件變得更大的可讀性變得越來越困難。
* 當前的代碼重用模式都具有缺點。
* Vue 2 提供了有限的 TypeScript 支 持。
我將詳細討論前兩個問題,這樣新 API 解決了什么問題就很明顯了。
# 大型組件是很難閱讀和維護的
首先我們來看一個查找商品的組件:

其源代碼如下:
```js
<script>
export default {
data() {
return {
// ...search data...
}
},
methods: {
// ...search methods...
}
}
</script>
```
當我們想添加其他一些數據或操作時,比如:添加一個搜索結果排序的操作!那么我們的代碼就會是這種形式:
```js
<script>
export default {
data() {
return {
// ...search data...
// ...sort data...
}
},
methods: {
// ...search methods...
// ...sort methods...
}
}
</script>
```
目前該代碼量還算好,如果我們還想要添加搜索過濾、結果分頁等功能時。我們就需要在該組件的選項(components、props、data、computed、methods 和 lifecycle methods)之間進行拆分,來填充功能代碼。如果我們使用顏色(下圖)將其可視化,您可以看到功能代碼將如何分解,這些使組件更加難以閱讀和解析哪些代碼與哪些功能相匹配:

查看上面?右圖,如果我們把某個功能的相關代碼都放在一起,就會?更加容易閱讀理解,也就更方便維護。查看我們原本的示例,如果通過 Composition API 來組織我們的代碼。那么情況就會像下面這樣:

Vue 3的 Composition API 的 `setup()`是新的語法。這種新語法完全是可選的,Vue 組件原本的標準方法仍然是完全有效的。
當您使用 Composition API 根據功能來組織組件時,我們會通過構成功能來對功能分組,然后會在`setup`方法中被調用。

現在,可以使用邏輯關注點(也稱為“功能”)來組織我們的組件。 但是,這并不意味著我們的用戶界面將由更少的組件組成。 我們仍然需要使用良好的組件設計模式來組織應用程序:

接下來我們討論 Vue 2的第二個限制!
# 沒有特別好的方式來重用組件的邏輯代碼
在 Vue 2 中有 3 種好的解決方案可以跨組件重用代碼,但是每種解決方案都有其局限性。 讓我們逐一介紹示例。 首先,有 Mixins。
## Mixins

1. 優點:
* 能按照功能組織代碼
2. 缺點:
* 它們容易發生沖突,并且您最終可能會遇到屬性名稱沖突。
* 如果 Mixins 存在相互作用,會不清楚它們是如何相互作用。
* 如果要在其他組件之間使用 Mixin,需要配置 Mixin,不方便重用。
## Mixin Factories
我們看一下 **Mixin Factories**,這是返回自定義版本的 Mixin 的函數。

如上圖所示,Mixin 工廠函數 允許我們通過傳入配置選項來自定義 Mixins。 現在,我們可以配置此代碼以在多個組件中使用。
1. 優點:
* 我們可以配置代碼,因此可以輕松重用。
* 關于 Mixins 如何進行交互,具有了更明確的關系。
2. 缺點:
* 命名間隔需要嚴格的慣例和紀律。
* 仍然有隱式的屬性添加,這意味著我們必須查看 Mixin 內部以找出它公開的屬性。
* 運行時沒有實例訪問權限,因此無法動態生成 Mixin 工廠。
## 作用域插槽
幸運的是,還有一種解決方案,作用域插槽(Scoped Slots):

1. 優點:
* 解決了 Mixins 的幾乎所有缺點。
2. 缺點:
* 您的配置最終出現在模板中,理想情況下,模板應僅包含我們要呈現的內容。
* 它們會增加模板的縮進量,從而降低可讀性。
* 公開的屬性僅在模板中可用。
* 由于我們使用的是3個組件而不是1個,因此性能有所降低。
如您所見,每種解決方案都有其局限性。 Vue 3的 Composition API 為我們提供了提取可重用代碼的第四種方式,該方式可能類似于:

現在,我們將使用組合 API 內部的函數來創建組件,這些函數將**在需要進行配置的`setup`方法中導入并使用**。
1. 優點:
* 編寫的代碼更少,因此將功能從組件中提取到函數中變得更加容易。
* 它建立在我們已有的技能上,而且我們早已熟悉了函數。
* 它比 Mixins 和作用域插槽更具靈活性,因為它們只是函數。
* 智能提示、自動完成和 typings 在很多代碼編輯器中已經可以使用了。
2. 缺點:
* 需要學習新的低級API來定義合成功能。
* 現在除了組件的標準語法外,還多了這種編寫組件的方法。
希望您現在清楚了組合 API 的由來。 在下一課中,我將深入探討組合組件的新語法。
# `setup` 和 響應式引用
首先,我們想弄清楚什么時候來使用它,然后我們將學習`setup`函數 和`Reactive References`以及`refs`。
> 免責聲明:如果你還不明白的話(If you haven’t caught on yet),Composition API 純粹是附加的,也沒有棄用什么。您可以像在 Vue 2 進行編碼一樣對 Vue 3 進行同樣編碼。
## 什么時候去使用 組合 API?
下面任何一種情況都可以:
1. 您需要最佳的 TypeScript 支持。
2. 組件太大了,需要根據功能劃分來組織!
3. 需要在其他組件中復用代碼。
4. 你和你的團隊更喜歡新的組合 API。
> 免責聲明:下面的示例很簡單,其實是不需要使用 Composition API 的!這里僅僅是為了方便學習而已!
先從普通 Vue 2 API 編寫非常簡單的組件開始,該組件在 Vue 3 中也是有效的。
```js
<template>
<div>Capacity: {{ capacity }}</div>
</template>
<script>
export default {
data() {
return {
capacity: 3
};
}
};
</script>
```
這里有個`capacity`屬性,它是響應式的。Vue 會獲取組件選項中`data`屬性所返回的每個屬性,并使他們成為響應式的屬性!當組件中的響應式屬性被改變時,組件就會被重新渲染。
## 使用`Setup`函數
我們使用 Vue 3 的組合 API 中 的`setup`函數:
```
<template>
<div>Capacity: {{ capacity }}</div>
</template>
<script>
export default {
setup() {
// more code to write
}
};
</script>
```
這個`setup`函數會在計算以下的任何選項之前,被執行:
* Components
* Props
* Data
* Methods
* Computed Properties
* Lifecycle methods
和其他組件選項不同,`setup`函數中,不能訪問`this`!所以為了獲得對組件屬性的訪問,方便操作,`setup`函數有兩個可選的參數。第一個參數是響應式的,而且能被監聽:
```
import { watch } from "vue";
export default {
props: {
name: String
},
setup(props) {
watch(() => {
console.log(props.name);
});
}
};
```
第二個參數是 `context`,可以獲取一堆有用的數據:
```
setup(props, context) {
context.attrs;
context.slots;
context.parent;
context.root;
context.emit;
}
```
回到我們的例子,我們現在需要一個響應式的引用。
## 響應式引用
```
<template>
<div>Capacity: {{ capacity }}</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const capacity = ref(3);
// additional code to write
}
};
</script>
```
上例中的`const capacity = ref(3);`創建了一個“響應式引用”。基本類型`3`被包裝到了一個對象中,方便我們跟蹤它的變化。先前的`data`中的`capacity`屬性已經被包裝在了一個對象中。
> 旁白:組合 API 允許我們聲明不與組件關聯的響應式基本類型,我們是這樣做的。
最后一步,我們需要明確的的返回包含需要被渲染的數據對象。
```
<template>
<div>Capacity: {{ capacity }}</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const capacity = ref(3);
return { capacity };
}
};
</script>
```
這個返回的對象是我們在`renderContext`中公開需要訪問哪些數據的方式。
像這樣明確表示有點冗長,但是故意為之的。 它有助于我們長期維護,因為我們可以控制暴露給模板的內容,并跟蹤定義模板屬性的位置。
## 將 Vue 3 與 Vue 2 一起使用
可以通過使用`@vue/composition-api`插件,將 Vue 3 Composition API 與 Vue 2一起使用。在 Vue 2 應用上安裝并配置它之后,您將使用上面的語法更改:
```
import { ref } from "vue";
```
改為:
~~~
import { ref } from "@vue/composition-api";
~~~
接下來,我們將學習如何使用這種新語法編寫組件。
# 方法
我們已經學會了如何創建響應式引用,那么組合組件的下一個構建塊就是創建方法。 這是我們當前的代碼:
```
<template>
<div>Capacity: {{ capacity }}</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const capacity = ref(3);
return { capacity };
}
};
</script>
```
如果我們要添加一個允許我們從按鈕增加`capacity`的方法,在常規組件語法中可以這樣寫:
```
methods: {
increase_capacity() {
this.capacity++;
}
}
```
但是,如何使用新的 Vue 3 Composition API 呢? 首先要在`setup`方法中定義一個函數,返回該方法使組件可以訪問它,然后在按鈕內使用它:
```
<template>
<div>
<p>Capacity: {{ capacity }}</p>
<button @click="increaseCapacity()">Increase Capacity</button>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const capacity = ref(3);
function increaseCapacity() { // <--- Our new function
// TBD
}
return { capacity, increaseCapacity };
}
};
</script>
```
當我們需要方法時,只需使用 Composition API 將它們創建為函數即可。但是,我們如何從`setup`方法內部增加`capacity`呢? 您可能會猜測:
```
function increaseCapacity() {
capacity++;
}
```
但是,`capacity`是一個響應式引用,一個包裝了我們整數的對象。 遞增操作一個對象肯定是錯誤的。 在本例中,我們需要遞增的是 響應式引用 封裝的內部整數`value`。 我們可以通過訪問`capacity.value`做到這一點:
```
function increaseCapacity() {
capacity.value++;
}
```
現在就可以了。 但是,如果查看模板,您會注意到在打印`capacity`時:
```
<p>Capacity: {{ capacity }}</p>
```
我們不必寫`capacity.value`,您可能想知道為什么。
事實證明,當 Vue 在模板中找到`ref`時,它會自動公開內部值,因此您無需在模板內調用`.value`。
# 計算屬性
學習使用新的 Composition API 語法創建計算屬性。 首先,需要將其添加到示例應用程序中,這樣就有了一個參加活動的人員列表。
```js
<template>
<div>
<p>Capacity: {{ capacity }}</p>
<button @click="increaseCapacity()">Increase Capacity</button>
<h2>Attending</h2>
<ul>
<li v-for="(name, index) in attending" :key="index">
{{ name }}
</li>
</ul>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const capacity = ref(4);
const attending = ref(["Tim", "Bob", "Joe"]); // <--- New Array
function increaseCapacity() {
capacity.value++;
}
return { capacity, attending, increaseCapacity };
}
};
</script>
```
現在我們有了一個 attending(參與者)的新數組,網頁展示如下:

要創建對計算屬性的需求,讓我們更改在模板中打印容量的方式:
```
<template>
<div>
<p>Spaces Left: {{ spacesLeft }} out of {{ capacity }}</p>
...
```
注意上面的`spacesLeft`,它將根據容量減去參加人數來顯示事件中剩余的空間數量。 使用常規組件語法創建計算屬性,它會是這樣:
```
computed: {
spacesLeft() {
return this.capacity - this.attending.length;
}
}
```
使用新的 Composition API 創建呢? 它會像這樣:
```
<template>
<div>
<p>Spaces Left: {{ spacesLeft }} out of {{ capacity }}</p>
<h2>Attending</h2>
<ul>
<li v-for="(name, index) in attending" :key="index">
{{ name }}
</li>
</ul>
<button @click="increaseCapacity()">Increase Capacity</button>
</div>
</template>
<script>
import { ref, computed } from "vue";
export default {
setup() {
const capacity = ref(4);
const attending = ref(["Tim", "Bob", "Joe"]);
const spacesLeft = computed(() => { // <-------
return capacity.value - attending.value.length;
});
function increaseCapacity() {
capacity.value++;
}
return { capacity, attending, spacesLeft, increaseCapacity };
}
};
</script>
```
如上面的代碼中看到的那樣,先從Vue API導入了`computed`,然后使用它,傳入一個匿名函數并將其設置為等于一個稱為`spacesLeft`的常量。 然后,從`setup`函數將其返回到對象中,使得模板可以訪問它。
## 定義可更改的計算屬性
`computed`可傳入`get`和`set`,用于定義可更改的計算屬性。
基本示例如下所示,與 Vue 2.x 類似的,可以定義可更改的計算屬性。
```
const count = ref(1);
const plusOne = computed({
get: () => count.value + 1,
set: val => { count.value = val - 1 }
});
plusOne.value = 1;
console.log(count.value); // 0
```
# 響應式語法
到目前為止,我們一直在使用響應式引用將 JavaScript 基本數據包裝裝在對象中以使其具有響應式。 但是,還有另一種方法可以將這些基本數據包裝在對象中。 就是使用`reactive`語法。
看下圖,左側是使用響應式引用的示例,右邊使用了替代的`reactive`語法。

如上右圖所示,我們創建了一個新的`event`常量,該常量接受一個普通的 JavaScript 對象并返回一個響應式對象。 這與在常規組件語法中使用`data`選項很相似,在常規組件語法中,我們還會發送一個對象。但是,如上面看到的,也可以將計算的屬性發送到該對象中。 還應該注意,使用這種語法時,在訪問屬性時不再需要編寫`.value`。 這是因為我們只是訪問`event`對象上的對象屬性。 您還應該注意,我們在`setup`函數的末尾返回了整個`event`對象。
請注意,這兩種語法都是完全有效的,不認為是“最佳實踐”。
為了使代碼正常工作,我們需要按以下方式更新模板代碼:
```
<p>Spaces Left: {{ event.spacesLeft }} out of {{ event.capacity }}</p>
<h2>Attending</h2>
<ul>
<li v-for="(name, index) in event.attending" :key="index">
{{ name }}
</li>
</ul>
<button @click="increaseCapacity()">Increase Capacity</button>
```
注意 我們現在是如何通過`event.`來訪問屬性的。
# 解構?
當我第一次看到以下代碼時:
```
return { event, increaseCapacity }
```
我在想是否可以通過其他方式來解構`event`對象,以便在模板中不必總是編寫`event.` ? 我更傾向于這樣書寫模板:
```
<p>Spaces Left: {{ spacesLeft }} out of {{ capacity }}</p>
<h2>Attending</h2>
<ul>
<li v-for="(name, index) in attending" :key="index">
{{ name }}
</li>
</ul>
<button @click="increaseCapacity()">Increase Capacity</button>
```
但是我如何解構`event`呢?我嘗試了下面兩種方式,都失敗了:
```
return { ...event, increaseCapacity };
return { event.capacity, event.attending, event.spacesLeft, increaseCapacity };
```
這些都不起作用,因為拆分該對象將刪除其響應式特性。 為了使這項工作有效,我們需要能夠將此對象拆分為**響應式引用**,以便能夠保持它的響應式特性。
## 介紹 `toRefs`
幸運的是,可以使用`toRefs`方法。 此方法將響應式對象轉換為普通對象,其中每個屬性都是指向原始對象上屬性的響應式引用。 這是我們使用此方法完成的代碼:
```
import { reactive, computed, toRefs } from "vue";
export default {
setup() {
const event = reactive({
capacity: 4,
attending: ["Tim", "Bob", "Joe"],
spacesLeft: computed(() => {
return event.capacity - event.attending.length;
})
});
function increaseCapacity() {
event.capacity++;
}
return { ...toRefs(event), increaseCapacity };
}
};
```
請注意,先導入`toRefs`,然后在`return`語句中使用它,然后解構該對象。 看起來這很棒吧!
## 旁白
組合 API 還提供`isRef`,用于檢查一個對象是否是`ref`對象:
~~~
const unwrapped = isRef(foo) ? foo.value : foo;
~~~
這里稍微提一下,如果代碼不需要同時在返回值中返回遞增容量的函數,那么可以簡單地編寫為:
```
return toRefs(event);
```
因為`setup`方法希望我們返回的是一個對象,而`toRefs`返回的也正是一個對象。(這肯定是一開始代碼設計者設計好的??)
# 模塊化
我們可能會使用組合 API 的兩個原因是**按功能組織組件并在其他組件之間重用我們的代碼**。 到目前為止,我們已經對代碼示例進行了深入探討,所以現在就開始做吧。 這是我們當前的代碼,請注意,我已改回使用**響應式引用**,該語法的內容對我來說似乎更干凈。
```
<template>
...
</template>
<script>
import { ref, computed } from "vue";
export default {
setup() {
const capacity = ref(4);
const attending = ref(["Tim", "Bob", "Joe"]);
const spacesLeft = computed(() => {
return capacity.value - attending.value.length;
});
function increaseCapacity() {
capacity.value++;
}
return { capacity, attending, spacesLeft, increaseCapacity };
}
};
</script>
```
## 將其提取到組合函數中
```
<template>
...
</template>
<script>
import { ref, computed } from "vue";
export default {
setup() {
return useEventSpace(); // <--- Notice I've just extracted a function
}
};
function useEventSpace() {
const capacity = ref(4);
const attending = ref(["Tim", "Bob", "Joe"]);
const spacesLeft = computed(() => {
return capacity.value - attending.value.length;
});
function increaseCapacity() {
capacity.value++;
}
return { capacity, attending, spacesLeft, increaseCapacity };
}
</script>
```
就是將所有代碼移到一個函數中,該函數不在`export default {}`的范圍內了。`setup()`方法現在成為我將組合函數綁定在一起的地方。
## 提取到文件以重用代碼
如果`useEventSpacep()`是想在多個組件中使用的一段代碼,那么我要做的就是將該函數提取到文件中,并使用導出默認值:
文件位置:**use/event-space.vue**:
```
import { ref, computed } from "vue";
export default function useEventSpace() {
const capacity = ref(4);
const attending = ref(["Tim", "Bob", "Joe"]);
const spacesLeft = computed(() => {
return capacity.value - attending.value.length;
});
function increaseCapacity() {
capacity.value++;
}
return { capacity, attending, spacesLeft, increaseCapacity };
}
```
我使用了名為`use`的文件夾來專門保存我的組合函數文件。當然你可以按你自己的規范命名。
`composables`或`hooks`也是畢竟好的名字。
現在,我的組件代碼只需要導入此組合函數并使用它。
```
<template>
...
</template>
<script>
import useEventSpace from "@/use/event-space";
export default {
setup() {
return useEventSpace();
}
};
</script>
```
## 添加另一個組合函數
如果我們還有另一個組合函數(可能在`use/event-mapping.js`中)來映射我們的事件,并且我們想在這里使用它,我們可以這樣寫:
```
<template>
...
</template>
<script>
import useEventSpace from "@/use/event-space";
import useMapping from "@/use/mapping";
export default {
setup() {
return { ...useEventSpace(), ...useMapping() }
}
};
</script>
```
如您所見,在各個組件之間共享組合函數非常簡單。 實際上,我可能會共享一些要發送給這些函數的數據,例如使用Vuex 從 API取得的事件數據。
# 生命周期鉤子
您可能對Vue生命周期掛鉤很熟悉,它使我們能夠在組件達到執行中的特定狀態時運行代碼。 讓我們回顧一下典型的生命周期鉤子:
* **beforeCreate-** 在初始化實例之后,在處理組件選項之前調用。
* **created -** 在創建實例之后調用。
* **beforeMount -** 在掛載 DOM 之前立即i調用。
* **mounted -** 在掛載實例時調用 (瀏覽器已經完成更新)
* **beforeUpdate -** 在響應式數據發生更改時,重新渲染 DOM 之前調用。
* **updated -** 當響應式數據發生更改并且 DOM 重新渲染后調用。
* **beforeDestroy -** 在 Vue 實例正好被銷毀之前調用。
* **destroyed -毀滅** 在 Vue 實例被銷毀后調用。
有兩種新的 Vue 2 生命周期方法,你可能不熟悉:
* **activated -** 用于當內部的一個組件被打開時。
* **deactivated -** 用于當內部的一個組件被關閉時。
* **errorCaptured -** 當捕獲來自任何子代組件的錯誤時調用。
更詳細的解釋請查 [生命周期鉤子](https://vuejs.org/v2/api/#Options-Lifecycle-Hooks)上的 API 文檔。
## 在 Vue 3 中的卸載
在 Vue 3 中,`beforeDestroy()`也可以編寫為`beforeUnmount()`,而`destroy()`可以編寫為`unmount()`。 當我向 Evan You 詢問這些更改時,他提到這只是更好的命名約定,因為 Vue 會掛載和卸載組件。
## 組合 API 的生命周期方法
在 Vue 3 的 Composition API 中,我們可以通過在`setup()`中創建回調,添加到生命周期方法名稱中:
```
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured
} from "vue";
export default {
setup() {
onBeforeMount(() => {
console.log("Before Mount!");
});
onMounted(() => {
console.log("Mounted!");
});
onBeforeUpdate(() => {
console.log("Before Update!");
});
onUpdated(() => {
console.log("Updated!");
});
onBeforeUnmount(() => {
console.log("Before Unmount!");
});
onUnmounted(() => {
console.log("Unmounted!");
});
onActivated(() => {
console.log("Activated!");
});
onDeactivated(() => {
console.log("Deactivated!");
});
onErrorCaptured(() => {
console.log("Error Captured!");
});
}
};
```
您可能會注意到缺少兩個鉤子。 [@vue/composition-api](https://github.com/vuejs/composition-api) 刪除了 `onBeforeCreate` 和 `onCreated`。因為`setup` 總是會在創建組件實例時被調用,`setup()`執行之前會立即調用`beforeCreate()`,而在`setup()`之后會立即調用`created()`,因此只使用`setup`即可。
## 兩種新的 Vue 3 生命周期方法
Vue 3 中還有另外兩個觀察者。Vue 2 Composition API 插件尚未實現這些觀察者(在我撰寫本文時),因此如果不使用 Vue 3 就無法使用它們。
* **onRenderTracked -** 在渲染期間,當響應式依賴在呈現函數中第一次被訪問時調用。現在將跟蹤此依賴項。這有助于查看正在跟蹤哪些依賴項,以便進行調試。
* **onRenderTriggered -** 觸發新的渲染時調用,允許您檢查是哪個依賴項觸發了組件的重新渲染。
高興期待看到這兩個鉤子在未來將會創建什么樣的優化工具。
# 監聽(Watch)
現在看一個使用組合 API 的簡單示例。 這里的代碼具有一個簡單的搜索輸入框,使用搜索文本來調用 API,并返回與輸入結果匹配的事件數。
```
<template>
<div>
Search for <input v-model="searchInput" />
<div>
<p>Number of events: {{ results }}</p>
</div>
</div>
</template>
<script>
import { ref } from "@vue/composition-api";
import eventApi from "@/api/event.js";
export default {
setup() {
const searchInput = ref("");
const results = ref(0);
results.value = eventApi.getEventCount(searchInput.value);
return { searchInput, results };
}
};
</script>
```
它好像沒有效果哦。 這是因為`results.value = eventApi.getEventCount(searchInput.value)` 僅中第一次運行`setup()`時被調用一次。 當`searchInput`被更新時,它便不會再次觸發。
## 解決辦法:Watch
要解決此問題,我們需要使用*監聽*。 這將在下一個刻度(the next tick)上運行我們的函數,同時以響應式的方式跟蹤其依賴關系,并在依賴項發生更改時會重新運行它。 像這樣:
```
setup() {
const searchInput = ref("");
const results = ref(0);
watch(() => {
results.value = eventApi.getEventCount(searchInput.value);
});
return { searchInput, results };
}
```
當它第一次運行時,使用了響應式來開始跟蹤`searchInput`,并且在`searchInput`更新時,它將重新進行 API 調用,然后更新`results`。 由于模板中使用了`results`,因此將重新渲染模板。
如果我想要更具體地監聽哪個源的更改,我可以在監視器(watcher)定義中指定它,像這樣
```
watch(searchInput, () => {
...
});
```
另外,如果我需要訪問被監視項的新值和舊值,我可以編寫:
```
watch(searchInput, (newVal, oldVal) => {
...
});
```
## 監聽多個項
如果我想觀察兩個響應式引用,我可以將它們發送到一個數組中:
```
watch([firstName, lastName], () => {
...
});
```
如果其中任何一個被更改,代碼將重新運行。 可以通過以下方式訪問它們的新值和舊值:
```
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
...
});
```
# 共享狀態(Sharing State)
現在使用 Composition API 來從組件中提取一些可重用的代碼。 在使用 API?? 調用時,很多時候我們可能希望圍繞調用構建許多代碼和功能。 具體來說就是加載狀態,錯誤狀態和try / catch塊。 讓我們看一下這段代碼,然后使用Composition API 正確地提取它。
**/src/App.js** 代碼示例:
```
<template>
<div>
Search for <input v-model="searchInput" />
<div>
<p>Loading: {{ loading }}</p>
<p>Error: {{ error }}</p>
<p>Number of events: {{ results }}</p>
</div>
</div>
</template>
<script>
import { ref, watch } from "@vue/composition-api";
import eventApi from "@/api/event.js";
export default {
setup() {
const searchInput = ref("");
const results = ref(null);
const loading = ref(false);
const error = ref(null);
async function loadData(search) {
loading.value = true;
error.value = null;
results.value = null;
try {
results.value = await eventApi.getEventCount(search.value);
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
watch(searchInput, () => {
if (searchInput.value !== "") {
loadData(searchInput);
} else {
results.value = null;
}
});
return { searchInput, results, loading, error };
}
};
</script>
```
## 操作共享狀態
這是在 Vue 應用程序中非常常見的模式,在該應用程序中我有一個 API 調用,我需要考慮結果,加載和錯誤狀態。 我如何提取它以使用 組合 API ? 首先,我可以創建一個新文件并提取常用功能。
**/composables/use-promise.js** 代碼示例:
~~~js
import { ref } from "@vue/composition-api";
export default function usePromise(fn) { // fn is the actual API call
const results = ref(null);
const loading = ref(false);
const error = ref(null);
const createPromise = async (...args) => { // Args is where we send in searchInput
loading.value = true;
error.value = null;
results.value = null;
try {
results.value = await fn(...args); // Passing through the SearchInput
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};
return { results, loading, error, createPromise };
}
~~~
請注意,此函數是如何保存響應式引用以及包裝 API 調用的函數,以及需要傳遞到 API 調用的所有參數的。 現在使用此代碼:
**/src/App.js** 代碼示例:
```
<template>
<div>
Search for <input v-model="searchInput" />
<div>
<p>Loading: {{ getEvents.loading }}</p>
<p>Error: {{ getEvents.error }}</p>
<p>Number of events: {{ getEvents.results }}</p>
</div>
</div>
</template>
<script>
import { ref, watch } from "@vue/composition-api";
import eventApi from "@/api/event.js";
import usePromise from "@/composables/use-promise";
export default {
setup() {
const searchInput = ref("");
const getEvents = usePromise(search =>
eventApi.getEventCount(search.value)
);
watch(searchInput, () => {
if (searchInput.value !== "") {
getEvents.createPromise(searchInput);
} else {
getEvents.results.value = null;
}
});
return { searchInput, getEvents };
}
};
</script>
```
上面就是所有代碼,并且我們獲得了前面示例的相同功能。
特別要注意的是,在我的`use-promise.js`文件中使用響應式狀態(加載,錯誤和結果)是多么容易,該文件在組件內部使用。 而且如果有另一個 API 調用時,就可以方便的使用`use-promise`了。
## 警告
當我讓 Vue 核心團隊的成員運行此操作時,他們提示有要注意`...getEvents`。 具體來說就是,我不應該破壞對象。 在不解構數據時,數據將在`getEvents`命名空間下進行訪問,這使它更易于封裝,并可以清楚地知道數據在組件中的位置。 看起來是這樣:
```js
<template>
<div>
Search for <input v-model="searchInput" />
<div>
<p>Loading: {{ getEvents.loading }}</p>
<p>Error: {{ getEvents.error }}</p>
<p>Number of events: {{ getEvents.results }}</p>
</div>
</div>
</template>
<script>
...
export default {
setup() {
...
return { searchInput, getEvents };
}
};
</script>
```
但是,當我在瀏覽器中運行時,得到以下結果:

似乎帶有組合 API 的 Vue 2 無法正確識別響應式引用并按應有的方式調用`.value`。 可以通過手動添加`.value`或使用 Vue 3版本來解決此問題。我用 Vue 3 測試了該代碼,它確實看到了響應引用,并正確地顯示了`.value`。
接下來,介紹 Vue 3 響應式引擎的一些新核心概念。
# 懸念(Suspense)
當編寫 Vue 應用程序的代碼時,會大量使用 API?? 調用來加載后端數據。 當用戶等待該 API 數據加載時,用戶界面最好是讓用戶知道數據此時正在加載。 如果用戶的互聯網連接速度較慢,則更加需要這樣做。
通常,在 Vue 中,我們在等待數據加載時使用了很多`v-if`和`v-else`語句來顯示一些 HTML,然后在數據加載后將其切換出來。 當我們有多個組件進行 API 調用時,事情會變得更加復雜,而我們希望等到所有數據加載完畢后再顯示頁面。
但是,Vue 3 帶有受 React 16.6 啟發的替代選項`Suspense`。 這樣,您就可以在顯示組件之前等待任何異步操作(例如進行數據 API 調用)完成。
`Suspense`是一個內置組件,我們可以使用它包裝兩個不同的模板,如下所示:
```
<template>
<Suspense>
<template #default>
<!-- Put component/components here, one or more of which makes an asychronous call -->
</template>
<template #fallback>
<!-- What to display when loading -->
</template>
</Suspense>
</template>
```
當`Suspense`加載時,它將首先嘗試渲染出在`<template #default>`中找到的內容。如果在任何時候找到了一個返回一個`promise`或異步組件(這是 Vue 3 的一個新功能)`setup`函數,那么它將渲染`<template #fallback>`直到所有的`promise`已經被解決了。
在這里您可以看到我正在加載`Event`組件:
```
<template>
<Suspense>
<template #default>
<Event />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
<script>
import useEventSpace from "@/composables/use-event-space";
export default {
async setup() {
const { capacity, attending, spacesLeft, increaseCapacity } = await useEventSpace();
return { capacity, attending, spacesLeft, increaseCapacity };
},
};
</script>
```
需要注意到:`setup()`方法被標記為`async`和`await useEventSpace()`。 顯然,`useEventSpace()`函數中有一個 API 調用,我將等待返回。
現在,當我加載頁面時,我會看到`loading …`消息,直到完成 API 調用承諾,然后顯示最終結果模板。
## 多個異步調用
`Suspense`的優點是可以進行多個異步調用,而`Suspense`將等待所有這些調用解決后才能顯示任何內容。 所以,如果我把:
```
<template>
<Suspense>
<template #default>
<Event />
<Event />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
```
注意有兩個`Event`嗎? `Suspense`將等待它們都解決,然后再顯示。
## 深度嵌套的異步調用
更強大的是,我可能具有一個嵌套嵌套的組件,該組件具有異步調用。`Suspense`將等待所有異步調用完成,然后再加載模板。 因此,您可以在應用程序上擁有一個加載屏幕,該屏幕等待應用程序的多個部分加載。
## 出現錯誤咋辦?
如果 API 調用無法正常運行,通常需要回退,因此我們需要某種錯誤展示以及加載展示。 幸運的是,`Suspense`語法允許您將其與舊的`v-if`一起使用,并且我們有一個新的`onErrorCaptured`生命周期掛鉤,可用于監聽錯誤:
```
<template>
<div v-if="error">Uh oh .. {{ error }}</div>
<Suspense v-else>
<template #default>
<Event />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
<script>
import Event from "@/components/Event.vue";
import { ref, onErrorCaptured } from "vue";
export default {
components: { Event },
setup() {
const error = ref(null);
onErrorCaptured((e) => {
error.value = e;
return true;
});
return { error };
},
};
</script>
```
注意`template`的`div`和`Suspense`標簽上的`v-else`。 還要注意`setup`方法中的`onErrorCaptured`回調。 從`onErrorCaptured`返回`true`可以防止錯誤進一步傳播。 這樣,我們的用戶就不會在其瀏覽器控制臺中看到錯誤。
## 創建骨架加載屏幕
使用`Suspense`標記使創建骨架加載屏幕之類的事情變得非常簡單。 就像這樣:
 
骨架將進入您的`<template #fallback>`中,而渲染出的 HTML 將進入您的`<template #default>`中。 真他媽的簡單??!
# Teleport
Vue 的組件體系結構使我們能夠將用戶界面構建到可以很好地組織業務邏輯和表示層的組件中。 但是,在某些情況下,一個組件中的某些 html 需要在其他位置呈現。 例如:
1. 需要固定或絕對定位和`z-index`的樣式。 例如,通常將 UI 組件(如模式)放置在標記之前,以確保將其正確放置在網頁的所有其他部分之前。
2. 當我們的 Vue 應用程序在網頁的一小部分(或窗口小部件)上運行時,有時我們可能希望將組件移至 Vue 應用程序之外的其他 DOM 位置中。
## 解決方案
Vue 3 提供的解決方案是**Teleport**組件。 以前,它被命名為“portal”,但名稱已更改為 Teleport,以免與將來的元素沖突,后者未來可能會成為 HTML 標準的一部分。 Teleport 組件允許我們指定模板 html(其中可能包含子組件),我們可以將其發送到 DOM 的另一部分中。
我將向您展示一些非常基本的用法,然后向您展示如何在更高級的方法中使用它。 首先,在 Vue CLI 生成的基本 Vue 應用程序之外添加 div 標簽:
**/public/index.html**:
```
...
<div id="app"></div>
<div id="end-of-body"></div>
</body>
</html>
```
然后,讓我們嘗試將一些文本從 Vue 應用程序內部傳送到該`#end-of-body` div 上。
**/src/App.vue**:
```
<template>
<teleport to="#end-of-body">
This should be at the end.
</teleport>
<div>
This should be at the top.
</div>
</template>
```
注意,在傳送線(teleport line)中,我們指定了要將模板代碼移動到的`div`,如果操作正確執行了,則頂部的文本應該移至底部。 果然:

## `Teleport`的`to`屬性
我們的`to`屬性只需要是一個有效的 DOM 查詢選擇器。 除了像我上面那樣使用`id`之外,還有另外三個示例:
1. **類選擇器**
~~~
<teleport to=".someClass">
~~~
2. **data 選擇器**
~~~
<teleport to="[data-modal]">
~~~
3. **動態選擇器**
如果需要,您甚至可以綁定動態選擇器,添加冒號
~~~
<teleport :to="reactiveProperty">
~~~
## 禁用狀態
模塊對話框和彈窗通常開始都是隱藏的,直到它們顯示在屏幕上為止。 因此,傳送具有`disabled`狀態,其中內容保留在原始組件內。 直到啟用傳送功能,它才會移動到目標位置。 讓我們更新代碼以能夠切換`showText`,如下所示:
```
<template>
<teleport to="#end-of-body" :disabled="!showText">
This should be at the end.
</teleport>
<div>
This should be at the top.
</div>
<button @click="showText = !showText">
Toggle showText
</button>
</template>
<script>
export default {
data() {
return {
showText: false
};
}
};
</script>
```
隨著切換,傳送內部的內容將從組件內部移動到組件外部。
如果我們實時檢查源,可以看到內容實際上正在從`DOM`中的一個地方移動到另一個地方。

## 自動保存狀態
當`teleport`從禁用變為啟用時,DOM 元素將被重用,因此它們完全保留現有的狀態。這可以通過傳送播放的視頻來說明。
```
<template>
<teleport to="#end-of-body" :disabled="!showText">
<video autoplay="true" loop="true" width="250">
<source src="flower.webm" type="video/mp4">
</video>
</teleport>
<div>
This should be at the top.
</div>
<button @click="showText = !showText">
Toggle showText
</button>
</template>
<script>
export default {
data() {
return {
showText: false
};
}
};
</script>
```
在實際效果中,你會看到視頻在 DOM 位置之間移動時的狀態保持不變。
## 隱藏文字
如果我們在`teleport`中擁有的內容是模態的內容,我們可能不希望在其激活之前將其顯示。 現在“This should be at the end.”,即使在`showText`為`false`的情況下也會中組件內部顯示。 我們可以通過添加`v-if`來禁止顯示。
這應該在最后。
```
<template>
<teleport to="#end-of-body" :disabled="!showText" v-if="showText">
This should be at the end.
</teleport>
...
```
現在,僅當`showText`為`true`時,文本才會被傳送到頁面底部顯示。
## 多個傳送到同一個地方
如果您將兩個相同內容的 DOM 傳送到同一位置時會發生什么? 我可以看到(尤其是使用模態)您可能想傳送多件事情。 先看一個簡單的示例,簡單地創建一個`showText2`。
```
<template>
<teleport to="#end-of-body" :disabled="!showText" v-if="showText">
This should be at the end.
</teleport>
<teleport to="#end-of-body" :disabled="!showText2" v-if="showText2">
This should be at the end too.
</teleport>
<div>
This should be at the top.
</div>
<button @click="showText = !showText">
Toggle showText
</button>
<button @click="showText2 = !showText2">
Toggle showText2
</button>
</template>
<script>
export default {
data() {
return {
showText: false,
showText2: false
};
}
};
</script>
```
可以看到:多個元素只是簡單的被添加到后面。例如先點擊“Toggle showText2”,然后再點擊“Toggle showText”的效果:

## 結論
`Teleport`提供了一種將代碼保留在同一組件中,但可以將代碼段移至頁面其他部分的方法。 可以將其用于模態窗口顯示(需要顯示在頁面其余部分的頂部,并置于`</body>`標簽的正上方)的解決方案。
有關更詳細的說明,請參閱[RFC](https://github.com/vuejs/rfcs/blob/rfc-portals/active-rfcs/0025-teleport.md)。
# 參考
> [VueMastery - Vue 3 Essentials](https://coursehunters.online/t/vuemastery-vue-3-essentials/2479)
> [0000-sfc-script-setup.md](https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md)
- Introduction
- Introduction to Vue
- Vue First App
- DevTools
- Configuring VS Code for Vue Development
- Components
- Single File Components
- Templates
- Styling components using CSS
- Directives
- Events
- Methods vs Watchers vs Computed Properties
- Props
- Slots
- Vue CLI
- 兼容IE
- Vue Router
- Vuex
- 組件設計
- 組件之間的通信
- 預渲染技術
- Vue 中的動畫
- FLIP
- lottie
- Unit test
- Vue3 新特性
- Composition API
- Reactivity
- 使用 typescript
- 知識點
- 附錄
- 問題
- 源碼解析
- 資源