侦听器
用于侦听指定变量,当其响应式状态变化时触发回调函数。
watch()
watch() 需明确指定侦听的数据源,并且仅当数据源变化时,才会执行回调,在创建侦听器时,不会执行回调,可以获取到数据源变化前后的值。
- 第一个参数为“数据源”,可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组
- 第二个参数为回调函数
侦听–响应式变量 / 计算属性
const x = ref(0)
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
侦听–响应式对象
会隐式地创建一个深层侦听器,对象的属性和嵌套属性发生变化时,都会触发回调
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 此处 `newValue` 和 `oldValue` 是相等的,因为它们是同一个对象!
})
若用getter 函数返回响应式对象 ,则只有在返回不同的对象时,才会触发回调:
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
通过添加 deep 选项,可以将其强制转成深层侦听器(即当对象的属性和嵌套属性发生变化时触发回调)
watch(
() => state.someObject,
(newValue, oldValue) => {
// 此处 `newValue` 和 `oldValue` 是相等的,除非 state.someObject 被整个替换了
},
{ deep: true }
)
侦听–对象的属性
方案1:用一个返回该属性的 getter 函数
// 侦听obj对象的count属性
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
方案2:使用 toRefs
import { ref, toRefs, watch } from "vue";
let obj = ref({ count: 30 });
let { count } = toRefs(obj.value);
watch(count, (newValue, oldValue) => {});
不能直接侦听响应式对象的属性值,因为属性值非响应式
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
侦听-- getter 函数
const x = ref(0)
const y = ref(0)
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
侦听-- 多个数据源
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
watchEffect()
watchEffect()在创建侦听器时,会立即执行一遍回调,并从中自动分析出依赖的数据源(其响应性依赖关系不那么明确),当数据源发生改变时,再次触发回调。无法获取到数据源变化前的值。
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
上例中,在页面创建时会先请求 url.value 接口获得初始数据,并自动追踪 url.value
当 url.value
变化时,会再次执行回调,访问新的接口获取数据。
改变回调的触发时机
默认情况下,侦听器回调会在 Vue 组件更新之前被调用(在侦听器回调中访问的 DOM 是被 Vue 更新之前的状态)
添加 flush: 'post'
选项可以让侦听器回调在 Vue 组件更新之后再调用,这样就能在侦听器回调中访问被 Vue 更新之后的 DOM 啦!
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect() 可以直接用 watchPostEffect()
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
停止侦听器
同步语句创建的侦听器,会在组件卸载时自动停止。
异步回调创建的侦听器,必须手动停止它,以防内存泄漏。
setTimeout(() => {
watchEffect(() => {})
}, 100)
手动停止侦听器的方法是调用 watch 或 watchEffect 返回的函数
const unwatch = watchEffect(() => {})
unwatch()
异步创建侦听器的情况很少,如果需要等待一些异步数据,可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
模板引用 ref
用于直接访问底层 DOM 元素,即 vue2 中的 $refs
<input ref="input" />
import { ref, onMounted } from 'vue'
// 声明一个ref变量来存放该元素的引用,变量名必须和模板里的 ref 属性值相同
const input = ref(null)
onMounted(() => {
// 页面加载后,输入框自动获得焦点
input.value.focus()
})
只可以在组件挂载后才能访问模板引用
若侦听模板引用 ref 的变化,需考虑到其值为 null 的情况:
watchEffect(() => {
if (input.value) {
input.value.focus()
} else {
// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
}
})
ref 绑定函数
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
- 绑定函数需使用
:ref
- 每次Dom更新时函数都会被调用
- Dom被卸载时,函数也会被调用一次,此时 el 参数的值是 null
子组件上的 ref
若子组件使用的是选项式 API 或没有使用 <script setup>
,则对子组件的模板引用即子组件的 this,可以直接访问子组件的属性和方法。(但仍推荐用标准的 props 和 emit 接口来实现父子组件交互)
使用了 <script setup>
的子组件是默认私有的:父组件无法访问私有子组件中的任何东西,除非子组件通过 defineExpose 宏显式暴露。
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
父组件通过模板引用获取到的实例类型为 { a: number, b: number } (ref 都会自动解包,和一般的实例一样)。
父子组件
父组件中使用子组件 import
vue3 中导入后就能直接使用,无需像 vue2 中进行注册
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template>
<ButtonCounter />
</template>
子组件接收父组件传入的数据 props
子组件用 defineProps() 接收父组件传入的数据
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
defineProps() 的参数和 props 选项的值相同
defineProps({
title: String,
likes: Number
})
搭配 TypeScript 使用类型标注来声明 props
<script setup lang="ts">
defineProps<{
title?: string
likes?: number
}>()
</script>
选项式风格中,props 对象会作为 setup() 函数的第一个参数被传入:
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
子组件触发自定义事件 emits
组件触发的事件不会冒泡,父组件只能监听直接子组件触发的事件。
父组件–在引入的子组件上绑定事件
<BlogPost @enlarge-text="postFontSize += 0.1"/>
子组件–用 defineEmits() 声明事件
<button @click="$emit('enlarge-text')">Enlarge text</button>
<script setup>
defineEmits(['enlarge-text']) // 多个事件则为 defineEmits(['inFocus', 'submit'])
</script>
选项式风格中,通过 emits 选项定义组件会抛出的事件,并用 setup() 的第二个参数(上下文对象)访问 emit 函数:
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
事件传参
子组件
<button @click="$emit('increaseBy', 1)"></button>
父组件
<MyButton @increase-by="(n) => count += n" />
或
<MyButton @increase-by="increaseCount" />
function increaseCount(n) {
count.value += n
}
子组件继承样式
vue2 中限定只能有一个根节点,父组件中给子组件添加的样式,都会渲染在子组件的根节点上,如:
<!-- 子组件 -->
<p class="child">你好</p>
<!-- 父组件使用子组件时,添加了新的样式 father -->
<MyComponent class="father" />
最终渲染的效果为:
<p class="child father">你好</p>
vue3 中支持多个根节点,所以需要通过 $attrs
指定具体哪些节点继承父组件添加的样式。
<!-- 子组件:在需要继承样式的元素上,添加 :class="$attrs.class" -->
<p :class="$attrs.class">你好</p>
<span>我是朝阳</span>
<!-- 父组件使用子组件时,添加了新的样式 father -->
<MyComponent class="father" />
最终渲染的效果为:
<p class="father">你好</p>
<span>我是朝阳</span>