如何在Vue项目中实现一个可折叠的树形菜单组件,结合`v - for`和递归组件实现树形结构的展示和折叠功能,实现列表项的拖拽和排序功能,需要考虑哪些技术细节包括动画效果和用户交互逻辑以及性能优化?

如何在Vue项目中实现一个可折叠的树形菜单组件,结合v - for和递归组件实现树形结构的展示和折叠功能,实现列表项的拖拽和排序功能,需要考虑哪些技术细节包括动画效果和用户交互逻辑以及性能优化?

嘿,程序员朋友们!今天咱就来唠唠怎么在 Vue2 和 Vue3 项目里搞一个超酷的可折叠树形菜单组件。这组件能展示树形结构,还能折叠,更厉害的是能让列表项随便拖拽排序。咱们就用 v-for 和递归组件来实现树形结构展示和折叠,下面咱一步一步来,顺便说说里面的技术细节,像动画效果、用户交互逻辑还有性能优化啥的。

1. 项目准备

首先,你得有个 Vue 项目。要是还没创建,用 Vue CLI 命令就能搞定:

# 创建 Vue2 项目
vue create my-vue2-project --default
# 创建 Vue3 项目
vue create my-vue3-project --default --packageManager npm --template vue-next

2. 数据结构

咱先定义树形菜单的数据结构,一般就是一个对象数组,每个对象有 idlabelchildren 这些属性。children 也是个数组,用来存子节点。

// 树形菜单数据
const treeData = [
  {
    id: 1,
    label: '一级菜单 1',
    children: [
      {
        id: 11,
        label: '二级菜单 1-1',
        children: [
          {
            id: 111,
            label: '三级菜单 1-1-1'
          }
        ]
      }
    ]
  },
  {
    id: 2,
    label: '一级菜单 2'
  }
];

3. 实现树形菜单组件

Vue2 版本
<template>
  <!-- 树形菜单组件 -->
  <ul>
    <!-- 遍历每个菜单项 -->
    <li v-for="item in treeData" :key="item.id">
      <!-- 菜单项标题,点击切换展开状态 -->
      <span @click="toggle(item)">{{ item.label }}</span>
      <!-- 如果有子菜单且当前展开,递归渲染子菜单组件 -->
      <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
    </li>
  </ul>
</template>

<script>
export default {
  // 组件名称
  name: 'TreeMenu',
  // 接收树形数据作为 props
  props: {
    treeData: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    // 切换菜单项展开状态
    toggle(item) {
      item.expanded = !item.expanded;
    }
  }
};
</script>
Vue3 版本
<template>
  <!-- 树形菜单组件 -->
  <ul>
    <!-- 遍历每个菜单项 -->
    <li v-for="item in treeData" :key="item.id">
      <!-- 菜单项标题,点击切换展开状态 -->
      <span @click="toggle(item)">{{ item.label }}</span>
      <!-- 如果有子菜单且当前展开,递归渲染子菜单组件 -->
      <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue';
// 接收树形数据作为 props
const props = defineProps({
  treeData: {
    type: Array,
    default: () => []
  }
});

// 切换菜单项展开状态
const toggle = (item) => {
  item.expanded = !item.expanded;
};
</script>

4. 实现拖拽排序功能

为了实现拖拽排序,咱可以用 @vueuse/core 这个库,它提供了 useDraggable 这个组合式函数,能让拖拽排序变得超简单。

安装依赖
npm install @vueuse/core
修改组件代码
Vue2 版本
<template>
  <!-- 树形菜单组件 -->
  <ul>
    <!-- 遍历每个菜单项 -->
    <li v-for="item in treeData" :key="item.id" draggable="true" @dragstart="onDragStart(item)" @dragover.prevent @drop="onDrop(item)">
      <!-- 菜单项标题,点击切换展开状态 -->
      <span @click="toggle(item)">{{ item.label }}</span>
      <!-- 如果有子菜单且当前展开,递归渲染子菜单组件 -->
      <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
    </li>
  </ul>
</template>

<script>
import { useDraggable } from '@vueuse/core';

export default {
  // 组件名称
  name: 'TreeMenu',
  // 接收树形数据作为 props
  props: {
    treeData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      draggedItem: null
    };
  },
  methods: {
    // 切换菜单项展开状态
    toggle(item) {
      item.expanded = !item.expanded;
    },
    // 开始拖拽时记录拖拽的项
    onDragStart(item) {
      this.draggedItem = item;
    },
    // 拖拽放下时交换位置
    onDrop(targetItem) {
      const draggedIndex = this.treeData.indexOf(this.draggedItem);
      const targetIndex = this.treeData.indexOf(targetItem);
      [this.treeData[draggedIndex], this.treeData[targetIndex]] = [this.treeData[targetIndex], this.treeData[draggedIndex]];
      this.draggedItem = null;
    }
  }
};
</script>
Vue3 版本
<template>
  <!-- 树形菜单组件 -->
  <ul>
    <!-- 遍历每个菜单项 -->
    <li v-for="item in treeData" :key="item.id" draggable="true" @dragstart="onDragStart(item)" @dragover.prevent @drop="onDrop(item)">
      <!-- 菜单项标题,点击切换展开状态 -->
      <span @click="toggle(item)">{{ item.label }}</span>
      <!-- 如果有子菜单且当前展开,递归渲染子菜单组件 -->
      <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue';
import { useDraggable } from '@vueuse/core';

// 接收树形数据作为 props
const props = defineProps({
  treeData: {
    type: Array,
    default: () => []
  }
});

// 记录拖拽的项
const draggedItem = ref(null);

