4种方案带你探索Vue代码复用的前世今生
作者:Lvzl
前言
在我们平时开发中,不论你使用什么语言,当遇到了大量的重复代码,我们可能会去将重复代码提取出来,独立一个模块,在多个地方引用,这是一个好习惯,是值得推荐的!当然也有些同学不感冒,使用到了直接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
搭建的应用中实现代码逻辑复用的最佳姿势!
以上就是4种方案带你探索Vue代码复用的前世今生的详细内容,更多关于Vue代码复用的资料请关注脚本之家其它相关文章!