【译】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…

相关文章
相关标签/搜索