面向將來編程 - vue-function-api到底是幹什麼的?

假設產品給咱們提出一個需求,讀取用戶的文章列表,並根據滾動距離來顯示或隱藏頂部導航條。 最終的實現效果以下:javascript

demo

若是你是一個有經驗的開發工程師,你很容易會想到提取一些公共邏輯方便多個組件間複用。html

在Vue2.x API中,通常有如下兩種方案:vue

  • 一、Mixins
  • 二、Higher-order components(高階組件)

本文咱們利用mixin實現滾動邏輯,高階組件實現數據邏輯。具體實現以下:java

Scroll mixin:

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
複製代碼

這裏isLoadingposts屬性初始化分別爲加載狀態和文章列表。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>
// ...
複製代碼

注意咱們如何使用useFetchPostsuseScroll函數來返回響應式屬性。這些函數能夠存儲在單獨的文件中,並在任何其餘組件中使用。與此前的解決方案相比:

  • 一、從任意命名的組合函數返回值,所以沒有名稱空間衝突
  • 二、暴露給模板的屬性有明確的來源,由於它們是從複合函數返回的值
  • 三、沒有爲邏輯重用而建立的沒必要要的組件實例

還有不少其餘的好處能夠在官方RFC頁面找到。

全部代碼示例能夠在這裏找到。

總結

如您所見,Vue的基於函數的API提出了一種乾淨靈活的方式來在組件內部和組件之間編寫邏輯,而沒有基於配置的API的缺點。 試想一下,對於從小型到大型,複雜的Web應用程序的任何類型的項目,這個API是多麼使人激動。

但願這篇文章可以對你有幫助,有任何想法和不一樣意見均可以在下方評論區告訴我,咱們一塊兒交流共同進步。🙂

相關文章
相關標籤/搜索