// 切换菜单项展开状态
const toggle = (item) => {
  item.expanded = !item.expanded;
};

// 开始拖拽时记录拖拽的项
const onDragStart = (item) => {
  draggedItem.value = item;
};

// 拖拽放下时交换位置
const onDrop = (targetItem) => {
  const draggedIndex = props.treeData.indexOf(draggedItem.value);
  const targetIndex = props.treeData.indexOf(targetItem);
  [props.treeData[draggedIndex], props.treeData[targetIndex]] = [props.treeData[targetIndex], props.treeData[draggedIndex]];
  draggedItem.value = null;
};
</script>

5. 技术细节

动画效果

为了让折叠和展开有动画效果,咱可以用 Vue 的过渡组件 <transition><transition-group>

<template>
  <!-- 树形菜单组件 -->
  <ul>
    <!-- 遍历每个菜单项 -->
    <li v-for="item in treeData" :key="item.id">
      <!-- 菜单项标题,点击切换展开状态 -->
      <span @click="toggle(item)">{{ item.label }}</span>
      <!-- 过渡组件,实现展开和折叠动画 -->
      <transition-group name="slide">
        <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
      </transition-group>
    </li>
  </ul>
</template>

<style scoped>
/* 展开和折叠动画 */
.slide-enter-active,
.slide-leave-active {
  transition: all 0.3s ease;
}
.slide-enter,
.slide-leave-to {
  max-height: 0;
  opacity: 0;
}
</style>
用户交互逻辑
  • 点击展开/折叠:点击菜单项标题,切换 expanded 状态。
  • 拖拽排序:开始拖拽时记录拖拽的项,放下时交换位置。
性能优化
  • 虚拟列表:如果树形菜单数据很多,可以用虚拟列表来优化性能,只渲染可见区域的菜单项。
  • 缓存计算结果:对于一些计算量大的操作,比如递归渲染子菜单,可以用缓存来避免重复计算。

好啦,现在你就有了一个功能强大的可折叠树形菜单组件,还能拖拽排序,动画效果也超棒,性能也优化得不错!赶紧在你的项目里试试吧!

如何使用Vue的v-for指令实现树形结构的展示?

在Vue里,借助v-for指令与递归组件,能够实现树形结构的展示。下面分别给出Vue 2和Vue 3的示例。

Vue 2示例

<template>
  <div id="app">
    <!-- 使用 TreeMenu 组件并传入树形数据 -->
    <TreeMenu :treeData="treeData" />
  </div>
</template>

<script>
import TreeMenu from './TreeMenu.vue';

export default {
  name: 'App',
  components: {
    TreeMenu
  },
  data() {
    return {
      // 定义树形结构的数据
      treeData: [
        {
          id: 1,
          label: '一级菜单 1',
          children: [
            {
              id: 11,
              label: '二级菜单 1-1',
              children: [
                {
                  id: 111,
                  label: '三级菜单 1-1-1'
                }
              ]
            }
          ]
        },
        {
          id: 2,
          label: '一级菜单 2'
        }
      ]
    };
  }
};
</script>    
<template>
  <ul>
    <!-- 使用 v-for 指令遍历每个菜单项 -->
    <li v-for="item in treeData" :key="item.id">
      <!-- 显示菜单项的标签 -->
      <span>{{ item.label }}</span>
      <!-- 如果菜单项有子项,递归渲染 TreeMenu 组件 -->
      <TreeMenu v-if="item.children && item.children.length > 0" :treeData="item.children"></TreeMenu>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'TreeMenu',
  props: {
    // 接收树形数据作为 prop
    treeData: {
      type: Array,
      default: () => []
    }
  }
};
</script>    

Vue 3示例

<template>
  <div id="app">
    <!-- 使用 TreeMenu 组件并传入树形数据 -->
    <TreeMenu :treeData="treeData" />
  </div>
</template>

<script setup>
import TreeMenu from './TreeMenu.vue';
import { ref } from 'vue';

// 定义树形结构的数据
const treeData = ref([
  {
    id: 1,
    label: '一级菜单 1',
    children: [
      {
        id: 11,
        label: '二级菜单 1-1',
        children: [
          {
            id: 111,
            label: '三级菜单 1-1-1'
          }
        ]
      }
    ]
  },
  {
    id: 2,
    label: '一级菜单 2'
  }
]);
</script>    
<template>
  <ul>
    <!-- 使用 v-for 指令遍历每个菜单项 -->
    <li v-for="item in treeData" :key="item.id">
      <!-- 显示菜单项的标签 -->
      <span>{{ item.label }}</span>
      <!-- 如果菜单项有子项,递归渲染 TreeMenu 组件 -->
      <TreeMenu v-if="item.children && item.children.length > 0" :treeData="item.children"></TreeMenu>
    </li>
  </ul>
</template>

<script setup>
import { defineProps } from 'vue';

// 定义接收的 prop
const props = defineProps({
  treeData: {
    type: Array,
    default: () => []
  }
});
</script>    

代码解释

  • v-for 指令:在 TreeMenu 组件里,v-for 指令用于遍历 treeData 数组中的每个菜单项。
  • 递归组件:当菜单项存在子项时,会递归调用 TreeMenu 组件,这样就能展示出完整的树形结构。
  • key 属性:为保证渲染效率,给 v-for 指令添加了 key 属性,其值为每个菜单项的 id

通过上述方式,你就能在Vue项目里利用 v-for 指令实现树形结构的展示。

那么,如何优化可折叠的树形菜单组件的性能

在 Vue2 和 Vue3 项目中,可从多方面对可折叠的树形菜单组件性能进行优化,下面分别详细介绍相关策略。

