4种方案带你探索 Vue.js 代码复用的前世今生
前言
在我们平时开发中,不论你使用什么语言,当遇到了大量的重复代码,我们可能会去将重复代码提取出来,独立一个模块,在多个地方引用,这是一个好习惯,是值得推荐的!当然也有些同学不感冒,使用到了直接CV
,撇开代码规范,设计模式这些不谈,往往CV
会给你带来更大的工作量(比如用了很多地方,你要去CV
很多地方,如果后续有变动,你又要重复CV
到很多地方......,当然不推荐CV
)。我们所熟知的Vue.js
也在如何提取公共代码复用方面也一直在探索优化,本文笔者就来和各位聊聊Vue.js代码复用的前世今生。
在Vue.js中我们可通过以下4种方案来实现代码逻辑复用:
- mixin
- 高阶组件
- 作用域插槽(scoped slots)
- Composition API 组合式函数
可能各位常用的是mixin
,没关系,其他几种也很好理解。笔者会通过一个实际的案例分别使用以上的方案实现,并分析各种方案的优缺点来带各位掘友体会Vue.js
在代码逻辑复用方面的优化历程。
案例:就以大家所熟知的 鼠标位置 来吧
Vue.js 代码逻辑复用
我们先不考虑复用,先来看看如何实现鼠标位置这个功能,功能十分简单,大家肯定都会,笔者就不废话了,直接看下代码吧:
基础实现
<script src="https://unpkg.com/vue@next"></script>
<div id="app"></div>
<script>
const { createApp } = Vue
const App = {
template: `{{x}} {{y}}`,
data() {
return {
x: 0,
y: 0
}
},
methods: {
handleMouseMove(e) {
this.x = e.pageX
this.y = e.pageY
}
},
mounted() {
window.addEventListener('mousemove', this.handleMouseMove)
},
unmounted() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
}
createApp(App).mount('#app')
</script>
效果:
接下来,我们尝试将这个功能提取以达到复用的目的,先来看看 mixin
这个方案。
mixin
简单来说,mixin
允许我们提供一个或多个像普通实例对象一样包含实例选项的对象,Vue.js会以一定的逻辑自动合并这些对象里面的选项和组件的选项。举例来说,如果你的 mixin 包含了一个 created
钩子,而组件自身也有一个,那么这两个函数都会被调用。本文不再赘述,请参考Vue.js——mixins。以下就是通过mixin
实现复用MouseMove
的逻辑:
<script>
const { createApp } = Vue
const MouseMoveMixin = {
data() {
return {
x: 0,
y: 0
}
},
methods: {
handleMouseMove(e) {
this.x = e.pageX
this.y = e.pageY
}
},
mounted() {
window.addEventListener('mousemove', this.handleMouseMove)
},
unmounted() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
}
const App = {
template: `{{x}} {{y}}`,
mixins: [ MouseMoveMixin ]
}
createApp(App).mount('#app')
</script>
效果与之前的一致。
我们来分析下mixin
的缺点:
- 当我们的组件有多个
mixin
,比如:mixins: [ MouseMoveMixin, anthorMixin, fooMixin ]
,我们就会分不清哪些变量是从MouseMoveMixin
来的?哪些变量是从anthorMixin
来的?那就出现了第一个缺点:变量来源不清 - 同样的,当我们的组件有多个
mixin
,我们不得不去考虑他们注入的变量名会不会存在冲突。那就出现了第二个缺点:命名冲突
高阶组件
所谓高阶组件,就是通过实现一个包装函数,这个包装函数返回像普通实例对象一样包含实例选项的对象,该对象内包含render
选项,render
用于渲染内部的组件,并将属性通过props
注入到内部组件。比如我们可以像下面这样通过高阶组件复用这个鼠标位置的逻辑。
<script>
const { createApp, h } = Vue
// 包装函数
function withMouse(inner) {
return {
data() {
return {
x: 0,
y: 0
}
},
methods: {
handleMouseMove(e) {
this.x = e.pageX
this.y = e.pageY
}
},
mounted() {
window.addEventListener('mousemove', this.handleMouseMove)
},
unmounted() {
window.removeEventListener('mousemove', this.handleMouseMove)
},
render() {
// 注入 x, y
return h(inner, { x: this.x, y: this.y })
}
}
}
const App = withMouse({
template: `{{x}} {{y}}`,
props: ['x', 'y']
})
createApp(App).mount('#app')
</script>
我们再来分析下,用高阶组件来实现逻辑复用,是不是就没有缺点呢?
同样的,我们还是假设我有还多块逻辑要复用,比如把mixins: [ MouseMoveMixin, anthorMixin, fooMixin ]
改写成高阶组件,那将变成以下代码:
function withMouse(inner) {
// 此处省略
}
function withFoo(inner) {
// 此处省略
}
function withAnthor(inner) {
// 此处省略
}
const App = withAnthor(withFoo(withMouse({
template: `{{x}} {{y}}`,
props: ['x', 'y', 'foo', 'anthor']
})))
createApp(App).mount('#app')
mixin
的问题它都有,props
中我们依然看不清哪些属性是由哪个高阶组件注入的,也依然不得不考虑命名冲突的问题。(有些同学可能觉得,如果注入的变量名能够和包裹函数名有联系,那就能够看出来。那确实是的,但是这就需要有很严格的开发规范和代码走查来约束开发人员了)显然高阶组件也不是什么”灵丹妙药“,我们接着看如何使用scoped slots
来实现这个逻辑复用。
作用域插槽(scoped slots)
作用域插槽(scoped slots)这种方式和高阶组件有点像,区别在于不是通过函数来包裹,而是通过实现一个组件来包裹,我们叫它父组件,在父组件实现需要复用的逻辑,使用作用域插槽,将父组件的状态共享给子组件。代码实现如下:
<script>
const { createApp } = Vue
const MouseMove = {
data() {
return {
x: 0,
y: 0
}
},
methods: {
handleMouseMove(e) {
this.x = e.pageX
this.y = e.pageY
}
},
mounted() {
window.addEventListener('mousemove', this.handleMouseMove)
},
unmounted() {
window.removeEventListener('mousemove', this.handleMouseMove)
},
// 等价于 template: `<slot :x="x" :y="y"></slot>`,
render() {
return this.$slots.default && this.$slots.default({
x: this.x,
y: this.y
})
}
}
const App = {
template: `<MouseMove v-slot="{x, y}">{{x}} {{y}}</MouseMove>`,
components: { MouseMove }
}
createApp(App).mount('#app')
</script>
我们还是来分析下这种方式的优缺点,还是通过假设我们需要重用多个逻辑,把mixins: [ MouseMoveMixin, anthorMixin, fooMixin ]
改写为使用作用域插槽:
const MouseMove = {
}
const Foo = {
}
const Anthor = {
}
const App = {
template: `
<MouseMove v-slot="{ x, y }">
<Foo v-slot="{ foo }">
<Anthor v-slot="{ anthor }">
{{x}} {{y}} {{foo}} {{anthor}}
</Anthor>
</Foo>
</MouseMove>`,
components: { MouseMove, Foo, Anthor }
}
createApp(App).mount('#app')
看上去是解决了上面两个问题了,我们能够很明显的看到每个属性是从哪个组件注入的,来源清晰了,即使有命名的问题,我们在解构的时候是可以重命名避免的,比如Foo
注入的也叫x
,那我们可以这么写<Foo v-slot="{ x: foo }">
。
那是不是这样就完美了呢?并没有,细心的同学可能发现了,我们为了复用逻辑导致了更多的组件实例创建,是不是有点鱼和熊掌不可兼得的感觉,我们接下来看Vue.js
的终极大招——Composition API 组合式函数。
Composition API 组合式函数
先简单介绍下Composition API:
组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它包含了这些API:
- 响应式API —— ref、reactive computed、watch......
- 生命周期钩子 —— onMounted、onUnmounted......
- 依赖注入 —— provide、inject......
接着我们用Composition API来实现一下:
<script>
const { createApp, ref, onMounted, onUnmounted } = Vue
function useMouseMove() {
const x = ref(0)
const y = ref(0)
const handleMouseMove = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', handleMouseMove)
})
onUnmounted(() => {
window.removeEventListener('mousemove', handleMouseMove)
})
return { x, y }
}
const App = {
setup() {
const { x, y } = useMouseMove()
return { x, y }
},
template: `{{x}} {{y}}`,
}
createApp(App).mount('#app')
</script>
看完这个实现,首先它肯定是没有以上的各种问题的,同时Composition API也是Vue3
的一个重大更新,能够让我们更轻松的组织我们的逻辑代码,更轻松的达到逻辑复用,可谓是完美方案!
可能你还有点小问题,比如setup
为啥要先解构,再返回 { x, y }
。
能直接返回useMouseMove()
吗?
const App = {
setup() {
return useMouseMove()
},
template: `{{x}} {{y}}`,
}
答:如果你没有其他变量需要暴露出去,你当然可以直接返回useMouseMove()
。但是直接返回useMouseMove()
,那又回到了之前的问题,又不能清晰地看出哪个变量是哪个组合式函数注入的。
我能不能在return的对象里解构?
const App = {
setup() {
return {
...useMouseMove()
}
},
template: `{{x}} {{y}}`,
}
答:可以,但不推荐,这么写还是又回到了之前的问题。
最佳实践
const App = {
setup() {
const { x, y } = useMouseMove()
return { x, y }
},
template: `{{x}} {{y}}`,
}
总结
本文用Vue.js
四种逻辑复用的方案实现了 鼠标位置 的例子,并且分析了每种方案的优缺点。
- mixin —— 存在
命名冲突
、变量来源不清
- 高阶组件 —— 存在
命名冲突
、变量来源不清
- 作用域插槽(scoped slots)—— 为了逻辑复用导致
更多组件实例创建
,得不偿失 - Composition API 组合式函数 ——
完美方案
相信读完本文,你一定学到了在Vue.js
搭建的应用中实现代码逻辑复用的最佳姿势!
🌹码字不易!欢迎点赞、评论、收藏、和关注哦!感谢浏览!
转载自:https://juejin.cn/post/7238604002354987064