若是你对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应用。
我但愿这篇文章可以起到点做用。若是你有任何想法和疑问,请在下面留言!我很乐意回答。谢谢。