假設產品給咱們提出一個需求,讀取用戶的文章列表,並根據滾動距離來顯示或隱藏頂部導航條。 最終的實現效果以下:javascript
若是你是一個有經驗的開發工程師,你很容易會想到提取一些公共邏輯方便多個組件間複用。html
在Vue2.x API中,通常有如下兩種方案:vue
本文咱們利用mixin
實現滾動邏輯,高階組件實現數據邏輯。具體實現以下:java
const scrollMixin = {
data() {
return {
pageOffset: 0
}
},
mounted() {
window.addEventListener('scroll', this.update)
},
destroyed() {
window.removeEventListener('scroll', this.update)
},
methods: {
update() {
this.pageOffset = window.pageYOffset
}
}
}
複製代碼
咱們利用scroll
事件監聽器,監聽滾動距離,並存放在pageOffset
屬性中。git
import { fecthUserInfo, fetchUserPosts } from '@/api'
const wrappedPostsHOC = WrappedComponent => ({
props: WrappedComponent.props,
data() {
return {
postsIsLoading: false,
fetchedPosts: [],
fetchedProfile: {}
}
},
watch: {
id: {
handler: 'fetchData',
immediate: true
}
},
methods: {
async fetchData() {
this.postsIsLoading = true
this.fetchedPosts = await fetchUserPosts(this.id)
this.fetchedProfile = await fecthUserInfo(this.id)
this.postsIsLoading = false
}
},
computed: {
postsCount() {
return this.fetchedPosts.length
}
},
render(h) {
return h(WrappedComponent, {
props: {
...this.$props,
isLoading: this.postsIsLoading,
profile: this.fetchedProfile,
posts: this.fetchedPosts,
count: this.postsCount
}
})
}
})
export default wrappedPostsHOC
複製代碼
這裏isLoading
,posts
屬性初始化分別爲加載狀態和文章列表。fetchData
方法將在組件實例化和每次props.id
發生變化時調用。github
而咱們最終的ArticlePage
組件是這樣的:npm
// ...
<script>
export default {
name: 'ArticlePage',
mixins: [scrollMixin],
props: {
id: Number,
isLoading: Boolean,
profile: Object,
posts: Array,
count: Number
}
}
</script>
// ...
複製代碼
在使用的時候,用高階組件進行包裝:api
const ArticlePage = wrappedPostsHOC(ArticlePage)
複製代碼
完整的代碼看這裏Github數組
至此咱們已經實現了產品的需求。若是你是一個追求卓越的工程師,你會慢慢的發現,這種方案存在幾個問題:bash
若是咱們想新增一個update
方法
// ...
<script>
export default {
name: 'PostsPage',
mixins: [scrollMixin],
props: {
id: Number,
isLoading: Boolean,
posts: Array,
count: Number
},
methods: {
update() {
console.log('some update logic here')
}
}
}
</script>
// ...
複製代碼
當你再次打開頁面並滾動時,頂部欄將再也不顯示。這是由於覆蓋了mixin的update
方法。 一樣的,若是你在HOC組件中將fetchedPosts
改成posts
:
const withPostsHOC = WrappedComponent => ({
props: WrappedComponent.props, // ['posts', ...]
data() {
return {
postsIsLoading: false,
posts: [] // fetchedPosts -> posts
}
},
// ...
)}
複製代碼
程序會報錯:
這是由於咱們的組件中已經存在了相同的屬性:posts
當過了一段時間,你決定使用另外一個mixin會怎樣?
// ...
<script>
export default {
name: 'PostsPage',
mixins: [scrollMixin, mouseMixin],
// ...
}
複製代碼
你如今還能準確地說出從哪一個mixin注入了pageOffset
屬性嗎? 或者另外一種狀況,這兩個mixin均可以有,例如,yOffset
屬性,所以最後一個mixin將覆蓋前一個mixin的屬性。
這不是一個好事,可能會致使不少意想不到的bug。 😕
HOC的另外一個問題是,須要咱們建立單獨的組件實例來實現邏輯複用,而這每每是以犧牲性能爲代價的。
讓咱們來看看Vue3會提供什麼替代方案,以及咱們如何使用 function-based API
來解決上述問題。
由於Vue 3尚未發佈,因此幫助插件是由vue-function-api建立的。它提供了來自Vue3.x到Vue2.x的api,用於開發下一代Vue應用。
第一步,你須要安裝它:
npm install vue-function-api
複製代碼
使用Vue.use()
來安裝它
import Vue from 'vue'
import { plugin } from 'vue-function-api'
Vue.use(plugin)
複製代碼
function-based API
提供了一個新的組件選項——setup()
。 咱們經過它來複用組件邏輯。 讓咱們實現一個功能,顯示topbar取決於滾動偏移。基本組件的例子
// ...
<script>
export default {
setup(props) {
const pageOffset = 0
return {
pageOffset
}
}
}
</script>
// ...
複製代碼
注意,setup
函數的第一個參數是props對象,而這個props是響應式對象。它返回一個對象,其中包含要暴露給模板渲染上下文的pageOffset
屬性。
pageOffset
是響應式的,咱們能夠像往常同樣在模板中使用它
<div class="topbar" :class="{ open: pageOffset > 120 }">...</div>
複製代碼
可是這個屬性應該在每一個滾動事件中發生變化,爲了實現這一點,咱們須要在組件將被掛載並在組件卸載時刪除偵聽器時添加滾動事件偵聽器。在API中存在這些方法:value
, onMounted
, onUnmounted
:
// ...
<script>
import { value, onMounted, onUnmounted } from 'vue-function-api'
export default {
setup(props) {
const pageOffset = value(0)
const update = () => {
pageOffset.value = window.pageYOffset
}
onMounted(() => window.addEventListener('scroll', update))
onUnmounted(() => window.removeEventListener('scroll', update))
return {
pageOffset
}
}
}
</script>
// ...
複製代碼
注意,全部的生命週期鉤子都在vue2.x版本有一個等效的onXXX
函數,能夠在setup()
使用這些方法.
您可能還注意到pageOffset
變量包含一個響應屬性:.value。咱們須要使用這個包裝屬性,由於JavaScript中的原始值(如數字和字符串)不是經過引用傳遞的。值包裝器提供了一種爲任意值類型傳遞可變和響應式引用的方法。
下一步是實現用戶的數據抓取邏輯。以及在使用基於選項的API時,可使用基於函數的API聲明計算值和觀察者
// ...
<script>
import {
value,
watch,
computed,
onMounted,
onUnmounted
} from 'vue-function-api'
import { fetchUserPosts } from '@/api'
export default {
setup(props) {
const pageOffset = value(0)
const isLoading = value(false)
const posts = value([])
const profile = value({})
const count = computed(() => posts.value.length)
const update = () => {
pageOffset.value = window.pageYOffset
}
onMounted(() => window.addEventListener('scroll', update))
onUnmounted(() => window.removeEventListener('scroll', update))
watch(
() => props.id,
async id => {
isLoading.value = true
posts.value = await fetchUserPosts(id)
profile.value = await fecthUserInfo(id)
isLoading.value = false
}
)
return {
isLoading,
pageOffset,
profile,
posts,
count
}
}
}
</script>
// ...
複製代碼
computed
的行爲就像vue2.x computed
同樣:跟蹤其依賴關係,而且僅在依賴關係發生更改時才從新計算。 傳遞給watch
的第一個參數稱爲「source」,它能夠是如下之一:
一、一個getter函數
二、一個value包裝類
三、包含上述兩種類型的數組
第二個參數是一個回調函數,只有在從getter或value包裝類返回的值發生更改時纔會調用它。
咱們只是使用基於功能的API實現了目標組件。🎉 下一步目標就是實現組件邏輯的複用。
這是最有趣的部分,重用與邏輯相關的代碼,咱們只需將其提取到所謂的組合函數中,並返回響應狀態:
// ...
<script>
import {
value,
watch,
computed,
onMounted,
onUnmounted
} from 'vue-function-api'
import { fetchUserPosts } from '@/api'
function useScroll() {
const pageOffset = value(0)
const update = () => {
pageOffset.value = window.pageYOffset
}
onMounted(() => window.addEventListener('scroll', update))
onUnmounted(() => window.removeEventListener('scroll', update))
return { pageOffset }
}
function useFetchPosts(props) {
const isLoading = value(false)
const profile = value({})
const posts = value([])
watch(
() => props.id,
async id => {
isLoading.value = true
posts.value = await fetchUserPosts(id)
profile.value = await fecthUserInfo(id)
isLoading.value = false
}
)
return { isLoading, posts }
}
export default {
props: {
id: Number
},
setup(props) {
const { isLoading, profile, posts } = useFetchPosts(props)
const count = computed(() => posts.value.length)
return {
...useScroll(),
isLoading,
profile,
posts,
count
}
}
}
</script>
// ...
複製代碼
注意咱們如何使用useFetchPosts
和useScroll
函數來返回響應式屬性。這些函數能夠存儲在單獨的文件中,並在任何其餘組件中使用。與此前的解決方案相比:
還有不少其餘的好處能夠在官方RFC頁面找到。
全部代碼示例能夠在這裏找到。
如您所見,Vue的基於函數的API提出了一種乾淨靈活的方式來在組件內部和組件之間編寫邏輯,而沒有基於配置的API的缺點。 試想一下,對於從小型到大型,複雜的Web應用程序的任何類型的項目,這個API是多麼使人激動。
但願這篇文章可以對你有幫助,有任何想法和不一樣意見均可以在下方評論區告訴我,咱們一塊兒交流共同進步。🙂