如果好的老师,好的医生, 评价标准是金钱, 那么也就没必要区分任何职业了 你干嘛的,我赚钱的。 你学啥的,我学赚钱的。 —— 罗翔
十-十三节
- 生命周期
- 侦听器
- 模板引用
- 组件基础
生命周期
钩子函数 | 描述 |
---|---|
beforeCreate | 实例刚在内存中创建,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。 |
created | 实例已经完全创建,数据观测 (data observer) 和 event/watcher 事件配置之后被调用。 |
beforeMount | 实例在挂载之前被调用。相关的 render 函数首次被调用。 |
mounted | 实例挂载到 DOM 后调用,对应 el 被新创建的 vm.$el 替换。 |
beforeUpdate | 数据更新时调用,但是在虚拟 DOM 重新渲染和打补丁之前。可以在这里访问更新前的状态。 |
updated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。 |
beforeDestroy | 实例销毁之前调用。在这一步,实例仍然完全可用。 |
destroyed | Vue 实例销毁后调用,清理工作应在这里进行。 |
errorCaptured | 捕获子孙组件抛出的错误,不会向上冒泡。 |
侦听器 watch
watch
是一个用于监听 Vue 实例数据变化的选项。
通过使用 watch
,你可以在数据发生变化时执行特定的逻辑操作,比如发送网络请求、更新其他数据、触发事件等。
watch
提供了一种响应式地监视数据变化的方式,使得你可以在数据状态改变时做出相应的反应。
watch
选项有两种用法:
- 直接在组件的选项中定义
- 是使用实例方法
$watch
1. 在组件选项中使用 watch:
export default {
data() {
return {
message: '',
};
},
watch: {
message(newValue, oldValue) {
// 当 this.message 改变时会执行这里的逻辑
console.log('Message changed from', oldValue, 'to', newValue);
},
},
};
2. 使用实例方法 $watch
:
export default {
data() {
return {
message: '',
};
},
created() {
// 使用 $watch 方法监视 message 的变化
this.$watch('message', (newValue, oldValue) => {
console.log('Message changed from', oldValue, 'to', newValue);
});
},
};
watch
选项可以监听一个或多个数据属性的变化,并在数据发生变化时执行特定的回调函数。这在很多场景下都很有用,例如:
- 实时校验表单输入并显示错误信息。
- 监听数据变化触发网络请求,实现自动搜索功能。
- 响应数据的增删改操作,更新其他相关数据。
- 监听路由参数的变化,根据参数变化更新页面内容。
需要注意的是,尽管 watch
提供了一种方便的方式来监视数据变化,但在某些情况下,你可能会更倾向于使用计算属性(computed)来实现类似的功能。计算属性可以更直接地响应数据变化并进行计算,但对于一些需要异步操作或监听多个数据的情况,使用 watch
更为合适。
侦听数据源类型
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
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}`)
})
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
// 正确,提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
深层侦听器
直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
即时回调的侦听器
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })
下面是一个表格形式的比较,展示了 watch
和 watchEffect
在不同方面的区别:
特性 | watch |
watchEffect |
---|---|---|
定义方式 | 选项对象中定义,可以指定要监听的数据源 | 作为函数使用,内部访问响应式数据并自动追踪依赖关系 |
数据监听 | 需要明确指定要监听的数据源 | 自动捕获函数内部使用的响应式数据 |
灵活性 | 提供更精细的控制,可以指定多个选项 | 更简洁,适用于处理函数内的副作用 |
适用场景 | 监听特定的数据变化,需要精细控制选项 | 在函数内部处理响应式数据的变化和副作用 |
回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: ‘post’ 选项:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
停止侦听器
用的比较少
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})
模板引用
模板引用(Template Refs)是 Vue 中的一个特性,它允许你在模板中使用 $refs
对象来引用组件或 DOM 元素。通过模板引用,你可以在 Vue 组件中直接操作 DOM 元素或其他组件实例,而不必通过选择器或其他方式进行查找。
在 Vue 中,通过在模板中使用 ref
特性来创建模板引用。ref
特性可以应用在普通 HTML 元素上,也可以应用在组件上。
以下是使用模板引用的示例:
<template>
<div>
<button ref="myButton" @click="changeText">Click me</button>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Initial text',
};
},
methods: {
changeText() {
// 通过 this.$refs.myButton 访问 DOM 元素
this.$refs.myButton.innerText = 'Text changed';
// 修改组件中的数据
this.message = 'Text changed';
},
},
};
</script>
在上面的示例中,我们通过 ref
特性给 <button>
元素创建了一个模板引用 myButton
。然后,在组件的方法中,我们可以通过 this.$refs.myButton
来访问这个 DOM 元素,并修改其文本内容。
需要注意的是,模板引用只在组件渲染完成后才能访问。也就是说,当组件的模板渲染完毕后,$refs
对象中才会包含模板引用。
此外,模板引用也可以应用在组件上,类似地,你可以通过模板引用来直接访问和调用组件实例的方法和属性。
<template>
<div>
<my-component ref="myComponentRef"></my-component>
<button @click="callComponentMethod">Call Component Method</button>
</div>
</template>
<script>
import MyComponent from './MyComponent.vue';
export default {
components: {
MyComponent,
},
methods: {
callComponentMethod() {
// 通过 this.$refs.myComponentRef 访问子组件实例
this.$refs.myComponentRef.myMethod();
},
},
};
</script>
如果一个子组件使用的是选项式 API 或没有使用 ,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
有一个例外的情况,使用了 的组件是默认私有的:一个父组件无法访问到一个使用了 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:
<script setup> import { ref } from 'vue' const a = 1 const b = ref(2) // 像 defineExpose 这样的编译器宏不需要导入 defineExpose({ a, b }) </script>
v-for 中的模板引用
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
// ref 数组并不保证与源数组相同的顺序
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
组件基础
示例
<!-- ParentComponent.vue -->
<template>
<div>
<h2>Parent Component</h2>
<p>Counter Value: {{ counter }}</p>
<button @click="incrementCounter">Increment Counter</button>
<child-component :message="message" @childEvent="handleChildEvent" />
<p>Computed Reversed Message: {{ reversedMessage }}</p>
</div>
</template>
<script>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent,
},
setup() {
const counter = ref(0);
const message = ref('Hello from parent');
const incrementCounter = () => {
counter.value++;
};
const handleChildEvent = (dataFromChild) => {
console.log('Received from child:', dataFromChild);
message.value = 'Updated from child';
};
const reversedMessage = computed(() => message.value.split('').reverse().join(''));
onMounted(() => {
console.log('Parent Component mounted');
});
onBeforeUnmount(() => {
console.log('Parent Component before unmount');
});
return {
counter,
message,
incrementCounter,
handleChildEvent,
reversedMessage,
};
},
};
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<h3>Child Component</h3>
<p>Received Message: {{ message }}</p>
<button @click="sendToParent">Send to Parent</button>
</div>
</template>
<script>
import { ref, emit, onMounted } from 'vue';
export default {
props: {
message: String,
},
setup(props) {
const sendToParent = () => {
const dataToSend = 'Data from child';
emit('childEvent', dataToSend);
};
onMounted(() => {
console.log('Child Component mounted');
});
return {
sendToParent,
};
},
};
</script>