# 递归组件

Vue递归组件通过自身调用自身来实现多层嵌套数据的渲染,递归组件实现的重要条件就是要有name名称,这样组件才能在调用自身的时候进行识别。递归组件思想就是抽取出重复出现的渲染元素,然后把这个元素拿出来封装成组件循环嵌套调用。

以创建一个无限嵌套的文件目录为例,可以把当前文件夹信息和子文件夹作为一个组件进行嵌套循环。可以用两种实现方式,一是以单个元素内容为组件封装起来,循环组件来展示。二是以一个列表做为组件封装,在内部进行循环。

递归组件向外传递事件只能传递到上一层,如需在父组件获取事件,可以使用vuex或$bus进行全局传递。

如何判断组件层级,可以在调用组件时传入一个数据,每次+1就可以判断当前层级了。

# 第一种实现方式

以子元素为循环体,子组件显示当前信息和子文件夹列表,通过children判断是否存在子文件夹。

// 父组件
<template>
  <div class="index">
    // 外面进行循环传入文件夹信息
    <TreeItem :depth="0" :treeData="item" v-for="(item, index) in treeList" :key="index" @GET_DEPTH_EVENT="getDepth"></TreeItem>
  </div>
</template>
<script>
import TreeItem from '@/components/TreeItem'


export default {
  name: 'FolderList',
  components: {
    TreeItem
  },
  data() {
    return {
      treeList: [
        {
          id: 1,
          label: '文件1',
          children: [
            {
              id: 11,
              label: '文件11',
              children: [
                {
                  id: 111,
                  label: '文件111',
                  children: []
                },
                {
                  id: 112,
                  label: '文件112',
                  children: []
                }
              ]
            },
            {
              id: 12,
              label: '文件12',
              children: []
            },
            {
              id: 12,
              label: '文件12',
              children: []
            }
          ]
        },
        {
          id: 2,
          label: '文件2',
          children: []
        }
      ]
    }
  },
  methods: {
    getDepth(depth) {
      console.log(depth)
    }
  }
}
</script>
// 递归组件
<template>
  <div class="tree">
    <div class="tree-detail" :style="{'padding-left': depth * 20 + 'px'}" @click="toggleExpand(depth)">
      <span>{{treeData.label}}</span>
    </div>
    <template v-if="treeData.children && treeData.children.length && isExpand">
      // 递归循环体,自身调用自身,通过name进行识别,不需要引入
      <TreeItem v-for="(item, index) in treeData.children" :depth="depth + 1" :treeData="item" :key="index" @GET_DEPTH_EVENT="getDepth"></TreeItem>
    </template>
  </div>
</template>
<script>
export default {
  name: 'TreeItem',
  props: {
    treeData: {
      type: Object,
      default: () => {}
    },
    depth: {
      type: Number
    }
  },
  data() {
    return {
      isExpand: false
    }
  },
  methods: {
    toggleExpand(depth) {
      this.isExpand = !this.isExpand
      this.$emit('GET_DEPTH_EVENT', depth)
    },
    getDepth(depth) {
      console.log(depth)
    }
  }
}
</script>
<style lang="less" scoped>
.tree {
  width: 100%;
  .tree-detail {
    display: flex;
    align-items: center;
    width: 100%;
    height: 40px;
  }
}
</style>

# 第二种实现方式

以整个列表作为循环体,传入整个数据列表,在内部进行循环,这样就避免在父组件还要循环一遍,直接把列表数据传入就行了。

// 父组件
<template>
  <div class="index">
    <TreeList :treeList="treeList" :depth="0" @GET_DEPTH_EVENT="getDepth"></TreeList>
  </div>
</template>
<script>
import TreeList from '@/components/TreeList'


export default {
  name: 'FolderList',
  components: {
    TreeList
  },
  data() {
    return {
      treeList: [
        {
          id: 1,
          label: '文件1',
          children: [
            {
              id: 11,
              label: '文件11',
              children: [
                {
                  id: 111,
                  label: '文件111',
                  children: []
                },
                {
                  id: 112,
                  label: '文件112',
                  children: []
                }
              ]
            },
            {
              id: 12,
              label: '文件12',
              children: []
            },
            {
              id: 12,
              label: '文件12',
              children: []
            }
          ]
        },
        {
          id: 2,
          label: '文件2',
          children: []
        }
      ]
    }
  },
  methods: {
    getDepth(depth) {
      console.log(depth)
    }
  }
}
</script>
// 递归组件
<template>
  <div class="tree">
    <div class="tree-item" v-for="(item, index) in treeList" :key="index">
      <div class="item-detail" :style="{'padding-left': depth * 20 + 'px'}" @click="toggleExpand(item)">
        <span>{{item.label}}</span>
      </div>
      <TreeList :depth="depth + 1" :treeList="item.children" @GET_DEPTH_EVENT="getDepth"></TreeList>
    </div>
  </div>
</template>
<script>
export default {
  name: 'TreeList',
  props: {
    treeList: {
      type: Array,
      default: () => []
    },
    depth: {
      type: Number
    }
  },
  methods: {
    toggleExpand(depth) {
      this.$emit('GET_DEPTH_EVENT', depth)
    },
    getDepth(depth) {
      console.log(depth)
    }
  }
}
</script>
<style lang="less" scoped>
.tree {
  width: 100%;
  .tree-item {
    width: 100%;
    .item-detail {
      display: flex;
      align-items: center;
      width: 100%;
      height: 40px;
    }
  }
}
</style>

# 递归组件操作思路

首先要排除通过对props传入的直进行操作,虽然能直接改变props传入的值,应用类型也会改变原数据,但这样不好。

所以要在当前触发事件在最外层父组件进行操作原始数据,要么试着通过组件的数据双向绑定在当前组件操作进而修改原数据。

v-bind='$attrs' v-on='$listeners'

$attrs在每一层获取除props最外层传入的绑定属性,不是每一层都传的

$listeners在每一层都能获取到任意一场的提交事件

# 递归组件问题

1、两个组件之间相互嵌套调用,提示循环调用,需要提供name

使用异步组件来引入组件,把第一个组件作为原点,引入的第二个组件使用异步组件引入就可以了。

// 组件A
components: {
  // 解决组件相互调用问题,使用异步组件加载组件B
  AssetRelevanceDialog: () => import('@/components/common/globalAddAssetRelevanceDialog.vue')
},