大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~
当我们传递的props不再是普通的字面量而是对象时,极易写出一些破坏props的单项数据流动的代码
我们以一个简单的todoList做举例,这是App.vue的代码,之后不会做变更
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { Task } from './types'
import TaskCom from './components/Task.vue'
const lists = ref<Task[]>([{
title: 'title1',
done: false,
}])
</script>
<template>
<TaskCom
v-for="(task, index) in lists"
:key="index"
:task="task" />
</template>
注意的点,我们没有接收Task组件的update:task事件,也就是说如果Task组件是受控的,那么它的值应当始终不发生变动,如果它的值不是受控的,也不应当影响到prop传递进去的task,否则就是破坏了props的单项数据流动
Task.vue组件代码
<script setup lang="ts">
import type { Task } from '../types'
const props = defineProps<{
task: Task
}>()
const emits = defineEmits<{
(e: 'update:task', newTask: Task): void
}>()
</script>
<template>
<div>
<input v-model="task.done" type="checkbox">
<span>{{ task.title }}</span>
</div>
</template>
如果运行起来去更改checkbox的值,你会发现App组件内的lists下面task的done值也进行了变更,Task组件直接修改了props的参数!!!
这种直接使用v-model的还好,因为eslint会给你一个报错,提醒你不会去犯这个错误
Unexpected mutation of "task" prop.eslint[vue/no-mutating-props](https://eslint.vuejs.org/rules/no-mutating-props.html)
比较常见的是使用toRef对props的值进行解构,进而规避掉了eslint的错误提醒
也会达到直接修改了props的效果
<script setup lang="ts">
//...
const task = toRef(props, 'task');
// 或者
const { task } = toRefs(props)
</script>
所以使用toRef或者toRefs对props的解构并不是一个很好的行为
useVmodel对于字面量的值做双向绑定可以做到受控的状态绑定,但是对于对象而言跟使用toRef是一样的结果
<script setup lang="ts">
//...
const task = useVModel(props, 'task', emits)
</script>
这种情况下,依然会直接修改到props的值
每一次更改都使用emit事件进行发送
<script setup lang="ts">
// ...
function handleChange(e: any) {
emits('update:task', {
...props.task,
done: e.target.checked,
});
e.target.checked = props.task.done;
}
</script>
<template>
<div>
<input :checked="task.done" type="checkbox" @change="handleChange" />
<span>{{ task.title }}</span>
</div>
</template>
需要手动赋值e.target.checked,是因为input的checked属性不是一个受控值
摘自mdn
checked
一个布尔属性,表示该复选框是否被默认选中(当页面加载时)。它不表示这个复选框当前是否被选中:如果复选框的状态被改变,这个内容属性不反映这个变化。此时,只有 `HTMLInputElement`[1] 的 IDL 属性
checked
会更新。
此时,点击checked的值是不会发生变更的,props的值也不会受到影响,除非父组件确定接收这个变更
如果App.vue对子组件发出的变更事件作出响应
<script setup lang="ts">
// ...
function updateTask(task: Task, index: number) {
lists.value[index] = task;
}
</script>
<template>
<TaskCom
v-for="(task, index) in lists"
:key="index"
:task="task"
@update:task="(val) => updateTask(val, index)"
/>
</template>
这样checked的点击效果又恢复成正常了,而且是完全受控的
当然也可以使用vueuse的usevmodel发送事件,就不如上面那么的直观
<script setup lang="ts">// ...const task = useVModel(props, 'task', emits);function handleChange(e: any) { task.value = { ...props.task, done: e.target.checked, }; e.target.checked = props.task.done;}</script>
<script setup lang="ts">
// ...
const task = ref({ ...props.task });
/** 处理props值同步 */
watch(
() => props.task,
() => {
task.value.done = props.task.done;
task.value.title = props.task.title;
},
{ deep: true }
);
/** 处理事件发送 */
watch(
() => task,
() => {
emits('update:task', {...task.value});
},
{ deep: true }
);
</script>
<template>
<div>
<input type="checkbox" v-model="task.done" />
<span>{{ task.title }}</span>
</div>
</template>
直接使用useVmodel或者watchEffect(()=>{task.value=props.task})都很容易出现死循环,所以建议一般还是处理成受控的组件
Vue props单向数据流动 - StackBlitz[2]
在 Vue 中,props 是单向数据流,即组件只能从父组件接收 props,并不能直接修改它们。这是为了保持代码的可维护性和可预测性。
然而,当我们将对象作为 props 传递给子组件时,容易写出一些破坏单向数据流的代码。比如,直接使用 v-model 绑定对象的属性,使用 toRef 或 toRefs 对 props 进行解构等等,这些行为都有可能直接修改父组件传递进来的 props,导致数据流的混乱和代码的难以维护。
在实际开发中,我们应该遵循 Vue 的设计原则,保持 props 的单向数据流动,而不是试图修改它们。为了解决这个问题,我们可以将 props 转换为受控组件,通过 emit 事件将更改的值传递给父组件进行响应,从而保持单向数据流的原则。
以一个 todoList 的例子,我们演示了一些破坏 props 单向数据流的代码和如何通过转换成受控组件避免这些问题的方法。虽然在实际开发中我们可能会使用其他方法来解决这个问题,但遵循单向数据流的原则对于代码的可维护性和可预测性都是非常重要的。
作者:成路
链接:https://juejin.cn/post/7225231707825045563
来源:稀土掘金
我是林三心