“Vue.js组件对话录:当奶爸组件遇上熊孩子组件,五花八门的沟通艺术”
在Vue.js中,组件间的通信是构建复杂应用的关键。无论是父子组件之间的信息传递,还是子组件向父组件的反馈,掌握有效的通信策略至关重要。本文将深入探讨五种常用的组件通信模式,包括代码示例和解析,以帮助开发者更好地理解和应用这些技术。
一、父子通信:Props
在Vue.js中,父组件向子组件传递数据最常用的方法是通过props。父组件将数据作为属性传递给子组件,子组件通过defineProps
来声明接受哪些props。
代码示例:
// 父组件
<template>
<Child :list="list" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue'
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
}
</script>
// 子组件
<script setup>
const props = defineProps({
list:{
type:Array,
default: () => []
}
})
</script>
解析:
当父组件渲染时,它会将list
的值传递给Child
组件作为list
属性。子组件可以通过props.list
访问这个列表。如果父组件中的list
发生变化(例如,通过add
方法添加新元素),子组件中的list
属性也会相应地更新,因为list
是一个响应式的引用。
二、父子通信:Provide/Inject
另一种父子通信方式是使用provide
和inject
。这种方法允许组件树中的祖先组件向下提供数据,而不管层级有多深,后代组件都可以注入这些数据。
代码示例:
// 祖先组件
<script setup>
import { provide, ref } from 'vue';
import Child from './child.vue'
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
}
provide('list',list.value)
</script>
// 后代组件
<script setup>
import { inject } from 'vue';
const list = inject('list');
</script>
解析:
- 提供数据:祖先组件使用
provide
函数来“发布”或“暴露”数据,这样就可以在整个组件树中被后代组件访问。 - 注入数据:后代组件使用
inject
函数来“订阅”或“接收”祖先组件提供的数据,而无需直接与祖先组件交互或通过中间组件传递props。
这种方法简化了多层嵌套组件间的通信,尤其是在大型应用中,避免了因过多的props传递导致的组件间耦合度增加。然而,过度使用provide
和inject
可能导致组件之间的关系变得模糊不清,因此,在使用时应确保其必要性,并且保持良好的代码组织和注释。
三、子父通信:事件监听
子组件可以通过触发事件来向父组件发送信息,父组件则通过监听这些事件来接收信息。这是子组件向父组件传递数据的常见方式。
代码示例:
// 父组件
<template>
<Child :msg="val" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue'
const newMsg = ref('')
const val = ref('')
const add = () => {
val.value = newMsg.value
}
</script>
//子组件
<script setup>
import { ref,computed,watch, onBeforeUpdate, onUpdated } from 'vue'
const list = ref(['html', 'css']) //这个list要变成_list,若使用下面的第一钟方法
const props = defineProps({
msg:''
})
const list = computed(() => {
_list.value.push(props.msg)
return [..._list.value]
})
watch (
() => props.msg,
(newVal,oldVal) => {
list.value.push(newVal)
}
)
onBeforeUpdate (() => {
list.value.push(props.msg)
})
看看看看看看
onUpdated(() => {
list.value.push(props.msg) //不太可取,会导致无限循环
})
</script>
1.使用computed
更新列表
- 优点:
1.
computed
属性是响应式的,它会根据其依赖项(这里是props.msg
和_list
)的变化自动重新计算。 2. 通过返回一个新的数组副本,可以避免直接修改原始数组带来的副作用。 - 缺点:
1.每当
props.msg
变化时,computed
属性会重新计算,这可能触发组件的多次重新渲染,尤其是如果list
在模板中被频繁使用的话。 2._list.value.push(props.msg)
直接修改了_list
的值,这在computed
中并不是最佳实践,因为它会导致不必要的计算。
2.使用watch
更新列表
- 优点:
1.
watch
可以监听特定的响应式引用,并在它们变化时执行回调。 2. 直接在watch
的回调中更新list
,可以更精确地控制何时以及如何更新。 - 缺点:
1. 如果
list
本身也是一个响应式引用,直接在watch
中修改它可能引发不必要的重新渲染。 2.watch
的回调函数中修改状态可能会导致数据状态和UI状态之间的不同步,特别是在复杂的状态管理中。
3.使用onBeforeUpdate
更新列表
- 优点:
1.
onBeforeUpdate
在组件更新之前执行,可以用来做一些准备工作,如更新状态。 2. 它确保了在组件重新渲染前状态已经更新,理论上可以避免无限循环的问题。 - 缺点:
1. 直接在
onBeforeUpdate
中修改状态可能导致组件状态和UI状态之间的不同步,尤其是在组件嵌套或状态依赖复杂的情况下。 2. 如果list
的更新触发了额外的重新渲染,onBeforeUpdate
将再次被调用,这可能导致无限循环,正如你提到的。
以上方法讲完后,让我们来看看另一种好用的方法:
// 父组件
<template>
<Child @addMsg="handle"/>
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue'
const list = ref(['html', 'css'])
const handle = (e) => {
// console.log(e);
list.value.push(e);
}
</script>
// 子组���
<script setup>
import { ref } from 'vue';
const newMsg = ref('');
const emits = defineEmits(['addMsg']) // 定义
const add = () => {
emits('addMsg',newMsg.value); // 发布
}
</script>
使用自定义事件(Event Emitter)
- 优点: 1. 动态更新:子组件可以实时地通知父组件数据的变化,父组件可以根据这些变化做出响应。 2. 解耦:子组件不知道也不需要知道父组件的内部状态,仅通过事件传递数据,这有助于保持组件的独立性。
- 缺点: 1. 性能考虑:频繁的事件触发可能会导致父组件不必要的渲染。 2. 调试难度:事件的传递路径不总是直观的,可能增加调试难度。
四、子父通信:V-Model
v-model
是一种特殊的语法糖,用于双向数据绑定。当用于子组件时,它允许子组件更新父组件的数据,同时父组件可以监听这些变化。
代码示例:
// 父组件
<template>
<Child v-model:list="list"/> //体现之处
</template>
<script setup>
import { ref } from 'vue';
import Child from './child.vue';
const list = ref(['html', 'css'])
</script>
// 子组件
<script setup>
import { ref } from 'vue'
const props = defineProps({
list:{
type: Array,
default: () => []
}
})
const emits = defineEmits(['update:list'])
const newMsg = ref('')
const add = () => {
const arr = props.list
arr.push(newMsg.value)
emits('update:list', arr)
}
</script>
解析:
在子组件中,当add
方法被调用时,它会更新本地的arr
数组,并通过emits('update:list', arr)
触发一个update:list
事件,将更新后的数组发送给父组件。v-model
在父组件中监听这个事件,并自动更新其绑定的list
变量。
因此,尽管看起来像是直接操作了父组件的list
,但实际上子组件通过emits
触发事件的方式通知父组件更新数据,从而实现了数据的双向绑定。
五、子父通信:Ref与DefineExpose
父组件可以通过ref
获取子组件的引用,进而访问子组件中通过defineExpose
暴露出来的数据或方法。
代码示例:
// 父组件
<template>
<Child ref="childRef"/>
</template>
<script setup>
import Child from './child.vue'
import { onMounted, ref } from 'vue'
const childRef = ref(null)
</script>
// 子组件
<script setup>
import { ref } from 'vue';
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
}
defineExpose({list}) // 子组件心甘情愿暴露出来list
</script>
解析:
当父组件挂载完成后,onMounted
钩子会被调用。此时,childRef
已经指向了子组件的实例。由于子组件使用defineExpose
暴露了list
,父组件现在能够通过childRef.value.list
访问子组件的list
数据。
这意味着父组件可以在任何时候读取或修改子组件的list
属性,只要它是通过defineExpose
明确暴露出来的。这是一种强大的机制,允许在组件之间进行更深层次的交互,但应当谨慎使用,以避免破坏组件之间的封装性和可维护性。
六、结论
组件间的通信是Vue.js应用中不可或缺的一部分,选择合适的通信方式取决于具体的应用场景和组件结构。通过上述五种通信模式的介绍和示例,开发者可以更灵活地构建和维护Vue.js应用,确保数据流的畅通无阻,提升应用的性能和用户体验。随着项目复杂度的增加,理解并熟练运用这些通信策略,将帮助开发者更加高效地解决问题,构建出更加健壮和可维护的Vue.js应用。
转载自:https://juejin.cn/post/7394290549580152844