虚拟列表技术

当树形菜单数据量庞大时,渲染所有节点会显著降低性能,此时可采用虚拟列表技术,仅渲染可视区域内的节点。

Vue2 示例
<template>
  <div>
    <!-- 滚动容器 -->
    <div ref="scrollContainer" @scroll="handleScroll" style="height: 300px; overflow-y: auto;">
      <!-- 占位元素,高度为所有项的总高度 -->
      <div :style="{ height: totalHeight + 'px' }"></div>
      <!-- 渲染可见区域的项 -->
      <ul>
        <li v-for="item in visibleItems" :key="item.id">
          <span @click="toggle(item)">{{ item.label }}</span>
          <!-- 递归渲染子菜单 -->
          <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualTreeMenu',
  props: {
    treeData: {
      type: Array,
      default: () => []
    },
    itemHeight: {
      type: Number,
      default: 30
    }
  },
  data() {
    return {
      visibleItems: [],
      scrollTop: 0
    };
  },
  computed: {
    totalHeight() {
      return this.treeData.length * this.itemHeight;
    }
  },
  mounted() {
    this.updateVisibleItems();
  },
  methods: {
    toggle(item) {
      item.expanded = !item.expanded;
    },
    handleScroll() {
      this.scrollTop = this.$refs.scrollContainer.scrollTop;
      this.updateVisibleItems();
    },
    updateVisibleItems() {
      const startIndex = Math.floor(this.scrollTop / this.itemHeight);
      const endIndex = startIndex + Math.ceil(this.$refs.scrollContainer.clientHeight / this.itemHeight);
      this.visibleItems = this.treeData.slice(startIndex, endIndex);
    }
  }
};
</script>
Vue3 示例
<template>
  <div>
    <!-- 滚动容器 -->
    <div ref="scrollContainer" @scroll="handleScroll" style="height: 300px; overflow-y: auto;">
      <!-- 占位元素,高度为所有项的总高度 -->
      <div :style="{ height: totalHeight + 'px' }"></div>
      <!-- 渲染可见区域的项 -->
      <ul>
        <li v-for="item in visibleItems" :key="item.id">
          <span @click="toggle(item)">{{ item.label }}</span>
          <!-- 递归渲染子菜单 -->
          <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue';
import TreeMenu from './TreeMenu.vue';

const props = defineProps({
  treeData: {
    type: Array,
    default: () => []
  },
  itemHeight: {
    type: Number,
    default: 30
  }
});

const scrollContainer = ref(null);
const visibleItems = ref([]);
const scrollTop = ref(0);

const totalHeight = computed(() => props.treeData.length * props.itemHeight);

const toggle = (item) => {
  item.expanded = !item.expanded;
};

const handleScroll = () => {
  scrollTop.value = scrollContainer.value.scrollTop;
  updateVisibleItems();
};

const updateVisibleItems = () => {
  const startIndex = Math.floor(scrollTop.value / props.itemHeight);
  const endIndex = startIndex + Math.ceil(scrollContainer.value.clientHeight / props.itemHeight);
  visibleItems.value = props.treeData.slice(startIndex, endIndex);
};

onMounted(() => {
  updateVisibleItems();
});
</script>

缓存计算结果

对于一些计算量大的操作,比如递归渲染子菜单,可以使用缓存来避免重复计算。

Vue2 示例
<template>
  <ul>
    <li v-for="item in treeData" :key="item.id">
      <span @click="toggle(item)">{{ item.label }}</span>
      <!-- 使用缓存的渲染结果 -->
      <TreeMenu v-if="item.children && item.expanded" :treeData="getCachedChildren(item)" />
    </li>
  </ul>
</template>

<script>
export default {
  name: 'CachedTreeMenu',
  props: {
    treeData: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      cache: {}
    };
  },
  methods: {
    toggle(item) {
      item.expanded = !item.expanded;
    },
    getCachedChildren(item) {
      if (!this.cache[item.id]) {
        this.cache[item.id] = item.children;
      }
      return this.cache[item.id];
    }
  }
};
</script>
Vue3 示例
<template>
  <ul>
    <li v-for="item in treeData" :key="item.id">
      <span @click="toggle(item)">{{ item.label }}</span>
      <!-- 使用缓存的渲染结果 -->
      <TreeMenu v-if="item.children && item.expanded" :treeData="getCachedChildren(item)" />
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue';
import TreeMenu from './TreeMenu.vue';

const props = defineProps({
  treeData: {
    type: Array,
    default: () => []
  }
});

const cache = ref({});

const toggle = (item) => {
  item.expanded = !item.expanded;
};

const getCachedChildren = (item) => {
  if (!cache.value[item.id]) {
    cache.value[item.id] = item.children;
  }
  return cache.value[item.id];
};
</script>

事件委托

若树形菜单有很多交互事件,使用事件委托可减少事件监听器的数量。

Vue2 示例
<template>
  <ul @click="handleClick">
    <li v-for="item in treeData" :key="item.id" :data-id="item.id">
      <span>{{ item.label }}</span>
      <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'EventDelegationTreeMenu',
  props: {
    treeData: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    handleClick(event) {
      const targetId = event.target.dataset.id;
      if (targetId) {
        const item = this.findItemById(targetId);
        if (item) {
          item.expanded = !item.expanded;
        }
      }
    },
    findItemById(id) {
      function search(items) {
        for (let i = 0; i < items.length; i++) {
          if (items[i].id === parseInt(id)) {
            return items[i];
          }
          if (items[i].children) {
            const found = search(items[i].children);
            if (found) {
              return found;
            }
          }
        }
        return null;
      }
      return search(this.treeData);
    }
  }
};
</script>
Vue3 示例
<template>
  <ul @click="handleClick">
    <li v-for="item in treeData" :key="item.id" :data-id="item.id">
      <span>{{ item.label }}</span>
      <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue';
