提示
在閱讀文檔之前,你應(yīng)該已經(jīng)熟悉了這兩個(gè) Vue 基礎(chǔ)和創(chuàng)建組件。
在 Vue Mastery 上觀看關(guān)于組合式 API 的免費(fèi)視頻。
通過創(chuàng)建 Vue 組件,我們可以將接口的可重復(fù)部分及其功能提取到可重用的代碼段中。僅此一項(xiàng)就可以使我們的應(yīng)用程序在可維護(hù)性和靈活性方面走得更遠(yuǎn)。然而,我們的經(jīng)驗(yàn)已經(jīng)證明,光靠這一點(diǎn)可能是不夠的,尤其是當(dāng)你的應(yīng)用程序變得非常大的時(shí)候——想想幾百個(gè)組件。在處理如此大的應(yīng)用程序時(shí),共享和重用代碼變得尤為重要。
假設(shè)在我們的應(yīng)用程序中,我們有一個(gè)視圖來顯示某個(gè)用戶的倉庫列表。除此之外,我們還希望應(yīng)用搜索和篩選功能。處理此視圖的組件可能如下所示:
// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// 使用 `this.user` 獲取用戶倉庫
}, // 1
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
該組件有以下幾個(gè)職責(zé):
searchQuery
字符串搜索存儲庫filters
對象篩選倉庫
用組件的選項(xiàng) (data
、computed
、methods
、watch
) 組織邏輯在大多數(shù)情況下都有效。然而,當(dāng)我們的組件變得更大時(shí),邏輯關(guān)注點(diǎn)的列表也會增長。這可能會導(dǎo)致組件難以閱讀和理解,尤其是對于那些一開始就沒有編寫這些組件的人來說。
一個(gè)大型組件的示例,其中邏輯關(guān)注點(diǎn)是按顏色分組。
這種碎片化使得理解和維護(hù)復(fù)雜組件變得困難。選項(xiàng)的分離掩蓋了潛在的邏輯問題。此外,在處理單個(gè)邏輯關(guān)注點(diǎn)時(shí),我們必須不斷地“跳轉(zhuǎn)”相關(guān)代碼的選項(xiàng)塊。
如果我們能夠?qū)⑴c同一個(gè)邏輯關(guān)注點(diǎn)相關(guān)的代碼配置在一起會更好。而這正是組合式 API 使我們能夠做到的。
既然我們知道了為什么,我們就可以知道怎么做。為了開始使用組合式 API,我們首先需要一個(gè)可以實(shí)際使用它的地方。在 Vue 組件中,我們將此位置稱為 setup
。
setup
組件選項(xiàng)觀看 Vue Mastery 上的免費(fèi) setup 視頻。
新的 setup
組件選項(xiàng)在創(chuàng)建組件之前執(zhí)行,一旦 props
被解析,并充當(dāng)合成 API 的入口點(diǎn)。
WARNING
由于在執(zhí)行 setup
時(shí)尚未創(chuàng)建組件實(shí)例,因此在 setup
選項(xiàng)中沒有 this
。這意味著,除了 props
之外,你將無法訪問組件中聲明的任何屬性——本地狀態(tài)、計(jì)算屬性或方法。
setup
選項(xiàng)應(yīng)該是一個(gè)接受 props
和 context
的函數(shù),我們將在稍后討論。此外,我們從 setup
返回的所有內(nèi)容都將暴露給組件的其余部分 (計(jì)算屬性、方法、生命周期鉤子等等) 以及組件的模板。
讓我們添加 setup
到我們的組件中:
// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
setup(props) {
console.log(props) // { user: '' }
return {} // 這里返回的任何內(nèi)容都可以用于組件的其余部分
}
// 組件的“其余部分”
}
現(xiàn)在讓我們從提取第一個(gè)邏輯關(guān)注點(diǎn)開始 (在原始代碼段中標(biāo)記為“1”)。
- 從假定的外部 API 獲取該用戶名的倉庫,并在用戶更改時(shí)刷新它
我們將從最明顯的部分開始:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
// 在我們的組件內(nèi)
setup (props) {
let repositories = []
const getUserRepositories = async () => {
repositories = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories // 返回的函數(shù)與方法的行為相同
}
}
這是我們的出發(fā)點(diǎn),但它還不能工作,因?yàn)槲覀兊?repositories
變量是非響應(yīng)式的。這意味著從用戶的角度來看,倉庫列表將保持為空。我們來解決這個(gè)問題!
ref
的響應(yīng)式變量
在 Vue 3.0 中,我們可以通過一個(gè)新的 ref
函數(shù)使任何響應(yīng)式變量在任何地方起作用,如下所示:
import { ref } from 'vue'
const counter = ref(0)
ref
接受參數(shù)并返回它包裝在具有 value
property 的對象中,然后可以使用該 property 訪問或更改響應(yīng)式變量的值:
import { ref } from 'vue'
const counter = ref(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
在對象中包裝值似乎不必要,但在 JavaScript 中保持不同數(shù)據(jù)類型的行為統(tǒng)一是必需的。這是因?yàn)樵?JavaScript 中,Number
或 String
等基本類型是通過值傳遞的,而不是通過引用傳遞的:
在任何值周圍都有一個(gè)包裝器對象,這樣我們就可以在整個(gè)應(yīng)用程序中安全地傳遞它,而不必?fù)?dān)心在某個(gè)地方失去它的響應(yīng)性。
提示
換句話說,ref
對我們的值創(chuàng)建了一個(gè)響應(yīng)式引用。使用引用的概念將在整個(gè)組合式 API 中經(jīng)常使用。
回到我們的例子,讓我們創(chuàng)建一個(gè)響應(yīng)式的 repositories
變量:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
// in our component
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
}
完成!現(xiàn)在,每當(dāng)我們調(diào)用 getUserRepositories
時(shí),repositories
都將發(fā)生變化,視圖將更新以反映更改。我們的組件現(xiàn)在應(yīng)該如下所示:
// src/components/UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'
import { ref } from 'vue'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
return {
repositories,
getUserRepositories
}
},
data () {
return {
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
我們已經(jīng)將第一個(gè)邏輯關(guān)注點(diǎn)中的幾個(gè)部分移到了 setup
方法中,它們彼此非常接近。剩下的就是在 mounted
鉤子中調(diào)用 getUserRepositories
,并設(shè)置一個(gè)監(jiān)聽器,以便在 user
prop 發(fā)生變化時(shí)執(zhí)行此操作。
我們將從生命周期鉤子開始。
setup
為了使組合式 API 的特性與選項(xiàng)式 API 相比更加完整,我們還需要一種在 setup
中注冊生命周期鉤子的方法。這要?dú)w功于從 Vue 導(dǎo)出的幾個(gè)新函數(shù)。組合式 API 上的生命周期鉤子與選項(xiàng)式 API 的名稱相同,但前綴為 on
:即 mounted
看起來像 onMounted
。
這些函數(shù)接受在組件調(diào)用鉤子時(shí)將執(zhí)行的回調(diào)。
讓我們將其添加到 setup
函數(shù)中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'
// in our component
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}
onMounted(getUserRepositories) // on `mounted` call `getUserRepositories`
return {
repositories,
getUserRepositories
}
}
現(xiàn)在我們需要對 user
prop 所做的更改做出反應(yīng)。為此,我們將使用獨(dú)立的 watch
函數(shù)。
watch
響應(yīng)式更改
就像我們?nèi)绾问褂?watch
選項(xiàng)在組件內(nèi)的 user
property 上設(shè)置偵聽器一樣,我們也可以使用從 Vue 導(dǎo)入的 watch
函數(shù)執(zhí)行相同的操作。它接受 3 個(gè)參數(shù):
下面讓我們快速了解一下它是如何工作的
import { ref, watch } from 'vue'
const counter = ref(0)
watch(counter, (newValue, oldValue) => {
console.log('The new counter value is: ' + counter.value)
})
例如,每當(dāng) counter
被修改時(shí) counter.value=5
,watch 將觸發(fā)并執(zhí)行回調(diào) (第二個(gè)參數(shù)),在本例中,它將把 'The new counter value is:5'
記錄到我們的控制臺中。
以下是等效的選項(xiàng)式 API:
export default {
data() {
return {
counter: 0
}
},
watch: {
counter(newValue, oldValue) {
console.log('The new counter value is: ' + this.counter)
}
}
}
有關(guān) watch
的詳細(xì)信息,請參閱我們的深入指南。
現(xiàn)在我們將其應(yīng)用到我們的示例中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs } from 'vue'
// 在我們組件中
setup (props) {
// 使用 `toRefs` 創(chuàng)建對prop的 `user` property 的響應(yīng)式引用
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// 更新 `prop.user` 到 `user.value` 訪問引用值
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// 在用戶 prop 的響應(yīng)式引用上設(shè)置一個(gè)偵聽器
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
你可能已經(jīng)注意到在我們的 setup
的頂部使用了 toRefs
。這是為了確保我們的偵聽器能夠?qū)?user
prop 所做的更改做出反應(yīng)。
有了這些變化,我們就把第一個(gè)邏輯關(guān)注點(diǎn)移到了一個(gè)地方。我們現(xiàn)在可以對第二個(gè)關(guān)注點(diǎn)執(zhí)行相同的操作——基于 searchQuery
進(jìn)行過濾,這次是使用計(jì)算屬性。
computed
屬性
與 ref
和 watch
類似,也可以使用從 Vue 導(dǎo)入的 computed
函數(shù)在 Vue 組件外部創(chuàng)建計(jì)算屬性。讓我們回到我們的 counter 例子:
import { ref, computed } from 'vue'
const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)
counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
在這里,computed
函數(shù)返回一個(gè)作為 computed
的第一個(gè)參數(shù)傳遞的 getter 類回調(diào)的輸出的一個(gè)只讀的響應(yīng)式引用。為了訪問新創(chuàng)建的計(jì)算變量的 value,我們需要像使用 ref
一樣使用 .value
property。
讓我們將搜索功能移到 setup
中:
// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'
// in our component
setup (props) {
// 使用 `toRefs` 創(chuàng)建對 props 的 `user` property 的響應(yīng)式引用
const { user } = toRefs(props)
const repositories = ref([])
const getUserRepositories = async () => {
// 更新 `props.user ` 到 `user.value` 訪問引用值
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
// 在用戶 prop 的響應(yīng)式引用上設(shè)置一個(gè)偵聽器
watch(user, getUserRepositories)
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(
repository => repository.name.includes(searchQuery.value)
)
})
return {
repositories,
getUserRepositories,
searchQuery,
repositoriesMatchingSearchQuery
}
}
對于其他的邏輯關(guān)注點(diǎn)我們也可以這樣做,但是你可能已經(jīng)在問這個(gè)問題了——這不就是把代碼移到 setup
選項(xiàng)并使它變得非常大嗎?嗯,那是真的。這就是為什么在繼續(xù)其他任務(wù)之前,我們將首先將上述代碼提取到一個(gè)獨(dú)立的組合式函數(shù)。讓我們從創(chuàng)建 useUserRepositories
開始:
// src/composables/useUserRepositories.js
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'
export default function useUserRepositories(user) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
然后是搜索功能:
// src/composables/useRepositoryNameSearch.js
import { ref, computed } from 'vue'
export default function useRepositoryNameSearch(repositories) {
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(repository => {
return repository.name.includes(searchQuery.value)
})
})
return {
searchQuery,
repositoriesMatchingSearchQuery
}
}
現(xiàn)在在單獨(dú)的文件中有了這兩個(gè)功能,我們就可以開始在組件中使用它們了。以下是如何做到這一點(diǎn):
// src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import { toRefs } from 'vue'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
setup (props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
return {
// 因?yàn)槲覀儾⒉魂P(guān)心未經(jīng)過濾的倉庫
// 我們可以在 `repositories` 名稱下暴露過濾后的結(jié)果
repositories: repositoriesMatchingSearchQuery,
getUserRepositories,
searchQuery,
}
},
data () {
return {
filters: { ... }, // 3
}
},
computed: {
filteredRepositories () { ... }, // 3
},
methods: {
updateFilters () { ... }, // 3
}
}
此時(shí),你可能已經(jīng)知道了這個(gè)練習(xí),所以讓我們跳到最后,遷移剩余的過濾功能。我們不需要深入了解實(shí)現(xiàn)細(xì)節(jié),因?yàn)檫@不是本指南的重點(diǎn)。
// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
setup(props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
const {
filters,
updateFilters,
filteredRepositories
} = useRepositoryFilters(repositoriesMatchingSearchQuery)
return {
// 因?yàn)槲覀儾⒉魂P(guān)心未經(jīng)過濾的倉庫
// 我們可以在 `repositories` 名稱下暴露過濾后的結(jié)果
repositories: filteredRepositories,
getUserRepositories,
searchQuery,
filters,
updateFilters
}
}
}
我們完成了!
請記住,我們只觸及了組合式 API 的表面以及它允許我們做什么。要了解更多信息,請參閱深入指南。
更多建議: