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 why";
}
return {
fullName,
changeName
}
}
}
</script>
<style scoped>
</style>
侦听数据的变化
在前面的Options API中,我们可以通过watch选项来侦听data或者props的数据变化,当数据变化时执行某一些操作。在Composition API中,我们可以使用watchEffect和watch来完成响应式数据的侦听。
- watchEffect用于自动收集响应式数据的依赖;
- watch需要手动指定侦听的数据源;
watchEffect
当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。
在以前的watch选项中,watch一开始并不会执行一次,只有当监听的数据改变的时候才会执行。watchEffect不一样,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖,比如如下我们在回调函数中使用了name和age,所以name和age都会被搜集到依赖中,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行。

watchEffect的停止侦听
如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可。
比如在上面的案例中,我们age达到20的时候就停止侦听:

watchEffect清除副作用
什么是清除副作用呢?
比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用。
在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate,当副作用即将重新执行或者侦听器被停止时会执行该函数传入的回调函数,我们可以在传入的回调函数中,执行一些清除工作。

setup中使用ref元素
在讲解 watchEffect 执行时机之前,我们先补充一个知识:在setup中如何使用ref元素或者组件?
其实非常简单,我们只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可。

watchEffect的执行时机
默认情况下,组件的更新会在副作用函数执行之前,如果我们希望在副作用函数中获取到元素,代码如下:


我们会发现打印结果打印了两次:这是因为setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载,所以打印为null。而当DOM挂载时,会给title的ref对象赋值新的值,副作用函数会再次执行,打印出来对应的元素。
调整watchEffect的执行时机
如果我们希望在第一次的时候就打印出来对应的元素呢?
这个时候我们需要改变副作用函数的执行时机,它的默认值是pre,它会在元素挂载或者更新之前执行,所以我们会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素。
我们可以设置副作用函数的执行时机,这时候我们就需要给watchEffect传入第二个参数:
<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>
<style scoped>
</style>
- flush 选项默认是pre,它会在元素挂载或者更新之前执行,所以我们会先打印出来一个空的。
- 当传入post就会在DOM挂载完之后再执行。
- 另外还接受 sync,这将强制效果始终同步触发,然而,这是低效的,应该很少需要,不推荐使用。
Watch的使用
-
watch的API完全等同于组件watch选项的Property:
watch需要侦听特定的数据源,并在回调函数中执行副作用;
默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调; -
与watchEffect的比较,watch允许我们:
懒执行副作用(第一次不会直接执行);
更具体的说明当哪些状态发生变化时,触发侦听器的执行;
可以访问侦听状态变化前后的值;
侦听单个数据源
watch侦听函数的数据源有两种类型:
① 一个getter函数,但是该getter函数必须引用可响应式的对象(比如ref或者reactive)。
② 直接写入一个可响应式的对象,比如ref或者reactive,比较常用的是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是一个普通的对象不是reactive对象,可以解构之后返回
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>
<style scoped>
</style>
侦听多个数据源
侦听器还可以使用数组同时侦听多个数据源:
<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
// 多个源直接传入数组就行了,如果我们不希望它是reactive对象,可以解构后返回,其实({...info})和上面的{return {...info}}意思是一样的
// 这时候回调函数的参数就是数组了,我们可以将其解构后再使用
watch([() => ({...info}), name], ([newInfo, newName], [oldInfo, oldName]) => {
console.log(newInfo, newName, oldInfo, oldName);
})
const changeData = () => {
info.name = "kobe";
}
return {
changeData,
info
}
}
}
</script>
<style scoped>
</style>
reactive里面也可以放数组,同理,如果我们不希望得到的是reactive对象,可以解构:[...names],然后使用getter返回,这时候其实就和上面的侦听多个数据源是一个意思。当然我们也可以对回调函数的newValue和oldValue进行解构(下面没对其解构)。

watch的其他选项
如果是如下这种写法,当我们直接传入reactive对象的时候,源码里面会直接设置deep=true,也就是默认就是深度侦听。
<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,
friend: {
name: "kobe"
}
});
// 2.侦听器watch
//直接传入info
watch(info, (newInfo, oldInfo) => {
console.log(newInfo, oldInfo);
})
const changeData = () => {
info.friend.name = "james";
}
return {
changeData,
info
}
}
}
</script>
<style scoped>
</style>

