若是你對Vue.js感興趣,那麼你應該知道Vue3立刻就要發佈了(若是你在未來來讀我這篇文章,那我但願他仍然是有用的)。新版本仍在積極開發中,不過全部新功能都能在RFC倉庫中找到。其中有一項是function-api,這將會較大地改變開發vue app的「姿式」。javascript
閱讀這篇文章的讀者應該要有點javascript和vue經驗vue
最好的方法是用一個例子來講明問題。好比咱們如今須要實現一個組件,它可以獲取數據,顯示loading狀態以及一個會根據頁面滾動而變化的topbar。效果以下:
java
一個比較好的作法是把公用的邏輯提取出來給別的組件重用。使用當前Vue2的API,比較經常使用的作法是:git
咱們把跟蹤滾動的邏輯放到mixin裏,把獲取數據的邏輯放到高階組件裏。一個典型的實現以下。github
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。web
import { fetchUserPosts } from '@/api'
const withPostsHOC = WrappedComponent => ({
props: WrappedComponent.props,
data() {
return {
postsIsLoading: false,
fetchedPosts: []
}
},
watch: {
id: {
handler: 'fetchPosts',
immediate: true
}
},
methods: {
async fetchPosts() {
this.postsIsLoading = true
this.fetchedPosts = await fetchUserPosts(this.id)
this.postsIsLoading = false
}
},
computed: {
postsCount() {
return this.fetchedPosts.length
}
},
render(h) {
return h(WrappedComponent, {
props: {
...this.$props,
isLoading: this.postsIsLoading,
posts: this.fetchedPosts,
count: this.postsCount
}
})
}
})
複製代碼
這裏兩個屬性isLoading,posts分別用來初始化loading狀態以及posts數據。fetchPosts方法在組件建立後每次props.id變化的時候會被調用,以便獲取新id的數據。npm
雖然這不算是個很完整的高階組件,不過對於這個例子來講已經夠用了。如今咱們就來包裝一個目標組件,而且傳入這個目標組件的props。api
目標組件是這樣的:app
// ...
<script>
export default {
name: 'PostsPage',
mixins: [scrollMixin],
props: {
id: Number,
isLoading: Boolean,
posts: Array,
count: Number
}
}
</script>
// ...
複製代碼
而後須要經過剛纔的HOC來包裝一下,這樣就能獲取到須要的props:dom
PostsPageOptions: withPostsHOC(PostsPageOptions)
全部源代碼能在這裏找到,【譯註】注意這裏面是整個項目的源代碼,還包括了後面改進版本的代碼,這裏的組件scrollMixin是在那個src/components/PostsPageOptions.vue裏,HOC是在src/App.vue裏的withPostsHOC,這裏的目標組件就是PostsPageOptions
好了,咱們剛剛使用mixin和HOC來實現了咱們的任務,而且mixin和HOC還可以被別的組件通用。可是並非一切都是那麼美好,仍有一些問題在裏面。
想象一下咱們在目標組件裏添加update方法的時候:
若是你再打開頁面,而且滾動的時候,這個topbar不會再顯示了。這是由於咱們重寫了mixin裏的update方法(【譯註】並且還不會有報錯)。一樣的事情也會在那個高階組件裏發生,若是你在data裏把fetchedPosts改爲posts:
。。。你將會獲得這樣的錯誤:
這是由於目標組件已經有posts了。
若是你之後在目標組件中想用另外一個mixin的話:
// ...
export default {
name: 'PostsPage',
mixins: [scrollMixin, mouseMixin],
// ...
複製代碼
你能說清楚pageOffset這個屬性是由哪一個mixin帶來的麼?或者還有一個場景,例如兩個mixins都能有yOffset這個屬性或方法,那麼後一個mixin將會覆蓋掉前一個的yOffset。這樣就不太好了,並且還會帶來許多不可預料的bugs。
高階組件的另外一個問題是,咱們僅僅爲了邏輯重用,就須要爲每一個目標組件來建立一個高階組件實例,這樣的建立會帶來性能損失。
讓咱們看看下一代Vue能給咱們提供什麼樣的替代方案,以及咱們該如何使用function-based API來解決這個問題。
雖然Vue 3尚未發佈,不過有個helper插件已經有了-vue-function-api。這樣就可以在Vue2中使用Vue3的函數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()。顧名思義,在這個函數裏咱們就可以使用新的API來設置咱們組件的邏輯。來,讓咱們來實現一下剛剛那個可以按照滾動來變化的topbar。簡單的組件例子以下:
// ...
<script>
export default {
setup(props) {
const pageOffset = 0
return {
pageOffset
}
}
}
</script>
// ...
複製代碼
注意:setup函數的第一個參數是解析過的而且是響應式的props對象。咱們在這裏還返回了包含pageOffset屬性的對象,暴露給render的做用域使用(簡單理解爲能夠在寫dom時候看成綁定變量使用)。這個pageOffset屬性也是響應式的,不過只在render的做用域內有效。咱們就能像之前同樣寫模板:
<div class="topbar" :class="{ open: pageOffset > 120 }">...</div>
複製代碼
不過這個屬性應該在每次頁面滾動時都會變化。因此咱們還須要在組件mounted的時候添加一個滾動事件監聽,而且在unmounted的時候刪除監聽。而value,onMounted,onUnmounted這3個API就是作這件事的:
// ...
<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>
// ...
複製代碼
其實能夠注意到,2.x版本中的每一個生命週期函數在setup()函數裏都有一個onXXX與其對應。
你可能也注意到了pageOffset變量(【譯註】被value api函數初始化後)僅有一個響應式屬性:.value。由於像number和string這類基本類型在js中不是按照引用傳遞的,因此咱們須要用這個value來包一下。這個value包裝器可以爲任何可變的類型提供響應式功能。(【譯註】這裏的響應式應該就是指一但值有變化,就可以作出響應,好比dom改變等)
這裏是包裝後的pageOffset的樣子:
接下來實現一下數據獲取。就像使用普通選項類api(option-based API)同樣,你可以(在setup裏)使用function-based API來申明computed values和watchers
// ...
<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 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)
isLoading.value = false
}
)
return {
isLoading,
pageOffset,
posts,
count
}
}
}
</script>
// ...
複製代碼
這個computed value和2.x中的computed屬性同樣:只有在他依賴的值發生變化的時候纔會從新計算。watch的第一個參數被稱爲"source",能夠是如下這些值:
咱們剛剛使用function-based API來實現了這個目標組件。接下來咱們要將這個邏輯變的可重用。
這裏比較有意思,爲了邏輯重用,咱們能夠把一些邏輯提取出來放進所謂的「composition function」而且返回可響應的state。
// ...
<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 posts = value([])
watch(
() => props.id,
async id => {
isLoading.value = true
posts.value = await fetchUserPosts(id)
isLoading.value = false
}
)
return { isLoading, posts }
}
export default {
props: {
id: Number
},
setup(props) {
const { isLoading, posts } = useFetchPosts(props)
const count = computed(() => posts.value.length)
return {
...useScroll(),
isLoading,
posts,
count
}
}
}
</script>
// ...
複製代碼
注意這裏咱們用了useFetchPosts and useScroll這兩個函數並返回了可響應的屬性。這兩個函數可以獨立保存成文件而且給其餘組件使用。咱們和以前option-based的方案對比一下:
還有不少其餘優勢能在官方RFC找到
文章全部源代碼在這裏
demo演示地址在這裏
能夠看到,Vue的function-based API能夠乾淨、靈活地來開發組成組件之間或組件內部的邏輯,而不會有傳統基於option-based API開發帶來的反作用。想象一下,基於composition functions的這種開發是十分強大的,而且可以應對任何類型的項目——從小到大,組成複雜的web應用。
我但願這篇文章可以起到點做用。若是你有任何想法和疑問,請在下面留言!我很樂意回答。謝謝。