在 Vue3 中,组件可以通过递归的方式 自身渲染自身

这种模式通常用于处理树形结构数据(如菜单、评论、文件目录等)。Vue3 支持组件在其模板中直接引用自身,但需要注意递归的终止条件,以避免无限递归。


一、组件自身渲染自身的实现

1. 基本实现
  • 在组件模板中,直接使用组件自身的标签名。

  • 通过 name 属性定义组件名称,以便在模板中引用。

vue

复制

<template>
  <div>
    <p>{{ data.name }}</p>
    <ul v-if="data.children">
      <li v-for="child in data.children" :key="child.id">
        <!-- 递归渲染自身 -->
        <RecursiveComponent :data="child" />
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'RecursiveComponent', // 定义组件名称
  props: {
    data: {
      type: Object,
      required: true,
    },
  },
};
</script>
2. 使用场景
  • 树形菜单:渲染嵌套的菜单项。

  • 评论系统:渲染嵌套的评论和回复。

  • 文件目录:渲染嵌套的文件夹和文件。


二、递归组件的关键点

1. 终止条件
  • 必须确保递归有终止条件,否则会导致无限递归和栈溢出。

  • 在上面的例子中,v-if="data.children" 是一个终止条件,当 data.children 为空时,递归停止。

2. 组件名称
  • 组件必须通过 name 属性定义名称,才能在模板中引用自身。

  • 如果使用 <script setup> 语法,可以通过文件名隐式定义组件名称。

3. 数据格式
  • 递归组件通常需要处理嵌套结构的数据,例如:

    json

    复制

    {
      "id": 1,
      "name": "Root",
      "children": [
        {
          "id": 2,
          "name": "Child 1",
          "children": []
        },
        {
          "id": 3,
          "name": "Child 2",
          "children": [
            {
              "id": 4,
              "name": "Grandchild 1",
              "children": []
            }
          ]
        }
      ]
    }

三、完整示例

1. 递归组件

vue

复制

<template>
  <div>
    <p>{{ data.name }}</p>
    <ul v-if="data.children && data.children.length">
      <li v-for="child in data.children" :key="child.id">
        <RecursiveComponent :data="child" />
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'RecursiveComponent', // 定义组件名称
  props: {
    data: {
      type: Object,
      required: true,
    },
  },
};
</script>
2. 使用递归组件

vue

复制

<template>
  <div>
    <h1>Tree Structure</h1>
    <RecursiveComponent :data="treeData" />
  </div>
</template>

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

export default {
  components: {
    RecursiveComponent,
  },
  data() {
    return {
      treeData: {
        id: 1,
        name: 'Root',
        children: [
          {
            id: 2,
            name: 'Child 1',
            children: [],
          },
          {
            id: 3,
            name: 'Child 2',
            children: [
              {
                id: 4,
                name: 'Grandchild 1',
                children: [],
              },
            ],
          },
        ],
      },
    };
  },
};
</script>

四、使用 <script setup> 语法

如果你使用 Vue3 的 <script setup> 语法,可以更简洁地实现递归组件:

1. 递归组件

vue

复制

<template>
  <div>
    <p>{{ data.name }}</p>
    <ul v-if="data.children && data.children.length">
      <li v-for="child in data.children" :key="child.id">
        <RecursiveComponent :data="child" />
      </li>
    </ul>
  </div>
</template>

<script setup>
defineProps({
  data: {
    type: Object,
    required: true,
  },
});
</script>
2. 使用递归组件

vue

复制

<template>
  <div>
    <h1>Tree Structure</h1>
    <RecursiveComponent :data="treeData" />
  </div>
</template>

<script setup>
import RecursiveComponent from './RecursiveComponent.vue';

const treeData = {
  id: 1,
  name: 'Root',
  children: [
    {
      id: 2,
      name: 'Child 1',
      children: [],
    },
    {
      id: 3,
      name: 'Child 2',
      children: [
        {
          id: 4,
          name: 'Grandchild 1',
          children: [],
        },
      ],
    },
  ],
};
</script>

五、注意事项

  1. 性能问题

    • 递归组件可能会导致性能问题,尤其是在数据嵌套层级很深时。

    • 可以通过虚拟滚动或懒加载优化性能。

  2. 终止条件

    • 必须确保递归有终止条件,否则会导致无限递归。

  3. 组件名称

    • 在模板中引用自身时,组件必须定义 name 属性。

  4. 数据格式

    • 确保数据结构正确,避免因数据问题导致渲染错误。


六、总结

  • Vue3 支持组件通过递归方式渲染自身。

  • 递归组件适用于树形结构数据的渲染。

  • 需要定义终止条件,避免无限递归。

  • 可以使用 <script setup> 语法简化代码。

通过递归组件,可以轻松实现复杂的嵌套结构渲染!

