You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue3 TreeList响应式Props引发递归更新问题求助

问题分析与修复方案

错误核心原因

  1. 直接修改props触发循环更新:你在onUpdate里直接修改props.filter.value,在clearCheckedIds和onNodeClick里直接修改props.filter.data的节点状态。Vue的props是单向数据流,修改props会触发父组件重新渲染,父组件更新后又会把新的props传给子组件,子组件再次处理事件修改props,形成无限循环。
  2. 引用类型数据的副作用:props.filter.data是数组(引用类型),你直接修改它的元素属性(如checked),会直接修改父组件/Pinia中的原始数据,触发响应式更新,进而导致子组件重复渲染。

修复步骤

  1. 子组件内部维护nodes的本地副本,避免直接操作props的引用数据。
  2. 所有状态修改通过emit通知父组件,由父组件更新Pinia中的状态,保持单向数据流。
  3. 移除对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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 16:30:58