import TreeMenu from './TreeMenu.vue';

const props = defineProps({
  treeData: {
    type: Array,
    default: () => []
  }
});

const handleClick = (event) => {
  const targetId = event.target.dataset.id;
  if (targetId) {
    const item = findItemById(targetId);
    if (item) {
      item.expanded = !item.expanded;
    }
  }
};

const findItemById = (id) => {
  function search(items) {
    for (let i = 0; i < items.length; i++) {
      if (items[i].id === parseInt(id)) {
        return items[i];
      }
      if (items[i].children) {
        const found = search(items[i].children);
        if (found) {
          return found;
        }
      }
    }
    return null;
  }
  return search(props.treeData);
};
</script>

减少不必要的渲染

运用 v-if 而非 v-show,只有在满足条件时才渲染组件,避免不必要的 DOM 操作。

懒加载

对于子菜单,采用懒加载方式,仅在展开时加载子菜单数据,降低初始加载时间。

优化动画效果

采用 CSS 动画替代 JavaScript 动画,因为 CSS 动画性能更佳。例如,使用 transitionanimation 属性实现折叠展开动画。

通过上述这些优化策略,能有效提升可折叠树形菜单组件在 Vue2 和 Vue3 项目中的性能。

Vue3中使用虚拟列表技术和Vue2有什么区别?

在 Vue3 中使用虚拟列表技术与 Vue2 存在一些区别,主要体现在语法、响应式原理、组合式 API 与选项式 API 的使用等方面,下面为你详细分析:

1. 响应式系统

  • Vue2:使用 Object.defineProperty() 来实现响应式,当数据发生变化时,Vue2 会自动更新与之绑定的 DOM。但它存在一些局限性,比如无法检测对象属性的添加或删除。
  • Vue3:采用 Proxy 对象实现响应式,能更全面地检测对象的变化,包括属性的添加、删除等。这使得在虚拟列表技术中,对数据的响应式处理更加高效和灵活。

2. 语法与 API

选项式 API(Vue2)

Vue2 使用选项式 API,将组件的不同方面(如数据、方法、生命周期钩子等)分组到不同的选项中。以下是 Vue2 中实现虚拟列表的示例代码片段:

<template>
  <div>
    <div ref="scrollContainer" @scroll="handleScroll" style="height: 300px; overflow-y: auto;">
      <div :style="{ height: totalHeight + 'px' }"></div>
      <ul>
        <li v-for="item in visibleItems" :key="item.id">{{ item.label }}</li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualList',
  props: {
    listData: {
      type: Array,
      default: () => []
    },
    itemHeight: {
      type: Number,
      default: 30
    }
  },
  data() {
    return {
      visibleItems: [],
      scrollTop: 0
    };
  },
  computed: {
    totalHeight() {
      return this.listData.length * this.itemHeight;
    }
  },
  mounted() {
    this.updateVisibleItems();
  },
  methods: {
    handleScroll() {
      this.scrollTop = this.$refs.scrollContainer.scrollTop;
      this.updateVisibleItems();
    },
    updateVisibleItems() {
      const startIndex = Math.floor(this.scrollTop / this.itemHeight);
      const endIndex = startIndex + Math.ceil(this.$refs.scrollContainer.clientHeight / this.itemHeight);
      this.visibleItems = this.listData.slice(startIndex, endIndex);
    }
  }
};
</script>
组合式 API(Vue3)

Vue3 引入了组合式 API,允许开发者以函数的形式组织逻辑,提高了代码的复用性和可维护性。以下是 Vue3 中实现虚拟列表的示例代码片段:

<template>
  <div>
    <div ref="scrollContainer" @scroll="handleScroll" style="height: 300px; overflow-y: auto;">
      <div :style="{ height: totalHeight + 'px' }"></div>
      <ul>
        <li v-for="item in visibleItems" :key="item.id">{{ item.label }}</li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue';

const props = defineProps({
  listData: {
    type: Array,
    default: () => []
  },
  itemHeight: {
    type: Number,
    default: 30
  }
});

const scrollContainer = ref(null);
const visibleItems = ref([]);
const scrollTop = ref(0);

const totalHeight = computed(() => props.listData.length * props.itemHeight);

const handleScroll = () => {
  scrollTop.value = scrollContainer.value.scrollTop;
  updateVisibleItems();
};

const updateVisibleItems = () => {
  const startIndex = Math.floor(scrollTop.value / props.itemHeight);
  const endIndex = startIndex + Math.ceil(scrollContainer.value.clientHeight / props.itemHeight);
  visibleItems.value = props.listData.slice(startIndex, endIndex);
};

onMounted(() => {
  updateVisibleItems();
});
</script>

3. 生命周期钩子

  • Vue2:使用 beforeCreatecreatedbeforeMountmounted 等选项式生命周期钩子来管理组件的生命周期。
  • Vue3:在组合式 API 中,使用 onBeforeMountonMountedonBeforeUpdateonUpdated 等函数式的生命周期钩子,使代码结构更加清晰。

4. 依赖注入

  • Vue2:使用 provideinject 选项来实现依赖注入,但在使用时需要在组件选项中明确声明。
  • Vue3:在组合式 API 中,使用 provideinject 函数,使用起来更加灵活,可以在任何地方进行依赖注入。

