Vue 3--Composition API 和 script setup

说一下 Composition API
和 Options API 的区别?

Composition API 也叫组合式 API,它主要就是为了解决 Vue2 中 Options API 的问题。

一是在 Vue2 中只能固定用 data、computed、methods 等选项组织代码,在组件越来越复杂的时候,一个功能相关的属性和方法就会在文件上中下到处都有,很分散,变越来越难维护

二是 Vue2 中虽然可以用 minxin 来做逻辑的提取复用,但是 minxin 里的属性和方法名会和组件内部的命名冲突,还有当引入多个 minxin 的时候,我们使用的属性或方法是来于哪个 minxin 也不清楚

而 Composition API 刚才就解决了这两个问题,可以让我们自由的组织代码,同一功能相关的全部放在一起,代码有更好的可读性更便于维护,单独提取出来也不会造成命名冲突,所以也有更好的可扩展性


Option的弊端

Option的缺陷–反复横跳

相信大部分同学都维护过超过200行的.vue组件,新增或者修改一个需求,就需要分别在data,methods,computed里修改 ,滚动条反复上下移动,我称之为『反复横跳』 比如我们简单的加个拍脑门的需求 加个累加器 ,这种写代码上下反复横条的感觉, 相信大家都懂的

Option的缺陷:mixin和this

反复横跳的本质,在于功能的分块组织,以及代码量太大了,如果我们能把代码控制在一屏,自然就解决了,vue2里的解决方案,是使用mixin来混合, 我们抽离一个counter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
return {
count:1
}
},
computed: {
double() {
return this.count * 2
}
},
methods:{
add(){
this.count++
}
}
}

在App.vue中

1
2
3
4
5
6
7
8
import Counter from './counter'
export default {
mixins:[Counter],
data(){
...
},
...
}

这样确实拆分了代码,但是有一个贼严重的问题,就是不打开counter.js,App.vue里的this上,count,add这些属性,是完全不知道从哪来的,你不知道是mixin,还是全局install,还是Vue.prototype.count设置的,数据来源完全模糊,调试爽死你,这也是option的一个大问题,this是个黑盒,template里写的count和double,完全不知道从哪来的

mixin命名冲突

如果有两个mixin,就更有意思了,比如我们又有一个需求,实时显示鼠标的坐标位置x,并且有一个乘以2的计算属性凑巧也叫double,再整一个mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
data() {
return {
x:0
}
},
methods:{
update(e){
this.x = e.pageX
}
},
computed:{
double(){
return this.x*2
}
},
mounted(){
window.addEventListener('mousemove', this.update)
},
destroyed(){
window.removeEventListener('mousemove', this.update)
}
}

这是是一个独立维护的mixin,可能在N个地方用到,他根本不知道会不会有人和他冲突,然后用一下

1
2
3
4
5
6
import Counter from './counter'
import Mouse from './mouse'
export default {
mixins:[Counter,Mouse],
......
}

两个mixin里都有double这个数,尴尬,看效果 ,lsp的count被覆盖了 很尴尬,而且在App.vue这里,你完全不知道这个double到底是哪个,调试很痛苦

Composition API 和 <script setup> 上手

Composition API 拆分代码

之前的累加器和清单,虽然功能都很简单,但也属于两个功能模块。如果在一个页面里有这两个功能,那就需要在 data 和 methods 里分别进行配置。但这样的话,数据和方法相关的代码会写在一起,在组件代码行数多了以后就不好维护。所以,我们需要使用 Composition API 的逻辑来拆分代码,把一个功能相关的数据和方法都维护在一起。
但是,所有功能代码都写在一起的话,也会带来一些问题:随着功能越来越复杂,script 内部的代码也会越来越多。因此,我们可以进一步对代码进行拆分,把功能独立的模块封装成一个独立的函数,真正做到按需拆分。
在下面,我们新建了一个函数 useTodos:

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

function useTodos() {
let title = ref("");
let todos = ref([{ title: "学习Vue", done: false }]);
function addTodo() {
todos.value.push({
title: title.value,
done: false,
});
title.value = "";
}
function clear() {
todos.value = todos.value.filter((v) => !v.done);
}
let active = computed(() => {
return todos.value.filter((v) => !v.done).length;
});
let all = computed(() => todos.value.length);
let allDone = computed({
get: function () {
return active.value === 0;
},
set: function (value) {
todos.value.forEach((todo) => {
todo.done = value;
});
},
});
return { title, todos, addTodo, clear, active, all, allDone };
}

这个函数就是把那些和清单相关的所有数据和方法,都放在函数内部定义并且返回,这样这个函数就可以放在任意的地方来维护。

