面向将来编程 - 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是多么使人激动。

但愿这篇文章可以对你有帮助,有任何想法和不一样意见均可以在下方评论区告诉我,咱们一块儿交流共同进步。🙂

相关文章
相关标签/搜索