5. 模板引用

  • Vue2:通过 this.$refs 来访问模板引用。
  • Vue3:在组合式 API 中,使用 ref 来创建模板引用,通过 .value 来访问引用的值。

综上所述,Vue3 在虚拟列表技术的实现上,由于其新的响应式系统、组合式 API 等特性,使得代码更加简洁、灵活和易于维护。

除了虚拟列表技术,以下还有一些优化树形菜单组件性能的方法:

数据层面优化

1. 数据扁平化与缓存
  • 扁平化处理:树形结构的数据在操作时可能涉及大量递归,会增加性能开销。将树形数据扁平化存储,例如存储为一个数组,每个节点包含父节点信息。这样在查找、排序等操作时,无需递归遍历整个树,直接操作数组即可,提高处理效率。
  • 缓存计算结果:对于一些需要重复计算的结果,如节点的层级、展开状态等,进行缓存。当再次需要这些数据时,直接从缓存中获取,避免重复计算。
2. 懒加载数据
  • 树形菜单可能存在大量的子节点,如果一次性加载所有数据,会导致初始加载时间过长。采用懒加载策略,只有当用户展开某个节点时,才去请求该节点的子节点数据。这样可以显著减少初始加载的数据量,提高页面加载速度。

渲染层面优化

1. 减少不必要的渲染
  • v-if 与 v-show 合理使用v-if 是真正的条件渲染,当条件不满足时,元素不会被渲染到 DOM 中;而 v-show 只是通过 CSS 的 display 属性来控制元素的显示与隐藏。对于树形菜单中那些可能很少显示的子菜单,使用 v-if 可以避免不必要的 DOM 渲染。
  • shouldComponentUpdate 或 watch 监听:在 Vue 中,对于一些复杂的树形菜单组件,可以通过 watch 监听数据的变化,只有当数据真正发生变化时才进行组件的重新渲染。在 Vue2 中还可以通过 shouldComponentUpdate 生命周期钩子来手动控制组件是否需要重新渲染。
2. 渲染优化技巧
  • 批量更新:当需要对树形菜单进行大量数据更新时,尽量采用批量更新的方式。例如,在 Vue 中可以使用 $nextTick 将多次数据更新合并为一次,减少不必要的 DOM 操作和重绘。
  • 虚拟 DOM 优化:虽然 Vue 已经对虚拟 DOM 进行了优化,但在编写代码时,也可以注意一些细节。比如,尽量保持节点的 key 唯一且稳定,避免频繁的 key 变化导致虚拟 DOM 比较时的性能损耗。

交互层面优化

1. 事件委托
  • 如果树形菜单有大量的交互事件(如点击、展开、折叠等),为每个节点都绑定事件监听器会增加内存开销。可以采用事件委托的方式,将事件监听器绑定到父容器上,通过事件冒泡机制来处理具体的事件。这样可以显著减少事件监听器的数量,提高性能。
2. 动画优化
  • CSS 动画替代 JavaScript 动画:CSS 动画由浏览器的渲染引擎直接处理,性能通常比 JavaScript 动画更好。在实现树形菜单的展开和折叠动画时,尽量使用 CSS 的 transitionanimation 属性。
  • 优化动画帧率:避免过于复杂或帧率过高的动画效果,过高的帧率会增加 CPU 负担。可以通过设置合适的动画时长和帧率,在保证动画效果的同时,提高性能。

其他优化方法

1. 代码分割与按需加载
  • 将树形菜单组件拆分成多个小的组件,根据需要进行按需加载。例如,对于一些不常用的功能组件,可以在用户需要时再进行加载,减少初始加载的代码量。
2. 服务端渲染(SSR)或静态站点生成(SSG)
  • 如果树形菜单的数据量较大,且更新频率较低,可以考虑使用服务端渲染或静态站点生成。这样可以在服务器端预先渲染好页面内容,减少客户端的渲染负担,提高页面加载速度。

**在 Vue2 和 Vue3 项目里,运用防抖和节流函数能够有效优化可折叠树形菜单组件的性能。下面分别介绍这两种函数在不同版本 Vue 中的应用方式。

防抖函数

防抖函数的作用是在一定时间内,只有最后一次触发事件才会执行相应的操作。在树形菜单里,像搜索功能、节点展开折叠的动画触发等场景,使用防抖函数可以避免短时间内的频繁操作导致性能问题。

Vue2 中使用防抖函数
<template>
  <div>
    <!-- 搜索输入框 -->
    <input v-model="searchQuery" @input="debouncedSearch" placeholder="搜索节点">
    <ul>
      <!-- 遍历树形数据 -->
      <li v-for="item in filteredTreeData" :key="item.id">
        <!-- 显示节点标签 -->
        <span @click="toggle(item)">{{ item.label }}</span>
        <!-- 递归渲染子菜单 -->
        <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import TreeMenu from './TreeMenu.vue';