如下是容器和折叠面板,拖拽组件,自动拖拽布局的代码示例:

<template>

  <Col v-bind="colPropsComputed" v-if="['grid', 'collapse'].includes(item.fieldTypeInnerKey)">

    <div

      :class="['box', activeId === (item.fieldId || item.id) ? 'active-form-item' : '']"

      :style="{ width: item.fieldWidth ?? '50%' }"

      @click.stop="emits('activeItem', item)"

    >

      <Row class="grid-row" v-bind="item.componentProps" v-if="'grid' === item.fieldTypeInnerKey">

        <Col

          class="grid-col"

          v-for="(colItem, index) in item.children"

          :key="index"

          :span="Number(colItem.fieldWidth)"

        >

          <draggable

            class="drawing-board draggable-box"

            :component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"

            v-bind="{

              group: 'componentsGroup',

              animation: 340,

            }"

            item-key="fieldName"

            v-model="colItem.children"

          >

            <template #item="{ element }">

              <LayoutItem

                :drawing-list="drawingList"

                :item="element"

                :active-id="activeId"

                :form-rule="formRule"

                :inner-key="innerKey"

                :isDisabled="isDisabled"

                v-bind="$attrs"

                @delete-item="emits('deleteItem', $event)"

                @delete-grid="emits('deleteGrid', $event)"

                @active-item="emits('activeItem', $event)"

                @change-item="(key, val) => emits('changeItem', key, val)"

                @is-required="(id, k) => emits('isRequired', id, k)"

                @is-read="(id, k) => emits('isRead', id, k)"

              />

            </template>

          </draggable>

        </Col>

      </Row>

      <Collapse default-active-key="1" v-if="'collapse' === item.fieldTypeInnerKey">

        <CollapsePanel key="1" :header="item.displayName">

          <div :class="['collapse-box']" :style="{ width: item.fieldWidth ?? '50%' }">

            <draggable

              class="drawing-board draggable-box"

              :component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"

              v-bind="{

                group: 'componentsGroup',

                animation: 340,

              }"

              item-key="fieldName"

              v-model="item.children"

            >

              <template #item="{ element }">

                <LayoutItem

                  :drawing-list="drawingList"

                  :item="element"

                  :active-id="activeId"

                  :form-rule="formRule"

                  :inner-key="innerKey"

                  :isDisabled="isDisabled"

                  v-bind="$attrs"

                  @delete-item="emits('deleteItem', $event)"

                  @delete-grid="emits('deleteGrid', $event)"

                  @active-item="emits('activeItem', $event)"

                  @change-item="(key, val) => emits('changeItem', key, val)"

                  @is-required="(id, k) => emits('isRequired', id, k)"

                  @is-read="(id, k) => emits('isRead', id, k)"

                />

              </template>

            </draggable>

          </div>

        </CollapsePanel>

      </Collapse>

      <Operate :isDisabled="isDisabled" :item="item" @delete-item="emits('deleteGrid', item)" />

    </div>

  </Col>

  <draggable-item

    v-else

    v-bind="$attrs"

    :drawing-list="drawingList"

    :item="item"

    :active-id="activeId"

    :form-rule="formRule"

    :inner-key="innerKey"

    :isDisabled="isDisabled"

    @delete-item="emits('deleteItem', $event)"

    @active-item="emits('activeItem', $event)"

    @change-item="(key, val) => emits('changeItem', key, val)"

    @is-required="(id, k) => emits('isRequired', id, k)"

    @is-read="(id, k) => emits('isRead', id, k)"

  />

</template>

<script lang="ts" setup name="LayoutItem">

  import { computed, defineComponent, PropType, reactive, toRefs } from 'vue';

  import draggable from 'vuedraggable';

  import DraggableItem from './DraggableItem.vue';

  import Operate from './Operate.vue';

  import { Row, Col, Collapse, CollapsePanel } from 'ant-design-vue';

  const props = defineProps({

    item: {

      type: Object as PropType<any>,

      default: () => ({}),

    },

    formRule: {

      type: Object as PropType<any>,

      default: () => ({}),

    },

    drawingList: {

      type: Object as PropType<any>,

      default: () => ({}),

    },

    activeId: {

      type: String as PropType<string>,

      default: '',

    },

    innerKey: {

      type: String as PropType<string>,

      default: '',

    },

    isDisabled: Boolean,

  });

  const emits = defineEmits([

    'deleteGrid',

    'dragStart',

    'handleColAdd',

    'handle-copy',

    'handle-delete',

    'deleteItem',

    'activeItem',

    'changeItem',

    'isRequired',

    'isRead',

  ]);

  const colPropsComputed = computed(() => {

    const { colProps = { span: 24 } } = props.item;

    return colProps;

  });

</script>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值