但是如果我们使用上面说的解构后再返回的方式那么深度侦听就没有了,这时候我们需要传第三个参数来进行一些配置:设置 deep 为 true 就可以又实现深度侦听了,也可以传入 immediate 立即执行。
<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,
friend: {
name: "kobe"
}
});
// 2.侦听器watch
// 解构后再返回的方式
watch(() => ({...info}), (newInfo, oldInfo) => {
console.log(newInfo, oldInfo);
}, {
//深度侦听
deep: true,
//立即执行一次
immediate: true
})
const changeData = () => {
info.friend.name = "james";
}
return {
changeData,
info
}
}
}
</script>
<style scoped>
</style>
生命周期钩子
我们前面说过 setup 可以用来替代 data、methods、computed、watch 等等这些选项,也可以替代生命周期钩子。
那么setup中如何使用生命周期函数呢?可以使用直接导入的 onXXX 函数注册生命周期钩子。
<template>
<div>
<button @click="increment">{{counter}}</button>
</div>
</template>
<script>
import { onMounted, onUpdated, onUnmounted, ref } from 'vue';
export default {
setup() {
const counter = ref(0);
const increment = () => counter.value++
onMounted(() => {
console.log("App Mounted1");
})
onMounted(() => {
// 生命周期可以使用多次
console.log("App Mounted2");
})
onUpdated(() => {
console.log("App onUpdated");
})
onUnmounted(() => {
console.log("App onUnmounted");
})
return {
counter,
increment
}
}
}
</script>
<style scoped>
</style>


Provide函数
事实上我们之前还学习过 Provide 和 Inject,Composition API也可以替代之前的 Provide 和 Inject 的选项。
我们可以通过 provide 方法来定义每个 Property,provide可以传入两个参数:
- name:提供的属性名称;
- value:提供的属性值;
父组件的代码:
<template>
<div>
<home/>
<h2>App Counter: {{counter}}</h2>
<button @click="increment">App中的+1</button>
</div>
</template>
<script>
import { provide, ref, readonly } from 'vue';
import Home from './Home.vue';
export default {
components: {
Home
},
setup() {
//包裹成响应式的
const name = ref("coderwhy");
let counter = ref(100);
// 为了实现单向数据流,设置只读,防止子组件修改
provide("name", readonly(name));
provide("counter", readonly(counter));
const increment = () => counter.value++;
return {
increment,
counter
}
}
}
</script>
<style scoped>
</style>
注意:Vue官方推荐能用ref的时候尽量用ref,而不要用reactive,因为ref更有利于代码的抽取。那什么时候用reactive比较好呢?比如有些信息关系特别紧密,如用户信息,肯定放一块比较好,如下:
cosnt user = reactive({
username:"",
password:""
})
Inject函数
在后代组件中可以通过 inject 方法来注入需要的属性和对应的值,inject方法可以传入两个参数:
- 要 inject 的 property 的 name;
- 默认值;
子孙组件的代码:
<template>
<div>
<h2>{{name}}</h2>
<h2>{{counter}}</h2>
<button @click="homeIncrement">home+1</button>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
const name = inject("name");
const counter = inject("counter");
//这时候子孙组件就无法修改父组件的数据了
const homeIncrement = () => counter.value++
return {
name,
counter,
homeIncrement
}
}
}
</script>
<style scoped>
</style>
数据的响应式
为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 或 reactive。

修改响应式Property
如果我们需要修改可响应的数据,那么最好是在数据提供的位置来修改,我们可以将修改方法进行共享,在后代组件中进行调用。