// 定义防抖函数
function debounce(func, delay) {
  let timer = null;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

const props = defineProps({
  // 接收树形数据
  treeData: {
    type: Array,
    default: () => []
  }
});

// 搜索关键词
const searchQuery = ref('');
// 过滤后的树形数据
const filteredTreeData = ref(props.treeData);

// 切换节点展开状态
const toggle = (item) => {
  item.expanded = !item.expanded;
};

// 搜索方法
const search = () => {
  // 根据搜索关键词过滤树形数据
  filteredTreeData.value = props.treeData.filter(item =>
    item.label.includes(searchQuery.value)
  );
};

// 防抖后的搜索方法
const debouncedSearch = debounce(search, 300);
</script>    
Vue3 中使用防抖函数
<template>
  <div>
    <!-- 搜索输入框 -->
    <input v-model="searchQuery" @input="debouncedSearch" placeholder="搜索节点">
    <ul>
      <!-- 遍历树形数据 -->
      <li v-for="item in filteredTreeData" :key="item.id">
        <!-- 显示节点标签 -->
        <span @click="toggle(item)">{{ item.label }}</span>
        <!-- 递归渲染子菜单 -->
        <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import TreeMenu from './TreeMenu.vue';

// 定义防抖函数
function debounce(func, delay) {
  let timer = null;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

const props = defineProps({
  // 接收树形数据
  treeData: {
    type: Array,
    default: () => []
  }
});

// 搜索关键词
const searchQuery = ref('');
// 过滤后的树形数据
const filteredTreeData = ref(props.treeData);

// 切换节点展开状态
const toggle = (item) => {
  item.expanded = !item.expanded;
};

// 搜索方法
const search = () => {
  // 根据搜索关键词过滤树形数据
  filteredTreeData.value = props.treeData.filter(item =>
    item.label.includes(searchQuery.value)
  );
};

// 防抖后的搜索方法
const debouncedSearch = debounce(search, 300);
</script>    

节流函数

节流函数会在一定时间内只执行一次操作。在树形菜单的滚动加载、节点拖拽排序等场景中使用节流函数,能防止短时间内大量事件触发,减轻性能负担。

Vue2 中使用节流函数
<template>
  <div ref="scrollContainer" @scroll="throttledScroll" style="height: 300px; overflow-y: auto;">
    <ul>
      <!-- 遍历树形数据 -->
      <li v-for="item in treeData" :key="item.id">
        <!-- 显示节点标签 -->
        <span @click="toggle(item)">{{ item.label }}</span>
        <!-- 递归渲染子菜单 -->
        <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
      </li>
    </ul>
  </div>
</template>

<script>
// 定义节流函数
function throttle(func, delay) {
  let timer = null;
  return function() {
    if (!timer) {
      const context = this;
      const args = arguments;
      func.apply(context, args);
      timer = setTimeout(() => {
        timer = null;
      }, delay);
    }
  };
}

export default {
  name: 'TreeMenu',
  props: {
    // 接收树形数据
    treeData: {
      type: Array,
      default: () => []
    }
  },
  methods: {
    // 切换节点展开状态
    toggle(item) {
      item.expanded = !item.expanded;
    },
    // 滚动处理方法
    handleScroll() {
      // 处理滚动逻辑
      console.log('Scroll event triggered');
    },
    // 节流后的滚动处理方法
    throttledScroll: null
  },
  created() {
    // 初始化节流后的滚动处理方法
    this.throttledScroll = throttle(this.handleScroll, 300);
  }
};
</script>    
Vue3 中使用节流函数
<template>
  <div ref="scrollContainer" @scroll="throttledScroll" style="height: 300px; overflow-y: auto;">
    <ul>
      <!-- 遍历树形数据 -->
      <li v-for="item in treeData" :key="item.id">
        <!-- 显示节点标签 -->
        <span @click="toggle(item)">{{ item.label }}</span>
        <!-- 递归渲染子菜单 -->
        <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import TreeMenu from './TreeMenu.vue';

// 定义节流函数
function throttle(func, delay) {
  let timer = null;
  return function() {
    if (!timer) {
      const context = this;
      const args = arguments;
      func.apply(context, args);
      timer = setTimeout(() => {
        timer = null;
      }, delay);
    }
  };
}

const props = defineProps({
  // 接收树形数据
  treeData: {
    type: Array,
    default: () => []
  }
});

const scrollContainer = ref(null);

// 切换节点展开状态
const toggle = (item) => {
  item.expanded = !item.expanded;
};

// 滚动处理方法
const handleScroll = () => {
  // 处理滚动逻辑
  console.log('Scroll event triggered');
};

// 节流后的滚动处理方法
const throttledScroll = throttle(handleScroll, 300);
</script>    

区别

在 Vue2 和 Vue3 项目中使用防抖和节流函数优化可折叠树形菜单组件性能时,主要区别体现在代码结构、响应式系统、生命周期钩子以及 API 的使用方式上,下面为你详细介绍:

1. 代码结构与 API 风格

Vue2(选项式 API)

Vue2 使用选项式 API,将组件的不同功能(如数据、方法、生命周期钩子等)划分到不同的选项中。在使用防抖和节流函数时,需要在 methods 选项中定义方法,并在 created 或其他生命周期钩子中初始化防抖或节流后的函数。

<template>
  <div>
    <input v-model="searchQuery" @input="debouncedSearch" placeholder="Search">
    <!-- 树形菜单内容 -->
  </div>
</template>

<script>
function debounce(func, delay) {
  let timer = null;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

export default {
  data() {
    return {
      searchQuery: ''
    };
  },
  methods: {
    search() {
      // 搜索逻辑
    },
    debouncedSearch: null
  },
  created() {
    this.debouncedSearch = debounce(this.search, 300);
  }
};
</script>
Vue3(组合式 API)

Vue3 引入了组合式 API,允许开发者以函数的形式组织逻辑。在使用防抖和节流函数时,可以在 <script setup> 中直接定义和使用,代码结构更加简洁和灵活。

<template>
  <div>
    <input v-model="searchQuery" @input="debouncedSearch" placeholder="Search">
    <!-- 树形菜单内容 -->
  </div>
</template>

<script setup>
import { ref } from 'vue';

function debounce(func, delay) {
  let timer = null;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

const searchQuery = ref('');

const search = () => {
  // 搜索逻辑
};

const debouncedSearch = debounce(search, 300);
</script>

2. 响应式系统

Vue2

Vue2 使用 Object.defineProperty() 实现响应式,在使用防抖和节流函数时,需要注意方法内部对数据的访问和修改方式,通过 this 来访问组件的数据和方法。

Vue3

Vue3 使用 Proxy 实现响应式,数据以 refreactive 的形式进行封装。在防抖和节流函数中,访问和修改响应式数据时需要使用 .value 来获取或设置值。

3. 生命周期钩子

Vue2

使用选项式生命周期钩子(如 createdmounted 等)来初始化防抖和节流函数。

Vue3

在组合式 API 中,使用 onMountedonCreated 等函数式生命周期钩子。不过,对于简单的防抖和节流函数初始化,可能不需要显式使用生命周期钩子。

4. 依赖注入和作用域

Vue2

依赖注入通过 provideinject 选项实现,作用域是基于组件实例的。在使用防抖和节流函数时,需要确保方法和数据在正确的组件实例中访问。

Vue3

在组合式 API 中,依赖注入通过 provideinject 函数实现,作用域更加灵活。可以在任何需要的地方注入和使用依赖,使得防抖和节流函数的使用更加方便。

5. 性能和维护性

Vue2

由于选项式 API 的结构,当组件变得复杂时,代码可能会变得冗长和难以维护。在使用防抖和节流函数时,需要在多个选项中进行配置,增加了代码的复杂度。

Vue3

组合式 API 使得代码更加模块化和可复用,防抖和节流函数可以作为独立的逻辑单元进行封装和复用,提高了代码的维护性和性能。

综上所述,Vue3 在使用防抖和节流函数优化树形菜单组件性能时,具有更加简洁、灵活和可维护的代码结构,同时利用了新的响应式系统和生命周期钩子,提升了开发效率和性能。

总结

通过在 Vue2 和 Vue3 项目里运用防抖和节流函数,能够有效优化可折叠树形菜单组件的性能,避免因频繁操作引发的性能问题。**

如何测试防抖和节流函数在树形菜单组件中的优化效果?

要测试防抖和节流函数在树形菜单组件中的优化效果,可以从性能指标监测、功能测试、用户体验评估等多个方面入手,以下为你详细介绍具体的测试方法:

1. 性能指标监测

浏览器开发者工具
  • CPU 使用率:在 Chrome 浏览器的开发者工具中,选择“Performance”面板,开始录制性能数据。在树形菜单组件上进行频繁的操作(如快速展开/折叠节点、快速输入搜索关键词等),观察 CPU 使用率的变化。使用防抖和节流函数前后,对比 CPU 使用率的峰值和平均值。优化后,CPU 使用率应该会明显降低,尤其是在高频操作时。
  • 内存占用:同样在“Performance”面板中,可以观察内存占用情况。操作树形菜单组件一段时间后,查看内存的增长趋势。如果防抖和节流函数有效,内存的增长应该会更加平稳,避免因频繁操作导致的内存泄漏或过度增长。
  • 帧率(FPS):帧率反映了页面的流畅度。在开发者工具的“Performance”面板中,可以看到帧率的实时变化。在进行操作时,帧率应该保持在一个较高且稳定的水平。使用防抖和节流函数后,页面在高频操作下的帧率应该会有所提升,减少卡顿现象。
性能分析工具
  • Lighthouse:这是一个开源的自动化工具,可用于分析网页的性能、可访问性、最佳实践等方面。在 Chrome 浏览器中,可以通过开发者工具的“Lighthouse”面板对包含树形菜单组件的页面进行性能分析。查看各项性能指标得分,如“First Contentful Paint”(首次内容绘制)、“Largest Contentful Paint”(最大内容绘制)等,对比使用防抖和节流函数前后的得分变化,评估优化效果。

2. 功能测试

单元测试
  • 使用测试框架:可以使用 Vue 官方推荐的测试框架,如 Jest 和 Vue Test Utils。编写单元测试用例,对防抖和节流函数进行单独测试,确保它们的基本功能正常。例如,对于防抖函数,测试在规定时间内多次触发事件,只有最后一次触发会执行相应的操作;对于节流函数,测试在规定时间内只执行一次操作。
// 防抖函数测试示例
import { debounce } from './debounce';

describe('debounce function', () => {
  jest.useFakeTimers();

  it('should only call the function once after the delay', () => {
    const mockFunction = jest.fn();
    const debouncedFunction = debounce(mockFunction, 300);

    // 多次调用防抖函数
    debouncedFunction();
    debouncedFunction();
    debouncedFunction();

    // 推进时间到延迟结束
    jest.advanceTimersByTime(300);

    // 断言函数只被调用了一次
    expect(mockFunction).toHaveBeenCalledTimes(1);
  });
});
集成测试
  • 测试组件交互:编写集成测试用例,测试防抖和节流函数在树形菜单组件中的实际应用效果。模拟用户在树形菜单上的各种操作,如点击展开/折叠按钮、输入搜索关键词等,检查组件的响应是否符合预期。例如,在搜索功能中,使用防抖函数后,输入关键词时不应该立即触发搜索请求,而是在用户停止输入一段时间后才触发。

3. 用户体验评估

手动测试
  • 实际操作体验:开发人员和测试人员亲自在不同的设备和浏览器上对树形菜单组件进行操作,感受使用防抖和节流函数前后的用户体验差异。重点关注在高频操作下,组件的响应速度、流畅度和稳定性。例如,在快速滚动树形菜单时,是否存在明显的卡顿现象;在快速输入搜索关键词时,搜索结果的显示是否及时且准确。
用户反馈
  • 收集用户意见:将包含树形菜单组件的页面部署到测试环境或小范围用户群体中,收集用户的反馈意见。了解用户在使用过程中是否感受到性能的提升,是否遇到任何问题或不便。根据用户反馈,进一步优化防抖和节流函数的参数设置或实现方式。

4. 数据统计与分析

日志记录
  • 记录操作数据:在树形菜单组件中添加日志记录功能,记录用户的操作行为和时间戳。例如,记录用户点击展开/折叠按钮的时间、输入搜索关键词的时间等。通过分析这些日志数据,可以了解用户的操作频率和模式,评估防抖和节流函数是否能够有效应对这些操作。
对比测试数据
  • 多次测试取平均值:进行多次测试,每次测试都记录性能指标和功能测试结果。对这些测试数据进行统计分析,取平均值和标准差,以更准确地评估防抖和节流函数的优化效果。同时,对比不同版本的树形菜单组件(使用防抖和节流函数前后)的测试数据,观察各项指标的变化趋势。

最后,一套可以在vue2和vue3项目开发当中都能使用的加接口数据请求方式以及性能优化完整版的可以即插即用的可折叠的树形菜单组件及样式,以及如何正确引入并使用该组件方法和步骤:

下面为你提供一个在 Vue2 和 Vue3 项目中都能使用的可折叠树形菜单组件,包含接口数据请求、性能优化,并且提供引入和使用该组件的方法和步骤。

组件实现

1. 数据请求与工具函数

创建一个工具函数来封装数据请求,这里以 axios 为例。首先安装 axios

npm install axios

创建 api.js 文件:

import axios from 'axios';

export const fetchTreeData = async () => {
    try {
        const response = await axios.get('your_api_url');
        return response.data;
    } catch (error) {
        console.error('Error fetching tree data:', error);
        return [];
    }
};
2. 防抖和节流函数

创建 utils.js 文件:

// 防抖函数
export function debounce(func, delay) {
    let timer = null;
    return function() {
        const context = this;
        const args = arguments;
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(context, args);
        }, delay);
    };
}

// 节流函数
export function throttle(func, delay) {
    let timer = null;
    return function() {
        if (!timer) {
            const context = this;
            const args = arguments;
            func.apply(context, args);
            timer = setTimeout(() => {
                timer = null;
            }, delay);
        }
    };
}
3. 树形菜单组件
Vue2 版本
<template>
    <ul>
        <li v-for="item in treeData" :key="item.id">
            <span @click="toggle(item)">{{ item.label }}</span>
            <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
        </li>
    </ul>
</template>

<script>
import { fetchTreeData } from './api';
import { debounce, throttle } from './utils';

export default {
    name: 'TreeMenu',
    props: {
        treeData: {
            type: Array,
            default: () => []
        }
    },
    data() {
        return {
            localTreeData: this.treeData
        };
    },
    created() {
        this.fetchData();
    },
    methods: {
        toggle(item) {
            item.expanded = !item.expanded;
        },
        async fetchData() {
            const data = await fetchTreeData();
            this.localTreeData = data;
        },
        debouncedFetchData: debounce(this.fetchData, 300),
        throttledToggle: throttle(this.toggle, 300)
    }
};
</script>

<style scoped>
ul {
    list-style-type: none;
    padding-left: 20px;
}
li {
    cursor: pointer;
}
</style>    
Vue3 版本
<template>
    <ul>
        <li v-for="item in treeData" :key="item.id">
            <span @click="toggle(item)">{{ item.label }}</span>
            <TreeMenu v-if="item.children && item.expanded" :treeData="item.children"></TreeMenu>
        </li>
    </ul>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { fetchTreeData } from './api';
import { debounce, throttle } from './utils';

const props = defineProps({
    treeData: {
        type: Array,
        default: () => []
    }
});

const localTreeData = ref(props.treeData);

const toggle = (item) => {
    item.expanded = !item.expanded;
};

const fetchData = async () => {
    const data = await fetchTreeData();
    localTreeData.value = data;
};

const debouncedFetchData = debounce(fetchData, 300);
const throttledToggle = throttle(toggle, 300);

onMounted(() => {
    debouncedFetchData();
});
</script>

<style scoped>
ul {
    list-style-type: none;
    padding-left: 20px;
}
li {
    cursor: pointer;
}
</style>    

引入和使用组件

Vue2 项目
  1. 创建组件文件:将上述 TreeMenu.vue 文件保存到项目的 src/components 目录下。
  2. 引入组件:在需要使用树形菜单的组件中引入 TreeMenu.vue
<template>
    <div>
        <TreeMenu />
    </div>
</template>

<script>
import TreeMenu from './components/TreeMenu.vue';

export default {
    components: {
        TreeMenu
    }
};
</script>
Vue3 项目
  1. 创建组件文件:同样将 TreeMenu.vue 文件保存到项目的 src/components 目录下。
  2. 引入组件:在需要使用树形菜单的组件中引入 TreeMenu.vue
<template>
    <div>
        <TreeMenu />
    </div>
</template>

<script setup>
import TreeMenu from './components/TreeMenu.vue';
</script>

总结

以上提供的树形菜单组件通过封装接口数据请求、使用防抖和节流函数进行性能优化,并且在 Vue2 和 Vue3 项目中都可以即插即用。你只需要将组件文件放置在项目中合适的位置,并按照上述步骤引入和使用即可。记得将 api.js 中的 your_api_url 替换为实际的接口地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端布洛芬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值