写在前面
在编程世界中,代码复用一直是我们追求的目标之一,因为它可以帮助我们提高开发效率,减少重复劳动,同时也使得代码更加清晰和易于维护。而在 Vue.js 这个优秀的前端框架中,如何实现代码复用,又如何找到最佳实践,是我一直在探索的问题。
今天,我将以一个简单功能的实现为例,和大家分享一下我的探索历程,希望能够为大家提供一些有益的启示和参考。
基于 Options API 实现
js
<script>
const { createApp } = Vue;
const App = {
template: `{{x}}, {{y}}`,
data() {
return { x: 0, y: 0 };
},
mounted() {
window.addEventListener('mousemove', this.update);
},
beforeDestory() {
window.removeEventListener('mousemove', this.update);
},
methods: {
update({ pageX: x, pageY: y }) {
this.x = x;
this.y = y;
}
}
};
createApp(App).mount('#app');
</script>
这个功能会在页面上展示当前鼠标位置的坐标信息。 接下来,我将通过不同的方式实现代码复用,将此功能代码抽离出来。
Mixins
js
<script>
const { createApp } = Vue;
const mouseMixin = {
data() {
return { x: 0, y: 0 };
},
mounted() {
window.addEventListener('mousemove', this.update);
},
beforeDestory() {
window.removeEventListener('mousemove', this.update);
},
methods: {
update({ pageX: x, pageY: y }) {
this.x = x;
this.y = y;
}
}
};
const App = {
mixins: [mouseMixin],
template: `{{x}}, {{y}}, {{selfZ}}`,
data() {
return { selfZ: 'self' };
}
};
createApp(App).mount('#app');
</script>
mixin已经实现了逻辑的复用,但是它有以下缺点:
- 没有自己的命名空间,容易造成命名冲突。
js
<script>
const { createApp } = Vue;
const mouseMixin = {
data() {
return { x: 0, y: 0 };
},
methods: {
update({ pageX: x, pageY: y }) {
this.x = x;
this.y = y;
}
}
};
const App = {
mixins: [mouseMixin],
template: `{{x}}, {{y}}`,
methods: {
// 来自mouseMixin的update和组件自身的update冲突
update(){}
}
};
createApp(App).mount('#app');
</script>
我们其实只需要 x y 这两个 props,其他的不需要暴露出来。
- 当有多个 mixins 应用到同一个组件时,我们无法直观的看到哪一个数据来自于哪一个 mixin 或者组件本身。
js
<script>
const { createApp } = Vue;
const mouseMixin = {
data() {
return { x: 0, y: 0 };
}
};
const anotherMixin = {
data() {
return { z: 0 };
}
};
const App = {
mixins: [mouseMixin, anotherMixin],
// 无法清晰知道哪些props来自于哪些mixin或者组件本身,不利于团队协作开发和维护。
template: `{{x}}, {{y}}, {{z}}`,
data() {
return { };
}
};
createApp(App).mount('#app');
</script>
使用 React 的高阶组件概念来实现
js
<script>
const { createApp, h } = Vue;
const useMouse = function (Inner) {
return {
data() {
return { x: 0, y: 0 };
},
mounted() {
window.addEventListener('mousemove', this.update);
},
beforeDestory() {
window.removeEventListener('mousemove', this.update);
},
methods: {
update({ pageX: x, pageY: y }) {
this.x = x;
this.y = y;
}
},
render() {
return h(Inner, {
x: this.x,
y: this.y
});
}
};
};
const App = useMouse({
template: `{{x}}, {{y}}`,
props: ['x', 'y']
});
createApp(App).mount('#app');
</script>
高阶组件内部有自己的命名空间,所以内部没有暴露出来的数据不存在命名冲突。不过假如有多个高阶组件同时应用的话,还是会存在下面的问题:
- 暴露出来的 props 还是会有命名冲突的风险
- 依然存在上面的问题,无法直观地看到数据来源
js
<script>
const { createApp, h } = Vue;
const useMouse = Inner => {
return {
data() {
return { x: 0, y: 0 };
},
mounted() {
window.addEventListener('mousemove', this.update);
},
beforeDestory() {
window.removeEventListener('mousemove', this.update);
},
methods: {
update({ pageX: x, pageY: y }) {
this.x = x;
this.y = y;
}
},
render() {
return h(Inner, {
...this.props,
x: this.x,
y: this.y
});
}
};
};
const useOther = Inner => {
return {
data() {
return { x: 'x' };
},
render() {
return h(Inner, {
...this.props,
x: this.x
});
}
};
};
const App = useOther(
useMouse({
template: `{{x}}, {{y}}`,
props: ['x', 'y']
})
);
createApp(App).mount('#app');
</script>
如上, props 中的 x 被覆盖,且无法确定数据来源。
使用slot来实现
js
<script>
const { createApp, h } = Vue;
const Mouse = {
data() {
return { x: 0, y: 0 };
},
mounted() {
window.addEventListener('mousemove', this.update);
},
beforeDestory() {
window.removeEventListener('mousemove', this.update);
},
methods: {
update({ pageX: x, pageY: y }) {
this.x = x;
this.y = y;
}
},
render() {
return this.$slots.default ? this.$slots.default({ x: this.x, y: this.y }) : '';
}
};
const Other = {
data() {
return { x: 'x' };
},
render() {
return this.$slots.default ? this.$slots.default({ x: this.x }) : '';
}
};
const App = {
template: `<Mouse v-slot="{x, y}">
<Other v-slot="{x: otherX}">
{{x}}, {{y}}, {{otherX}}
</Other>
</Mouse>`,
components: { Mouse, Other }
};
createApp(App).mount('#app');
</script>
到此为止,我们已经解决了上面暴露的所有问题。使用 slot 我们可以清晰的看到数据来自于哪一个组件,并且加入有命名冲突,我们可以向上述的处理一样使用别名。
但是,这里又有了新的思考,我们这里为了数据的复用,创建了很多组件,性能方面是否会变差?
毕竟,维护一个组件也是需要很多性能开销的。
使用 Composition API 来实现
js
<script>
const { createApp, ref, onMounted, onUnmounted } = Vue;
const useMouse = () => {
const x = ref(0);
const y = ref(0);
const update = e => {
x.value = e.pageX;
y.value = e.pageY;
};
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
return {
x,
y
};
};
const useOther = () => {
const x = ref('otherX');
return {
x
};
};
const App = {
setup() {
const { x, y } = useMouse();
const { x: otherX } = useOther();
return { x, y, otherX };
},
template: `{{x}}, {{y}}, {{otherX}}`
};
createApp(App).mount('#app');
</script>
我们好像又回到了js最初的模样。 使用 Composition API 使得我们不再需要考虑 mixins 中的命名空间等问题,也不需要考虑使用 scope slot 方案时造成的额外组件实例维护开销。我们可以轻松地将这些逻辑应用在任何我们想用的地方。
还有一个额外的好处就是,如果我们使用 TS 开发时,我们不需要再为类型推导头疼,在 mixin 中我们不太清楚我们合并过来的到底是什么格式,但是在这里我们可以很容易做到这些。
这些心得主要来自于尤大的讲解视频,从中受益良多,整理出来分享给大家,欢迎大家一起讨论,一起进步。