【譯】Vue.js 3: 面向將來編程

基於函數的api是怎樣來解決邏輯複用問題

若是你對Vue.js感興趣,那麼你應該知道Vue3立刻就要發佈了(若是你在未來來讀我這篇文章,那我但願他仍然是有用的)。新版本仍在積極開發中,不過全部新功能都能在RFC倉庫中找到。其中有一項是function-api,這將會較大地改變開發vue app的「姿式」。javascript

閱讀這篇文章的讀者應該要有點javascript和vue經驗vue

當前的api有些什麼問題?

最好的方法是用一個例子來講明問題。好比咱們如今須要實現一個組件,它可以獲取數據,顯示loading狀態以及一個會根據頁面滾動而變化的topbar。效果以下:
java

效果
效果

demo演示

一個比較好的作法是把公用的邏輯提取出來給別的組件重用。使用當前Vue2的API,比較經常使用的作法是:git

  • Mixins (經過mixins選項)
  • Higher-order components (HOCs) 高階組件

咱們把跟蹤滾動的邏輯放到mixin裏,把獲取數據的邏輯放到高階組件裏。一個典型的實現以下。github

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。web

higher-order component:

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: {
        idNumber,
        isLoadingBoolean,
        postsArray,
        countNumber
    }
}
</script>
// ...
複製代碼

而後須要經過剛纔的HOC來包裝一下,這樣就能獲取到須要的props:dom

PostsPageOptions: withPostsHOC(PostsPageOptions)

全部源代碼能在這裏找到,【譯註】注意這裏面是整個項目的源代碼,還包括了後面改進版本的代碼,這裏的組件scrollMixin是在那個src/components/PostsPageOptions.vue裏,HOC是在src/App.vue裏的withPostsHOC,這裏的目標組件就是PostsPageOptions

好了,咱們剛剛使用mixin和HOC來實現了咱們的任務,而且mixin和HOC還可以被別的組件通用。可是並非一切都是那麼美好,仍有一些問題在裏面。

1.命名衝突

想象一下咱們在目標組件裏添加update方法的時候:
若是你再打開頁面,而且滾動的時候,這個topbar不會再顯示了。這是由於咱們重寫了mixin裏的update方法(【譯註】並且還不會有報錯)。一樣的事情也會在那個高階組件裏發生,若是你在data裏把fetchedPosts改爲posts:

。。。你將會獲得這樣的錯誤:

error
error

這是由於目標組件已經有posts了。

2.來源不明確

若是你之後在目標組件中想用另外一個mixin的話:

// ...
export default {
    name'PostsPage',
    mixins: [scrollMixin, mouseMixin],
// ...
複製代碼

你能說清楚pageOffset這個屬性是由哪一個mixin帶來的麼?或者還有一個場景,例如兩個mixins都能有yOffset這個屬性或方法,那麼後一個mixin將會覆蓋掉前一個的yOffset。這樣就不太好了,並且還會帶來許多不可預料的bugs。

3.性能

高階組件的另外一個問題是,咱們僅僅爲了邏輯重用,就須要爲每一個目標組件來建立一個高階組件實例,這樣的建立會帶來性能損失。

讓咱們「setup」

讓咱們看看下一代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的樣子:

pageOffset
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",能夠是如下這些值:

  • 一個getter函數
  • 一個value包裝器
  • 一個包含以上兩種類型的array(【譯註】應該是用來監聽多個值時使用的吧)
    第二個參數是一個回調函數,這個函數會在第一個參數裏的值有變化後回調。

咱們剛剛使用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: {
        idNumber
    },
    setup(props) {
        const { isLoading, posts } = useFetchPosts(props)
        const count = computed(() => posts.value.length)
        return {
            ...useScroll(),
            isLoading,
            posts,
            count
        }
    }
}
</script>
/
/ ...
複製代碼

注意這裏咱們用了useFetchPosts and useScroll這兩個函數並返回了可響應的屬性。這兩個函數可以獨立保存成文件而且給其餘組件使用。咱們和以前option-based的方案對比一下:

  • 暴露給外面使用的屬性具備明確的來源,由於它們是做爲組合函數的返回值返回出來的。
  • 在組合函數裏返回的值均可以隨便命名,不會存在命名衝突。(【譯註】從上面兩個函數能夠看出來,在函數裏的命名如pageOffset不會對外面形成污染,而只要保證目標組件使用的時候沒有衝突就好了)
  • 代碼重用以後也不須要有new組件實例。(【譯註】對應上面說的高階組件的額外開銷)

還有不少其餘優勢能在官方RFC找到

文章全部源代碼在這裏

demo演示地址在這裏

總結

能夠看到,Vue的function-based API能夠乾淨、靈活地來開發組成組件之間或組件內部的邏輯,而不會有傳統基於option-based API開發帶來的反作用。想象一下,基於composition functions的這種開發是十分強大的,而且可以應對任何類型的項目——從小到大,組成複雜的web應用。

我但願這篇文章可以起到點做用。若是你有任何想法和疑問,請在下面留言!我很樂意回答。謝謝。

本文翻譯自:blog.bitsrc.io/vue-js-3-fu…

相關文章
相關標籤/搜索