Composition API 初体验
假如我们有计数器、展示鼠标位置、展示滚动位置等功能,如果用以前的Optional API,就是先定义data,再写method,还需要computed,这样的代码有两个问题:
- 同一逻辑的代码太分散,以后修改的时候要跳来跳去
- 可复用性特别差
这两个问题我们都可以通过Composition API来解决。
如果我们把上面代码都写到一个setup里面,其实也没有抽出来,所以一般我们把一个逻辑抽取到一个函数里面,由于和reactive里面的hook函数很像,所以一般我们称之为hook函数,一个hook函数是一个js文件,并且以use开头,这也是习惯。
useCounter.js
我们先来对之前的 counter 逻辑进行抽取:
import { ref, computed } from 'vue';
export default function() {
const counter = ref(0);
const doubleCounter = computed(() => counter.value * 2);
const increment = () => counter.value++;
const decrement = () => counter.value--;
return {
counter,
doubleCounter,
increment,
decrement
}
}
useTitle.js
我们编写一个修改 title 的Hook:
import { ref, watch } from 'vue';
//带有默认的值
export default function(title = "默认的title") {
const titleRef = ref(title);
watch(titleRef, (newValue) => {
document.title = newValue
}, {
//要求立即执行一次
immediate: true
})
return titleRef
}
useScrollPosition.js
我们来完成一个监听界面滚动位置的Hook:
import { ref } from 'vue';
export default function() {
const scrollX = ref(0);
const scrollY = ref(0);
document.addEventListener("scroll", () => {
scrollX.value = window.scrollX;
scrollY.value = window.scrollY;
});
return {
scrollX,
scrollY
}
}
useMousePosition.js
我们来完成一个监听鼠标位置的Hook:
import { ref } from 'vue';
export default function() {
const mouseX = ref(0);
const mouseY = ref(0);
window.addEventListener("mousemove", (event) => {
mouseX.value = event.pageX;
mouseY.value = event.pageY;
});
return {
mouseX,
mouseY
}
}
useLocalStorage.js
我们来完成一个使用 localStorage 存储和获取数据的Hook:
import { ref, watch } from 'vue';
export default function(key, value) {
// 将value包装成ref对象
const data = ref(value);
if (value) {
// 设置值
window.localStorage.setItem(key, JSON.stringify(value));
} else {
//取值
data.value = JSON.parse(window.localStorage.getItem(key));
}
//如果别人拿到我们返回的data,然后通过data.value = "kobe";修改数据,所以我们要监听我们返回的data的改变,然后重新保存值到本地
watch(data, (newValue) => {
window.localStorage.setItem(key, JSON.stringify(newValue));
})
return data;
}
如果传入一个参数,就是取值:
const data = useLocalStorage("name");
如果传入两个参数,就是设置值:
const data = useLocalStorage("name", "coderwhy");
如果别人拿到我们返回的data,然后通过data.value = "kobe";修改数据,所以我们要监听我们返回的data的改变,然后重新保存值到本地:
watch(data, (newValue) => {
window.localStorage.setItem(key, JSON.stringify(newValue));
})
index.js
如果都在App.vue里面导入,感觉不是很优雅,我们可以把导入的操作抽出来放到index.js里面。
import useCounter from './useCounter';
import useTitle from './useTitle';
import useScrollPosition from './useScrollPosition';
import useMousePosition from './useMousePosition';
import useLocalStorage from './useLocalStorage';
//这个文件的作用就是把一些导入的操作抽进来,使代码更优雅
export {
useCounter,
useTitle,
useScrollPosition,
useMousePosition,
useLocalStorage
}
App.vue
使用上面那些hook函数如下:
<template>
<div>
<h2>当前计数: {{counter}}</h2>
<h2>计数*2: {{doubleCounter}}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<h2>{{data}}</h2>
<button @click="changeData">修改data</button>
<p class="content"></p>
<div class="scroll">
<div class="scroll-x">scrollX: {{scrollX}}</div>
<div class="scroll-y">scrollY: {{scrollY}}</div>
</div>
<div class="mouse">
<div class="mouse-x">mouseX: {{mouseX}}</div>
<div class="mouse-y">mouseY: {{mouseY}}</div>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue';
// 导入函数, 默认会从index.js文件里面加载
import {
useCounter,
useLocalStorage,
useMousePosition,
useScrollPosition,
useTitle
} from './hooks';
export default {
setup() {
// counter
const { counter, doubleCounter, increment, decrement } = useCounter();
// 使用title
// 先改成coderwhy,3s后改成kobe
const titleRef = useTitle("coderwhy");
setTimeout(() => {
titleRef.value = "kobe"
}, 3000);
// 滚动位置
const { scrollX, scrollY } = useScrollPosition();
// 鼠标位置
const { mouseX, mouseY } = useMousePosition();
// localStorage
const data = useLocalStorage("info");
const changeData = () => data.value = "哈哈哈哈"
return {
counter,
doubleCounter,
increment,
decrement,
scrollX,
scrollY,
mouseX,
mouseY,
data,
changeData
}
}
}
</script>
<style scoped>
.content {
width: 3000px;
height: 5000px;
}
.scroll {
position: fixed;
right: 30px;
bottom: 30px;
}
.mouse {
position: fixed;
right: 30px;
bottom: 80px;
}
</style>
文件目录如下:

setup顶层编写方式(实验特性)
以前我们编写Composition API都是在setup函数中,并且需要返回,如果我们想不写setup函数,我们可以直接在script中加一个setup,代表我们的代码都会按照以前Composition API那种方式来解析,并且不需要返回。
App.vue文件代码:
<template>
<div>
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>
<!-- message="呵呵呵"给子组件传递值, @increment="getCounter接受子组件传递的事件 -->
<hello-world message="呵呵呵" @increment="getCounter"></hello-world>
</div>
</template>
实验特性
<script setup>
import { ref } from 'vue';
import HelloWorld from './HelloWorld.vue';
const counter = ref(0);
const increment = () => counter.value++;
const getCounter = (payload) => {
console.log(payload);
}
</script>
<style scoped>
</style>
HelloWorld.vue文件代码:
<template>
<div>
<h2>Hello World</h2>
<h2>{{message}}</h2>
<button @click="emitEvent">发射事件</button>
</div>
</template>
<script setup>
// 如果不使用setup函数,我们定义props和emit就需要导入这两个函数
import { defineProps, defineEmit } from 'vue';
// 定义props
const props = defineProps({
message: {
type: String,
default: "哈哈哈"
}
})
//定义emit
const emit = defineEmit(["increment", "decrement"]);
//子组件发送事件
const emitEvent = () => {
emit('increment', "100000")
}
</script>
<style scoped>
</style>
网友评论