vue2中Options API的弊端
-
在Vue2中,我们 编写组件的方式是Options API:
- Options API的一大特点就是在
对应的属性
中编写对应的功能模块
; p比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变
,也包括生命周期钩子
;
- Options API的一大特点就是在
-
但是这种代码有一个很大的弊端:
- 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;
- 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;
- 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
-
下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:
- 这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;
- 并且当我们处理单个逻辑关注点时,需要不断的跳到相应的代码块中;
大组件的逻辑分散
- 如果我们能将
同一个逻辑关注点相关的代码
收集在一起
会更好。 - 这就是Composition API想要做的事情,以及可以帮助我们完成的事情。
- 也有人把
Vue Composition API
简称为VCA
。
认识Composition API
- 那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
- 为了开始使用Composition API,我们需要有一个可以实际使用它
(编写代码)的地方
; - 在Vue组件中,这个位置就是
setup 函数
;
- 为了开始使用Composition API,我们需要有一个可以实际使用它
- setup其实就是组件的另外一个选项:
- 只不过这个选项强大到我们可以
用它来替代之前所编写的大部分其他选项
; - 比如
methods、computed、watch、data、生命周期
等等;
- 只不过这个选项强大到我们可以
- 接下来我们一起学习这个函数的使用:
- 函数的参数
- 函数的返回值
setup函数的参数
父组件home.vue
<template>
<div>
<home message="hahahaha" id="aaa" class="bbbb"></home>
</div>
</template>
<script>
import Home from './Home.vue';
export default {
components: {
Home
}
}
</script>
子组件Son.vue
<template>
<div>
Home Page
<h2>{{message}}</h2>
<h2>{{title}}</h2>
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
required: true
}
},
data() {
return {
counter: 100
}
},
/**
* 参数一: props, 父组件传递过来属性
*/
// setup函数有哪些参数?
// setup函数有什么样的返回值
// setup(props, context) {
setup(props, {attrs, slots, emit}) {
console.log(props.message);
console.log(attrs.id, attrs.class);
console.log(slots);
console.log(emit);
return {
title: "Hello Home",
counter: 100
}
},
methods: {
btnClick() {
this.$emit("")
}
}
}
</script>
- 我们先来研究一个setup函数的参数,它主要有两个参数:
- 第一个参数:
props
- 第二个参数:
context
- 第一个参数:
- props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可
以直接通过props参数获取:- 对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义;
- 并且在template中依然是可以正常去使用props中的属性,比如message;
- 如果我们在
setup函数
中想要使用props
,那么不可以通过this
去获取; - 因为
props
有直接作为参数传递到setup函数中
,所以我们可以直接通过参数来使用即可
;
- 另外一个参数是
context
,我们也称之为是一个SetupContext
,它里面包含三个属性
:attrs
:所有的非prop的attribute;slots
:父组件传递过来的插槽(这个在以渲染函数返回时会有作用);emit
:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件);
setup函数的返回值
- setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
- psetup的返回值可以在模板template中被使用;
- 也就是说我们可以通过setup的返回值来替代data选项;
- 甚至是我们可以返回一个执行函数来代替在methods中定义的方法
- 但是,如果我们将 counter 在 increment 或者 decrement进行操作时,是否可以实现界面的响应式呢?
- 答案是不可以;
- 这是因为对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作;
setup不可以使用this
- 官方关于this有这样一段描述
- 表达的含义是
this并没有指向当前组件实例
; - 并且在setup被调用之前,
data、computed、methods
等都没有被解析; - 所以
无法在setup中获取this
;
Reactive API
如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数:
那么这是什么原因呢?为什么就可以变成响应式的呢?
- 这是因为当我们
使用reactive函数处理我们的数据之后
,数据再次被使用
时就会进行依赖收集
; - 当
数据发生改变
时,所有收集到的依赖
都是进行对应的响应式
操作(比如更新界面); - 事实上,我们编写的
data选项
,也是在内部交给了reactive函数
将其编程响应式对象的;
<template>
<div>
Home Page
<h2>{{message}}</h2>
<h2>当前计数: {{state.counter}}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
props: {
message: {
type: String,
required: true
}
},
setup() {
const state = reactive({
counter: 100
})
// 局部函数
const increment = () => {
state.counter++;
console.log(state.counter);
}
return {
state,
increment
}
}
}
</script>
Ref API
- eactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:
- 如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告;
<template>
<div>
Home Page
<h2>{{message}}</h2>
<!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>
<show-message :message="counter"></show-message>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
props: {
message: {
type: String,
required: true
}
},
setup() {
// counter编程一个ref的可响应式的引用
// counter = 100;
let counter = ref(100);
// 局部函数
const increment = () => {
counter.value++;
console.log(counter.value);
}
return {
counter,
increment
}
}
}
</script>
-
这个时候Vue3给我们提供了另外一个API:ref API
- ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
- 它内部的值是在ref的 value 属性中被维护的;
-
这里有两个注意事项:
- 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用;
- 但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式;
<template>
<div>
Home Page
<h2>{{message}}</h2>
<!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
<h2>当前计数: {{counter}}</h2>
<!-- ref的解包只能是一个浅层解包(info是一个普通的JavaScript对象) -->
<h2>当前计数: {{info.counter.value}}</h2>
<!-- 当如果最外层包裹的是一个reactive可响应式对象, 那么内容的ref可以解包 -->
<h2>当前计数: {{reactiveInfo.counter}}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
props: {
message: {
type: String,
required: true
}
},
setup() {
let counter = ref(100);
const info = {
counter
}
const reactiveInfo = reactive({
counter
})
// 局部函数
const increment = () => {
counter.value++;
console.log(counter.value);
}
return {
counter,
info,
reactiveInfo,
increment
}
}
}
</script>
readonly的使用
-
在readonly的使用过程中,有如下规则:
-
preadonly返回的对象都是不允许修改的;
-
但是经过readonly处理的原来的对象是允许被修改的;
- ✅ 比如 const info = readonly(obj),info对象是不允许被修改的;
- ✅ 当obj被修改时,readonly返回的info对象也会被修改;
- ✅ 但是我们不能去修改readonly返回的对象info;
-
p其实本质上就是readonly返回的对象的setter方法被劫持了而已;
-
toRefs
import { reactive, toRefs, toRef } from 'vue';
如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的:
那么有没有办法让我们解构出来的属性是响应式的呢?
- Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;
- 那么我们再次进行结构出来的 name 和 age 本身都是 ref的;
setup() {
const info = reactive({name: "why", age: 18});
// 1.toRefs: 将reactive对象中的所有属性都转成ref, 建立链接
let { name, age } = toRefs(info);
return {
name,
age
}
}
这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化;
toRef
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
setup() {
const info = reactive({name: "why", age: 18});
let { name } = info;
let age = toRef(info, "age");
const changeAge = () => {
age.value++;
}
return {
name,
age,
changeAge
}
}
computed
在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理
- 在前面的Options API中,我们是使用computed选项来完成的;
- 在Composition API中,我们可以在 setup 函数中使用 computed 方法来编写一个计算属性;
如何使用computed呢?
方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
<template>
<div>
<h2>{{fullName}}</h2>
<button @click="changeName">修改firstName</button>
</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref("Kobe");
const lastName = ref("Bryant");
// 1.用法一: 传入一个getter函数
// computed的返回值是一个ref对象
const fullName = computed(() => firstName.value + " " + lastName.value);
// 2.用法二: 传入一个对象, 对象包含getter/setter
const fullName = computed({
get: () => firstName.value + " " + lastName.value,
set(newValue) {
const names = newValue.split(" ");
firstName.value = names[0];
lastName.value = names[1];
}
});
const changeName = () => {
// firstName.value = "James"
fullName.value = "coder";
}
return {
fullName,
changeName
}
}
}
</script>
侦听数据的变化
在前面的Options API中,我们可以通过watch选项来侦听data或者props的数据变化,当数据变化时执行某一些操作。
在Composition API中,我们可以使用watchEffect和watch来完成响应式数据的侦听;
- watchEffect用于
自动收集响应式数据的依赖
; - watch需要
手动指定侦听的数据源
;
watchEffect
**当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。 **
我们来看一个案例:
首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;
<template>
<div>
<h2>{{name}}-{{age}}</h2>
<button @click="changeName">修改name</button>
<button @click="changeAge">修改age</button>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
// watchEffect: 自动收集响应式的依赖
const name = ref("why");
const age = ref(18);
const changeName = () => name.value = "kobe"
const changeAge = () => age.value++
watchEffect(() => {
console.log("name:", name.value, "age:", age.value);
});
return {
name,
age,
changeName,
changeAge
}
}
}
</script>
watchEffect的停止侦听
如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可。
- 比如在上面的案例中,我们age达到20的时候就停止侦听:
<template>
<div>
<h2>{{name}}-{{age}}</h2>
<button @click="changeName">修改name</button>
<button @click="changeAge">修改age</button>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
// watchEffect: 自动收集响应式的依赖
const name = ref("tom");
const age = ref(18);
const stop = watchEffect(() => {
console.log("name:", name.value, "age:", age.value);
});
const changeName = () => name.value = "kobe"
const changeAge = () => {
age.value++;
if (age.value > 25) {
stop();
}
}
return {
name,
age,
changeName,
changeAge
}
}
}
</script>
watchEffect清除副作用
什么是清除副作用呢?
-
比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,
或者侦听器侦听函数被再次执行了。
-
那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用;
在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate
- 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数;
- 我们可以在传入的回调函数中,执行一些清楚工作;
<template>
<div>
<h2>{{name}}-{{age}}</h2>
<button @click="changeName">修改name</button>
<button @click="changeAge">修改age</button>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
// watchEffect: 自动收集响应式的依赖
const name = ref("tom");
const age = ref(18);
const stop = watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log("网络请求成功~");
}, 2000)
// 根据name和age两个变量发送网络请求
onInvalidate(() => {
// 在这个函数中清除额外的副作用
// request.cancel()
clearTimeout(timer);
console.log("onInvalidate");
})
console.log("name:", name.value, "age:", age.value);
});
const changeName = () => name.value = "kobe"
const changeAge = () => {
age.value++;
if (age.value > 25) {
stop();
}
}
return {
name,
age,
changeName,
changeAge
}
}
}
</script>
setup中使用ref
在讲解 watchEffect执行时机之前,我们先补充一个知识:在setup中如何使用ref或者元素或者组件?
其实非常简单,我们只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可;
<template>
<div>
<h2 ref="title">哈哈哈</h2>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const title = ref(null);
watchEffect(() => {
console.log(title.value);
}, {
flush: "post"
})
return {
title
}
}
}
</script>
watchEffect的执行时机与ref引用
默认情况下,组件的更新会在副作用函数执行之前:
如果我们希望在副作用函数中获取到元素,是否可行呢?
<template>
<div>
<h2 ref="title">哈哈哈</h2>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const title = ref(null);
watchEffect(() => {
console.log(title.value);
}, {
flush: "post"
})
return {
title
}
}
}
</script>
我们会发现打印结果打印了两次:
- 这是因为setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载,所以打印为null;
- 而当DOM挂载时,会给title的ref对象赋值新的值,副作用函数会再次执行,打印出来对应的元素;
调整watchEffect的执行时机
如果我们希望在第一次的时候就打印出来对应的元素呢?
- 这个时候我们需要改变副作用函数的执行时机;
- 它的默认值是pre,它会在元素 挂载 或者 更新 之前执行;
- 所以我们会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素;
我们可以设置副作用函数的执行时机:
flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
Watch的使用
- watch的API完全等同于组件watch选项的Property:
- watch需要侦听特定的数据源,并在回调函数中执行副作用;
- 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;
与watchEffect的比较,watch允许我们:
- 懒执行副作用(第一次不会直接执行);
- 更具体的说明当哪些状态发生变化时,触发侦听器的执行;
- 访问侦听状态变化前后的值;
侦听单个数据源
watch侦听函数的数据源有两种类型:
- 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref);
- 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref);
<template>
<div>
<h2 ref="title">{{info.name}}</h2>
<button @click="changeData">修改数据</button>
</div>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
setup() {
const info = reactive({name: "why", age: 18});
// 1.侦听watch时,传入一个getter函数
watch(() => info.name, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 2.传入一个可响应式对象: reactive对象/ref对象
// 情况一: reactive对象获取到的newValue和oldValue本身都是reactive对象
// watch(info, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
// 如果希望newValue和oldValue是一个普通的对象
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 情况二: ref对象获取newValue和oldValue是value值的本身
// const name = ref("why");
// watch(name, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
const changeData = () => {
info.name = "kobe";
}
return {
changeData,
info
}
}
}
</script>
侦听多个数据源
侦听器还可以使用数组同时侦听多个源:
侦听响应式对象
如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构:
<template>
<div>
<h2 ref="title">{{info.name}}</h2>
<button @click="changeData">修改数据</button>
</div>
</template>
<script>
import { ref, reactive, watch } from 'vue';
export default {
setup() {
// 1.定义可响应式的对象
const info = reactive({name: "why", age: 18});
const name = ref("why");
// 2.侦听器watch
watch([() => ({...info}), name], ([newInfo, newName], [oldInfo, oldName]) => {
console.log(newInfo, newName, oldInfo, oldName);
})
const changeData = () => {
info.name = "kobe";
}
return {
changeData,
info
}
}
}
</script>
watch的选项
如果我们希望侦听一个深层的侦听,那么依然需要设置 deep 为true:
也可以传入 immediate 立即执行;