而我们的组件入口,也就是<script setup>中的代码,就可以变得非常简单和清爽了。在下面的代码中,我们只需要调用 useTodos,并且获取所需要的变量即可,具体的实现逻辑可以去 useTodos 内部维护,代码可维护性大大增强。

使用

1
2
3
4
5
6
7
8
9
10
11

<script setup>
import { ref, computed } from "vue";

let count = ref(1)
function add(){
count.value++
}

let { title, todos, addTodo, clear, active, all, allDone } = useTodos();
</script>

我们在使用 Composition API 拆分功能时,也就是执行 useTodos 的时候,ref、computed 等功能都是从 Vue 中单独引入,而不是依赖 this 上下文。其实你可以把组件内部的任何一段代码,从组件文件里抽离出一个独立的文件进行维护。

获取鼠标位置函数封装

现在,我们引入追踪鼠标位置的需求进行讲解,比如我们项目中可能有很多地方需要显示鼠标的坐标位置,那我们就可以在项目的 src/utils 文件夹下面新建一个 mouse.js。我们先从 Vue 中引入所需要的 ref 函数,然后暴露一个函数,函数内部和上面封装的 useTodos 类似,不过这次独立成了文件,放在 utils 文件下独立维护,提供给项目的所有组件使用。

想获取鼠标的位置,我们就需要监听 mousemove 事件。这需要在组件加载完毕后执行,在 Composition API 中,我们可以直接引入 onMounted 和 onUnmounted 来实现生命周期的功能。
看下面的代码,组件加载的时候,会触发 onMounted 生命周期,我们执行监听 mousemove 事件,从而去更新鼠标位置的 x 和 y 的值;组件卸载的时候,会触发 onUnmounted 生命周期,解除 mousemove 事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {ref, onMounted,onUnmounted} from 'vue'

export function useMouse(){
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})

onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}

完成了上面的鼠标事件封装这一步之后,我们在组件的入口就可以和普通函数一样使用 useMouse 函数。在下面的代码中,上面的代码返回的 x 和 y 的值可以在模板任意地方使用,也会随着鼠标的移动而改变数值。

1
2
3
4
5

import {useMouse} from '../utils/mouse'

let {x,y} = useMouse()

简单来看,因为 ref 和 computed 等功能都可以从 Vue 中全局引入,所以我们就可以把组件进行任意颗粒度的拆分和组合,这样就大大提高了代码的可维护性和复用性。

<script setup> 好用的功能

如果没有 <script setup>,那么我们需要写出下面这样的代码来实现累加器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script >
import { ref } from "vue";
export default {
setup() {
let count = ref(1)
function add() {
count.value++
}
return {
count,
add
}
}
}
</script>

在上面的代码中,我们要在<script>中导出一个对象。我们在 setup 配置函数中写代码时,和 Options 的写法比,也多了两层嵌套。并且,我们还要在 setup 函数中,返回所有需要在模板中使用的变量和方法。上面的代码中,setup 函数就返回了 count 和 add。

使用 <script setup>可以让代码变得更加精简,这也是现在开发 Vue 3 项目必备的写法。除了我们上面介绍的功能,<script setup> 还有其它一些很好用的功能,比如能够使用顶层的 await 去请求后端的数据等等,我们会在后面的项目中看到这种使用方法。

style 样式的特性

比如,在 style 标签上,当我们加上 scoped 这个属性的时候,我们定义的 CSS 就只会应用到当前组件的元素上,这样就很好地避免了一些样式冲突的问题。

如果在 scoped 内部,你还想写全局的样式,那么你可以用:global 来标记,这样能确保你可以很灵活地组合你的样式代码(后面项目中用到的话,我还会结合实战进行讲解)。而且我们甚至可以通过 v-bind 函数,直接在 CSS 中使用 JavaScript 中的变量。

在下面这段代码中, 我在 script 里定义了一个响应式的 color 变量,并且在累加的时候,将变量随机修改为红或者蓝。在 style 内部,我们使用 v-bind 函数绑定 color 的值,就可以动态地通过 JavaScript 的变量实现 CSS 的样式修改,点击累加器的时候文本颜色会随机切换为红或者蓝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

<template>
<div>
<h1 @click="add">{{ count }}</h1>
</div>
</template>

<script setup>
import { ref } from "vue";
let count = ref(1)
let color = ref('red')
function add() {
count.value++
color.value = Math.random()>0.5? "blue":"red"
}
</script>

<style scoped>
h1 {
color:v-bind(color);
}
</style>>
参考文章

新的代码组织方式:Composition API + script setup 到底好在哪里?
那个忙了一夜的Vue3动画很好,就是太短了