信息发布→ 登录 注册 退出

Vue组件化开发的必备技能之组件递归

发布时间:2026-01-11

点击量:
目录
  • 前言
  • 效果展示
  • 渲染完整数据
    • 效果如下
  • 获取节点数据
    • 效果如下
  • 动态展开收起
    • 效果如下
  • 完整代码
    • 效果如下
  • 总结

    前言

    不知道大家有没遇到过这样的场景:渲染列表数据的时候,列表的子项还是列表。如果层级少尚且可以用几个for循环搞定,但是层级多或者层级不确定就有点无从下手了。

    其实这就是树形结构数据,像常见的组织架构图,文件夹目录,导航菜单等都属于这种结构。很多组件库都带有树形组件,但往往样式不是我们想要的,改起来也非常的费劲。那么,如何自己渲染这些数据呢?答案就是——组件递归!

    效果展示

    以上就是使用组件递归,并加入简单交互的展示效果。点击节点会在控制台输出节点对应的数据,如果有子节点,则会展开或收起子节点。接下来我们就看看如何实现以上效果吧!

    渲染完整数据

    渲染数据这一步非常简单,首先是把树形结构封装成一个列表组件,其次判断每一项有没有子节点,如果有子节点,再使用自身组件去渲染就可以了。

    src/components/myTree.vue

    <template>
      <div class="tree-item">
        <div v-for="item in treeData" :key="item.id">
          <div class="item-title">{{ item.name }}</div>
          <div v-if="item.children && item.children.length" class="item-childen">
            <my-tree :treeData="item.children"></my-tree>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'myTree',
      props: {
        treeData: {
          type: Array,
          default: () => []
        }
      }
    }
    </script>
    
    <style lang="scss" scoped>
    .tree-item {
      .item-title {
        padding: 4px 8px;
      }
      .item-childen {
        padding-left: 20px;
      }
    }
    </style>

    src/App.vue

    <template>
      <my-tree :tree-data="treeData"></my-tree>
    </template>
    
    <script>
    const treeData = [
      { id: 1, name: '一级1' },
      {
        id: 2,
        name: '一级2',
        children: [
          { id: 3, name: '二级2-1' },
          { id: 4, name: '二级2-2' }
        ]
      },
      {
        id: 5,
        name: '一级3',
        children: [
          {
            id: 6,
            name: '二级3-1',
            children: [
              { id: 7, name: '三级3-1-1' },
              { id: 8, name: '三级3-1-2' }
            ]
          },
          { id: 9, name: '二级3-2' },
          { id: 10, name: '二级3-3' }
        ]
      }
    ]
    import myTree from '@/components/myTree.vue'
    export default {
      components: {
        myTree
      },
      data() {
        return {
          treeData: treeData
        }
      }
    }
    </script>

    效果如下

    获取节点数据

    接下来我们要做的是,点击节点时在控制台输出对应的数据。首先我们使用 $emit,将一级节点的 item 传递出去,也就是子传父的方法,相信大家都会。

    其次是将内层节点的数据传递出去,同样使用子传父的方法,只是我们需要给组件里面的 my-tree 绑定@node-click="$emit('node-click', $event)",这样每次子级每次都可以调用父级的 node-click 方法,父级又调用它的父级 node-click 方法,最终调的都是最外层的 node-click 方法,我们只需要在这个过程中,把数据传递过去就可以了。这块有点绕,相信大家多看几遍应该可以看懂。修改如下:

    src/components/myTree.vue

    <div class="item-title" @click="itemNodeClick(item)">{{ item.name }}</div>
    <div v-if="item.children && item.children.length" class="item-childen">
      <my-tree
        :treeData="item.children"
        @node-click="$emit('node-click', $event)"
      ></my-tree>
    </div>
    ...
    itemNodeClick(item) {
      this.$emit("node-click", item)
    }

    src/App.vue

    <my-tree :tree-data="treeData" @node-click="nodeClick"></my-tree>
    ...
    nodeClick(val) {
      console.log(val)
    }

    效果如下

    动态展开收起

    这一步的思路是给组件设置一个数组,数组中存放的是当前列表中需要展开的节点的id,当点击节点的时候添加或删除节点id,然后判断每个节点的id在不在这个数组,在则显示子节点,不在则隐藏子节点。

    src/components/myTree.vue

    <div class="item-title" @click="nodeClick(item)">
      <span>{{ item.name }}</span>
      <span v-if="item.children && item.children.length">
        [{{ isOpen(item.id) ? '-' : '+' }}]
      </span>
    </div>
    <div
      v-if="item.children && item.children.length"
      v-show="isOpen(item.id)"
      class="item-childen"
    >
      <my-tree
        :treeData="item.children"
        @node-click="$emit('node-click', $event)"
      ></my-tree>
    </div>
    ...
    data() {
      return {
        expandedKeys: [] // 当前列表需要展开的节点id组成的数组
      }
    },
    methods: {
      nodeClick(item) {
        this.$emit('node-click', item)
        if (item.children && item.children.length) {
          let index = this.expandedKeys.indexOf(item.id)
          if (index > -1) {
            // 如果当前节点id存在数组中,则删除
            this.expandedKeys.splice(index, 1)
          } else {
            // 如果当前节点id不存在数组中,则添加
            this.expandedKeys.push(item.id)
          }
        }
      },
      isOpen(id) {
        // 判断节点id在不在数组中,在则显示,不在则隐藏
        return this.expandedKeys.includes(id)
      }
    }

    效果如下

    最后我们再添加一些样式,就大功告成啦!

    完整代码

    src/components/myTree.vue

    <template>
      <div class="tree-item">
        <div v-for="item in treeData" :key="item.id">
          <div class="item-title" @click="nodeClick(item)">
            <span>{{ item.name }}</span>
            <span v-if="item.children && item.children.length">
              [{{ isOpen(item.id) ? '-' : '+' }}]
            </span>
          </div>
          <div
            v-if="item.children && item.children.length"
            v-show="isOpen(item.id)"
            class="item-childen"
          >
            <my-tree
              :treeData="item.children"
              @node-click="$emit('node-click', $event)"
            ></my-tree>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'myTree',
      props: {
        treeData: {
          type: Array,
          default: () => []
        }
      },
      data() {
        return {
          expandedKeys: [] // 当前展开的节点id组成的数组
        }
      },
      methods: {
        nodeClick(item) {
          this.$emit('node-click', item)
          if (item.children && item.children.length) {
            let index = this.expandedKeys.indexOf(item.id)
            if (index > -1) {
              // 如果当前节点id存在数组中,则删除
              this.expandedKeys.splice(index, 1)
            } else {
              // 如果当前节点id不存在数组中,则添加
              this.expandedKeys.push(item.id)
            }
          }
        },
        isOpen(id) {
          // 判断节点id在不在数组中,在则显示,不在则隐藏
          return this.expandedKeys.includes(id)
        }
      }
    }
    </script>
    
    <style lang="scss" scoped>
    .tree-item {
      cursor: pointer;
      .item-title {
        padding: 4px 8px;
        &:hover {
          background: #eee;
        }
      }
      .item-childen {
        padding-left: 20px;
      }
    }
    </style>

    src/App.vue

    <template>
      <my-tree :tree-data="treeData" @node-click="nodeClick"></my-tree>
    </template>
    
    <script>
    const treeData = [
      { id: 1, name: '一级1' },
      {
        id: 2,
        name: '一级2',
        children: [
          { id: 3, name: '二级2-1' },
          { id: 4, name: '二级2-2' }
        ]
      },
      {
        id: 5,
        name: '一级3',
        children: [
          {
            id: 6,
            name: '二级3-1',
            children: [
              { id: 7, name: '三级3-1-1' },
              { id: 8, name: '三级3-1-2' }
            ]
          },
          { id: 9, name: '二级3-2' },
          { id: 10, name: '二级3-3' }
        ]
      }
    ]
    import myTree from '@/components/myTree.vue'
    export default {
      components: {
        myTree
      },
      data() {
        return {
          treeData: treeData
        }
      },
      methods: {
        nodeClick(val) {
          console.log(val)
        }
      }
    }
    </script>

    效果如下

    以上就是今天的分享!有兴趣的小伙伴可以动手试一哈,把组件进一步封装,或修改成自己想要的样式。 Vue官方的树形视图:cn.vuejs.org/v2/examples…

    总结

    在线客服
    服务热线

    服务热线

    4008888355

    微信咨询
    二维码
    返回顶部
    ×二维码

    截屏,微信识别二维码

    打开微信

    微信号已复制,请打开微信添加咨询详情!