Vue3 TreeList响应式Props引发递归更新问题求助
问题分析与修复方案
错误核心原因
- 直接修改props触发循环更新:你在
onUpdate里直接修改props.filter.value,在clearCheckedIds和onNodeClick里直接修改props.filter.data的节点状态。Vue的props是单向数据流,修改props会触发父组件重新渲染,父组件更新后又会把新的props传给子组件,子组件再次处理事件修改props,形成无限循环。 - 引用类型数据的副作用:
props.filter.data是数组(引用类型),你直接修改它的元素属性(如checked),会直接修改父组件/Pinia中的原始数据,触发响应式更新,进而导致子组件重复渲染。
修复步骤
- 子组件内部维护
nodes的本地副本,避免直接操作props的引用数据。 - 所有状态修改通过
emit通知父组件,由父组件更新Pinia中的状态,保持单向数据流。 - 移除对props的直接修改,改用本地状态+事件通知的方式。
修复后的完整代码
<template> <div data-cy="filter-tree-list"> <tree data-cy="filter-tree-list-component" data-test="filter-tree-list" class="flex flex-row w-full justify-start items-stretch rounded-md px-3 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300" v-if="localNodes.length > 0" rowHoverBackground="#b2ffff40" :nodes="localNodes" :use-checkbox="true" @update:nodes="onUpdate" @nodeClick="onNodeClick" > </tree> </div> </template> <script setup lang="ts"> import { inject, defineEmits, defineProps, ref, watch } from "vue"; import { Filter } from "../../types/common/filter"; interface Node { id: string; label: string; nodes: Array<Node>; checked?: boolean; } const piniaRef: unknown = inject("piniaStore"); const props = defineProps({ filter: { type: Object as () => Filter, required: true, }, isReadOnly: { type: Boolean, default: false, }, }); const emit = defineEmits(["filterChange"]); const checkedIds = ref<string[]>([]); // 维护本地nodes副本,深拷贝避免引用关联 const localNodes = ref<Node[]>(JSON.parse(JSON.stringify(props.filter.data || []))); // 监听父组件传来的filter.data变化,同步更新本地副本 watch(() => props.filter.data, (newData) => { localNodes.value = JSON.parse(JSON.stringify(newData || [])); }, { deep: true }); // 监听父组件传来的filter.value变化,同步更新本地节点选中状态 watch(() => props.filter.value, (newValue) => { if (newValue === "") { clearLocalCheckedIds(); } else { const idSet = new Set(newValue.split(",")); syncLocalCheckedIds(localNodes.value, idSet); } }); const syncLocalCheckedIds = (nodes: Node[], idSet: Set<string>) => { nodes.forEach(node => { node.checked = idSet.has(node.id); if (Array.isArray(node.nodes)) { syncLocalCheckedIds(node.nodes, idSet); } }); }; const clearLocalCheckedIds = () => { const clear = (nodes: Node[]) => { nodes.forEach(node => { node.checked = false; if (Array.isArray(node.nodes)) { clear(node.nodes); } }); }; clear(localNodes.value); }; const onUpdate = (nodes: Node[]) => { if (piniaRef && props.filter.value === undefined) return; checkedIds.value = []; getCheckedIds(nodes); // 通知父组件更新filter.value,不直接修改props emit("filterChange", { ...props.filter, value: checkedIds.value.length > 0 ? checkedIds.value.join(",") : "" }); }; const onNodeClick = (node: Node) => { const checked = node.checked; // 修改本地nodes副本,而非props的原始数据 const updateChildNodes = (targetNodes: Node[]) => { targetNodes.forEach(childNode => { childNode.checked = checked; if (Array.isArray(childNode.nodes)) { updateChildNodes(childNode.nodes); } }); }; if (Array.isArray(node.nodes)) { updateChildNodes(node.nodes); } }; const getCheckedIds = (nodes: Node[]) => { if (Array.isArray(nodes)) { nodes.forEach((node) => { if (Array.isArray(node.nodes)) { getCheckedIds(node.nodes); } else if (node.checked) { checkedIds.value.push(node.id); } }); } }; </script>
额外说明
- 用
JSON.parse(JSON.stringify())做深拷贝是简单实现,如果你的节点数据包含复杂类型(如Date、函数),建议使用更可靠的深拷贝工具(如lodash的cloneDeep)。 - 父组件需要监听
filterChange事件,更新Pinia中的对应状态,确保数据流是:子组件事件 → 父组件更新Pinia → 子组件接收新props更新本地状态,完全单向。
内容的提问来源于stack exchange,提问作者Farid Guzman
相关产品推荐
相关产品推荐

