在這篇文章中,我將講講 Vue 的 Composition API 為什麼比之前的 Options API 要好,以及它是如何工作的。
Options API 有什麼問題
首先,這裡不是要大家放棄 Options API,如果你覺得 Options API 還不錯,已經習慣了,就繼續使用它。但我希望你能明白為什麼 Composition API 是一種更好的選擇。
當我剛開始接觸 Vue 時,我喜歡它用匿名物件來構建元件的方式。簡單易學,也很自然。但隨著使用的時間越長,就會遇到一些很奇怪的問題。比如下面這段程式碼:
export default {
mounted: async function () {
this.load().then(function () {
this.name = 'Foo'
})
},
}
這裡的第二個this
的問題我想很多人都遇到過。在不除錯的情況下,很難知道兩個this
為什麼不一樣。我們只知道要解決這個問題,反正得用箭頭函式,例如:
mounted: async function () {
this.load().then(() => {
this.name = 'Foo';
});
}
此外,從data
函式返回成員後,Vue 會包裝它們(通常使用Proxy
),以便能夠監聽物件的修改。但如果你嘗試直接替換物件(比如陣列),就可能監聽不到物件修改了。比如:
export default {
data: () => {
return {
items: [],
}
},
mounted: async function () {
this.load().then((result) => {
this.items = result.data
})
},
methods: {
doSomeChange() {
this.items[0].foo = 'foo'
},
},
}
這是 Vue 最常見的問題之一。儘管可以很簡單地解決這些問題,但畢竟給學習 Vue 工作原理造成了障礙。
最後,Options API 很難實現元件間的功能共享,為了解決這個問題,Vue 使用了mixins
的概念。例如,你可以建立一個 mixin 來擴充套件元件,如下所示:
export default {
data: function () {
return { items: [] };
},
methods: {
...
}
}
mixin 看起來很像 Options API。它只是對物件進行合併,以允許你新增到特定元件上:
import dataService from "./dataServiceMixin";
export default {
mixins: [dataService],
...
mixin 最大問題是名稱衝突。
鑑於 Options API 這些不足,Composition API 誕生了。
瞭解 Composition API
現在來介紹一下 Composition API。Vue 2 也可以使用 Composition API。首先,你需要引入 Composition API 庫:
> npm i -s @vue/composition-api
要啟用 Composition API,你只需在main.js/ts
中註冊一下:
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
讓我們來寫一個 Composition API 元件。首先,元件仍然是匿名物件:
export default {
setup() {
return {}
},
}
Composition API 的關鍵就是這個setup
方法,所有所需的資料都從setup
中返回。這非常類似於 Options API 的data
方法:
export default {
setup() {
const name = 'Foo'
return { name }
},
}
與其只在return
中賦值,不如在setup
裡面用一個區域性變數建立。為什麼要這樣做呢?因為 JavaScript 的閉包。讓我們擴充套件一下,再寫一個函式。
export default {
setup() {
const name = 'Foo'
function save() {
alert(`Name: ${name}`)
}
return { name, save }
},
}
該函式可以訪問name
,因為它們在同一個作用域內。這就是 Composition API 的神奇之處。沒有魔術物件,只有 JavaScript。這個模式可以支援更復雜的場景,更靈活,這一切的基礎只是閉包,僅此而已。
在這個例子中name
是永遠不會改變的。為了讓 Vue 能夠處理繫結,它需要知道name
的變化。在這個例子中,如果你在save()
中修改了程式碼來改變名稱,它不會反映在 UI 中:
export default {
setup() {
const name = 'Foo'
function save() {
name = name + ' saved'
alert(`Name: ${name}`)
}
return { name, save }
},
}
響應式
要使物件具有響應式,Vue 提供了兩種方法:ref
和reactive
包裝器。
你可以將name
包裝成一個ref
物件:
import { ref } from '@vue/composition-api'
export default {
setup() {
const name = ref('Foo')
function save() {
name.value = name.value + ' saved'
alert(`Name: ${name.value}`)
}
return { name, save }
},
}
這是一個簡單的響應式,ref 物件的 value 屬性才是實際的值。
這與簡單物件可以很好地配合,但是具有自身狀態的物件(例如類或陣列),對值的更改是不夠的。你真正需要的是使用一個 proxy 函式以便監聽該物件內部發生的任何更改。
import { ref, reactive } from '@vue/composition-api'
export default {
setup() {
const name = ref('Foo')
const items = reactive([])
function save() {
// Change Array
items.splice(0, items.length)
name.value = name.value + ' saved'
alert(`Name: ${name.value}`)
}
return { name, save, items }
},
}
在此示例中,使用了splice
更改陣列,Vue 需要了解此更改。你可以通過使用另一個稱為reactive
的包裝器包裝該物件(在本例中為陣列)來實現。
Composition API 中的 reactive 包裝器與 Vue2 的 Vue.observable 包裝器相同。
ref 和 react 之間的區別在於,reactive 用 proxy 包裝物件,而 ref 是簡單的值包裝器。
因此,你不需要將返回的 reactive 物件再包裝,不需要像 ref 物件一樣通過 value 屬性訪問其值。
一旦有了 ref 和 reactive 物件,就可以觀察更改。有兩種方法可以做到這一點:watch
和watchEffect
。watch 允許你監聽單個物件的更改:
setup() {
const name = ref("Shawn");
watch(() => name, (before, after) => {
console.log("name changes");
});
return { name };
},
該watch
函式有兩個引數:將資料返回到watch
的回撥以及發生更改時要呼叫的回撥。它提供兩個引數分別是修改前的值before
和修改後的值after
。
或者,你可以使用watchEffect
監聽 reactive 物件的任何更改。它只需要一個回撥:
watchEffect(() => {
console.log('Ch..ch...ch...changes.')
})
組合元件
有時候你希望把一個已有元件的功能組合到另一個元件,以便程式碼重用和資料互動。當組合元件時,Composition API 只是使用作用域和閉包解決問題。例如,你可以建立一個簡單的 service 服務:
import axios from 'axios'
export let items = []
export async function load() {
let result = await axios.get(API_URL)
items.splice(0, items.length, ...result.data)
}
export function removeItem(item) {
let index = items.indexOf(item)
if (index > -1) {
items.splice(index, 1)
}
}
然後你可以按需將需要的功能(物件或函式) import 到你的元件中:
import { ref, reactive, onMounted } from '@vue/composition-api'
import { load, items, removeItem } from './dataService'
export default {
setup() {
const name = ref('Foo')
function save() {
alert(`Name: ${this.name}`)
}
return {
load, // from import
removeItem, // from import
name,
save,
items, // from import
}
},
}
你可以使用 Composition API 更明顯式地組合你的元件。接下來我們來談談元件的使用。
使用 Props
在 Composition API 中定義 Props 的方式與 Options API 相同:
export default {
name: 'WaitCursor',
props: {
message: {
type: String,
required: false,
},
isBusy: {
type: Boolean,
required: false,
},
},
setup() {},
}
你可以通過向setup
新增可選引數來訪問 Props:
setup(props) {
watch(() => props.isBusy, (b,a) => {
console.log(`isBusy Changed`);
});
}
此外,你可以新增第二個引數,可以訪問emit
,slots
和attrs
物件,和 Options API 上 this 指標上的物件對應:
setup(props, context) {
watch(() => props.isBusy, (b,a) => context.emit("busy-changed", a));
}
使用元件
在 Vue 中有兩種使用元件的方法。你可以全域性註冊元件(用作通用元件),以便可以在任何地方使用它們:
import WaitCursor from './components/WaitCursor'
Vue.component('wait-cursor', WaitCursor)
通常,你可以將元件新增到正在使用的特定元件中。在 Composition API 中,與 Options API 相同:
import WaitCursor from './components/waitCursor'
import store from './store'
import { computed } from '@vue/composition-api'
export default {
components: {
WaitCursor, // Use Component
},
setup() {
const isBusy = computed(() => store.state.isBusy)
return { isBusy }
},
}
在 Composition API 中指定了元件後,就可以使用它:
<div>
<WaitCursor message="Loading..." :isBusy="isBusy"></WaitCursor>
<div class="row">
<div class="col">
<App></App>
</div>
</div>
</div>
使用元件的方式與 Options API 中的方式沒有什麼不同。
在 Vue 3 中使用 Composition API
如果你使用的是 Vue 3,則無需單獨引用 Composition API。Vue 3 已經整合好了。該@vue/composition-api
庫僅用於 Vue 2 專案。Vue 3 的真正變化是,當你需要匯入 Composition API 時,只需要直接從 Vue 獲取它們:
import {
ref,
reactive,
onMounted,
watch,
watchEffect,
//from "@vue/composition-api";
} from 'vue'
其他一切都一樣。只需從“ vue”匯入即可。在 Vue 3 中,使用 Composition API 只是簡單一些,因為它是預設行為。
Vue 3 的主要目標之一是改善 TypeScript 體驗,所有內容都有型別庫。但是要增加型別安全性,你必須進行一些小的更改才能使用 TypeScript。在建立元件時,需使用defineComponent
:
import { defineComponent, reactive, onMounted, ref } from "vue";
import Customer from "@/models/Customer";
export default defineComponent({
name: "App",
setup() {
const customers = reactive([] as Array<Customer>);
const customer = ref(new Customer());
const isBusy = ref(false);
const message = ref("");
return {
customers, customer, isBusy, message
}
}
});
在這些示例中,變數被推斷為型別例項(例如,Customers
物件是Reactive<Customer[]>
型別的例項)。此外,使用強型別可以降低傳遞錯誤資料的機會。當然,如果你使用 TypeScript(尤其是在 Visual Studio 或 VS Code 中),則會有非常友好的智慧提示。