vue-js-3-future-oriented-programming

现有API有什么缺点?

假设产品给我们提出一个需求,读取用户的文章列表,并根据滚动距离来显示或隐藏顶部导航条。
最终的实现效果如下:

demo

如果你是一个有经验的开发工程师,你很容易会想到提取一些公共逻辑方便多个组件间复用。

在Vue2.x API中,一般有以下两种方案:

  • 1、Mixins
  • 2、Higher-order components(高阶组件)

本文我们利用mixin实现滚动逻辑,高阶组件实现数据逻辑。具体实现如下:

Scroll mixin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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属性中。

高阶组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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发生变化时调用。

而我们最终的ArticlePage组件是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
<script>
export default {
name: 'ArticlePage',
mixins: [scrollMixin],
props: {
id: Number,
isLoading: Boolean,
profile: Object,
posts: Array,
count: Number
}
}
</script>
// ...

在使用的时候,用高阶组件进行包装:

1
const ArticlePage = wrappedPostsHOC(ArticlePage)

完整的代码看这里Github

至此我们已经实现了产品的需求。如果你是一个追求卓越的工程师,你会慢慢的发现,这种方案存在几个问题:

1、命名冲突

如果我们想新增一个update方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ...
<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:

1
2
3
4
5
6
7
8
9
10
const withPostsHOC = WrappedComponent => ({
props: WrappedComponent.props, // ['posts', ...]
data() {
return {
postsIsLoading: false,
posts: [] // fetchedPosts -> posts
}
},
// ...
)}

程序会报错:

这是因为我们的组件中已经存在了相同的属性:posts

2、代码不清晰

当过了一段时间,你决定使用另一个mixin会怎样?

1
2
3
4
5
6
7
// ...
<script>
export default {
name: 'PostsPage',
mixins: [scrollMixin, mouseMixin],
// ...
}

你现在还能准确地说出从哪个mixin注入了pageOffset属性吗? 或者另一种情况,这两个mixin都可以有,例如,yOffset属性,因此最后一个mixin将覆盖前一个mixin的属性。

这不是一个好事,可能会导致很多意想不到的bug。 😕

3、性能

HOC的另一个问题是,需要我们创建单独的组件实例来实现逻辑复用,而这往往是以牺牲性能为代价的。

如何解决呢

让我们来看看Vue3会提供什么替代方案,以及我们如何使用 function-based API来解决上述问题。

因为Vue 3还没有发布,所以帮助插件是由vue-function-api创建的。它提供了来自Vue3.x到Vue2.x的api,用于开发下一代Vue应用。

第一步,你需要安装它:

1
npm install vue-function-api

使用Vue.use()来安装它

1
2
3
4
import Vue from 'vue'
import { plugin } from 'vue-function-api'

Vue.use(plugin)

function-based API提供了一个新的组件选项——setup()。 我们通过它来复用组件逻辑。
让我们实现一个功能,显示topbar取决于滚动偏移。基本组件的例子

1
2
3
4
5
6
7
8
9
10
11
12
// ...
<script>
export default {
setup(props) {
const pageOffset = 0
return {
pageOffset
}
}
}
</script>
// ...

注意,setup函数的第一个参数是props对象,而这个props是响应式对象。它返回一个对象,其中包含要暴露给模板渲染上下文的pageOffset属性。

pageOffset是响应式的,我们可以像往常一样在模板中使用它

1
<div class="topbar" :class="{ open: pageOffset > 120 }">...</div>

但是这个属性应该在每个滚动事件中发生变化,为了实现这一点,我们需要在组件将被挂载并在组件卸载时删除侦听器时添加滚动事件侦听器。在API中存在这些方法:value, onMounted, onUnmounted:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ...
<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声明计算值和观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ...
<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”,它可以是以下之一:

  • 1、一个getter函数

  • 2、一个value包装类

  • 3、包含上述两种类型的数组

第二个参数是一个回调函数,只有在从getter或value包装类返回的值发生更改时才会调用它。

我们只是使用基于功能的API实现了目标组件。🎉 下一步目标就是实现组件逻辑的复用。

组件逻辑的复用

这是最有趣的部分,重用与逻辑相关的代码,我们只需将其提取到所谓的组合函数中,并返回响应状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ...
<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函数来返回响应式属性。这些函数可以存储在单独的文件中,并在任何其他组件中使用。与此前的解决方案相比:

  • 1、从任意命名的组合函数返回值,因此没有名称空间冲突
  • 2、暴露给模板的属性有明确的来源,因为它们是从复合函数返回的值
  • 3、没有为逻辑重用而创建的不必要的组件实例

还有很多其他的好处可以在官方RFC页面找到。

所有代码示例可以在这里找到。

总结

如您所见,Vue的基于函数的API提出了一种干净灵活的方式来在组件内部和组件之间编写逻辑,而没有基于配置的API的缺点。 试想一下,对于从小型到大型,复杂的Web应用程序的任何类型的项目,这个API是多么令人激动。

希望这篇文章能够对你有帮助,有任何想法和不同意见都可以在下方评论区告诉我,我们一起交流共同进步。🙂