在Vue中它的数据传输方式与React的概念是一样的,都是遵循单向数据流原则。关于单向数据流的概念是指数据只能沿着一个方向流动,通常是从父组件到子组件。这种设计模式的优点是数据流动路径清晰,状态管理简单,容易调试和维护。这里具体可以入门React——项目实战TodoList - 掘金 (juejin.cn)来到这篇文章了解一下。
而正是因为单向数据流的原因,导致了一些通信需求。这里我们模仿一个简单的TodoList来先聊两种方法。
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
}
</script>
下面我们都基于这段代码来解释。
父子通信(props)
这种通信方法是父组件通过props将数据传递给子组件。子组件不能直接修改这些props。这里我们创建一个父组件一个子组件代码分别为:
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
<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>
<template>
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
const props = defineProps({
list: {
type: Array,
default: () => []
}
})
</script>
这里就是通过给add函数将我们输入的内容添加到list数组中再通过props方法向子组件传递list这个数组子组件通过props接收通过v-for循环数组展示在页面上。
子父通信
在Vue.js中,理论上确实可以在子组件中直接修改父组件传递过来的数据,但这违背了Vue的设计理念鼓励数据单向流动的原则。因此,在Vue中并不推荐子组件直接修改父组件的数据。如果多个子组件都直接修改同一个父组件的数据,就会导致状态不一致,难以追踪哪个子组件做了修改,从而增加调试的难度。
所以子父通信使用的是一种事件驱动模型,在这个模型中,子组件通过触发事件(发布)来通知父组件(订阅者),而父组件通过监听这些事件来进行响应。
$emit
父子组件的代码如下:
<template>
<Child @addMsg="handle" />
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import Child from './child.vue';
import { ref } from 'vue';
const list = ref(['html', 'css'])
const handle = (e) => {
list.value.push(e)
}
</script>
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const newMsg = ref('')
const emits = defineEmits(['addMsg']) // 定义事件
const add = () => {
emits('addMsg', newMsg.value) // 发布
}
</script>
这里我们在子组件使用defineEmits定义可以触发的自定义事件,定义一个点击事件用来触发'addMsg'事件,并传递输入框的值,然后在父组件定义一个处理函数,用于将子组件传递的数据添加到列表中。
v-model
<template>
<Child v-model:list="list" />
<div class="body">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import Child from './child.vue';
import { ref } from 'vue';
const list = ref(['html', 'css'])
</script>
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
list: []
})
const newMsg = ref('')
const emits = defineEmits(['update:list'])
const add = () => {
const arr = props.list
arr.push(newMsg.value)
emits('update:list', arr)
}
</script>
这里我们在子组件使用defineEmits定义可以触发的自定义事件,定义一个点击事件用来触发'update:list'事件,并传递输入框的值。然后在父组件中,通过v-model:list="list"实现了双向数据绑定,这样当子组件触发'update:list'事件时,父组件中的list数组会自动更新为子组件传递的新数组。
ref
<template>
<Child ref="childRef" />
<div class="body">
<ul>
<li v-for="item in childRef?.list">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
import Child from './child.vue';
import { ref } from 'vue';
const childRef = ref(null)
</script>
<template>
<div class="header">
<input type="text" v-model="newMsg">
<button @click="add">确定</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const newMsg = ref('')
const list = ref(['html', 'css'])
const add = () => {
list.value.push(newMsg.value)
}
defineExpose({ list })
</script>
这里我们在子组件使用ref初始化一个响应式的数组list,包含初始的两个字符串'html'和'css'。定义了一个点击事件用来将输入框的值添加到list数组中。然后在父组件中,使用ref初始化一个引用childRef,指向子组件的实例,这样父组件可以通过childRef访问到子组件的list属性,并在模板中使用v-for遍历childRef?.list数组,显示每个元素。
这里的?.表示可选链操作符。它的作用是在尝试访问childRef.list之前先检查childRef是否为null或undefined。如果childRef存在,则继续访问list属性;如果childRef不存在,则返回undefined,避免了运行时错误。