一文带你掌握 VUE3 中 Slots 内容的获取技巧

1,321 阅读3分钟

前言

我们开发组件时,一般会用 slots 插槽来给别人个性化组件内部某块内容。某些场景下,我们可能会需要拿到插槽里的内容来做一些处理。在 jQuery 时代,我们可以很方便的通过一些 DOM 操作的方法来实现。但是在 VUE 中,已经没有这些 dom 操作的方法了,我们该怎么办?

分析

在 VUE 中提供了 $slots 对象来给我们获取组件的插槽,它的定义如下:

interface ComponentPublicInstance {
    $slots: { [name: string]: Slot }
}
type Slot = (...args: any[]) => VNode[]

从上面定义我们可以知道 this.$slots.default() 出来的是一个 VNode 数组,它就是插槽内容的最顶层的 vnode 数组(插槽内容未限制只能单个根元素,所以是一个数组)。

拿到最顶层的 vnode 数组后,我们需要继续往里递归遍历来拿到所有的子 vnode 对象。但是在遍历时,我们怎么知道这个 vnode 有没有子节点呢?

VUE 的源码中我们可以知道 vnode 有一个 shapeFlag 属性,它是一个用二进制的方式来描述 vnode 的类型的。shapeFlag 的值定义在 @vue/shared 中:

export enum ShapeFlags {
  ELEMENT = 1, // 普通的 html 元素
  FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
  STATEFUL_COMPONENT = 1 << 2, // 有状态组件
  TEXT_CHILDREN = 1 << 3, // 子节点是文本
  ARRAY_CHILDREN = 1 << 4, // 子节点是数组
  SLOTS_CHILDREN = 1 << 5, // 子节点是插槽
  TELEPORT = 1 << 6, // teleport 组件
  SUSPENSE = 1 << 7, // suspense 组件
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 需要被 keep-alive 的有状态组件
  COMPONENT_KEPT_ALIVE = 1 << 9, // 已被 keep-alive 的有状态组件
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT, // 组件。有状态和函数式组件的统称
}

只有类型值是 ShapeFlags.TEXT_CHILDRENShapeFlags.ARRAY_CHILDRENShapeFlags.SLOTS_CHILDREN 这三个时,vnode 就是有子节点需要进一步遍历的。

注意我们要判断 vnode 的 shapFlag 值是否是某个类型时不能直接用 = 来判断。因为一个 vnode 可以是多个不同的类型的,比如可能是一个普通的 ELEMENT 元素并且子节点是一个 ARRAY_CHILDREN 数组:

vnode.shapFlag = ShapFlag.Element | ShapFlag.ARRAY_CHILDREN

我们要判断它子节点是一否是个 ARRAY_CHILDREN 数组,只需要用它的 shapFlag 的值和 ShapFlag.ARRAY_CHILDREN& 操作:

const isArrayChildren = (vn) => {
  return Boolean(vn?.shapeFlag & ShapeFlags.ARRAY_CHILDREN);
};

至此,我们就可以用递归来把 slots 里的所有元素取出来了。

实现

下面贴出来具体的实现代码供大家参考:

const ShapeFlags = {
  ELEMENT: 1,
  FUNCTIONAL_COMPONENT: 1 << 1,
  STATEFUL_COMPONENT: 1 << 2,
  COMPONENT: (1 << 2) | (1 << 1),
  TEXT_CHILDREN: 1 << 3,
  ARRAY_CHILDREN: 1 << 4,
  SLOTS_CHILDREN: 1 << 5,
  TELEPORT: 1 << 6,
  SUSPENSE: 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE: 1 << 8,
  COMPONENT_KEPT_ALIVE: 1 << 9
};


const isElement = (vn) => {
  return Boolean(vn?.shapeFlag & ShapeFlags.ELEMENT);
};

const isComponent = (vn) => {
  return Boolean(vn?.shapeFlag & ShapeFlags.COMPONENT);
};

const isTextChildren = (child) => {
  return Boolean(child && child.shapeFlag & ShapeFlags.TEXT_CHILDREN);
};

const isArrayChildren = (vn) => {
  return Boolean(vn?.shapeFlag & ShapeFlags.ARRAY_CHILDREN);
};

const isSlotsChildren = (vn) => {
  return Boolean(vn?.shapeFlag & ShapeFlags.SLOTS_CHILDREN);
};

/**
 * 获取所有子节点
 * @param {VNode[]} children 顶层 vnode 节点数组
 * @param {Boolean} includeText 是否包含文本节点
 * @returns 所有子节点数组
 */
const getAllElements = (children, includeText = false) => {
  const results = [];
  for (const item of children ?? []) {
    if (
      isElement(item) ||
      isComponent(item) ||
      (includeText && isTextChildren(item))
    ) {
      results.push(item);
    }

    if (isArrayChildren(item)) {
      results.push(...getAllElements(item.children, includeText));
    } else if (isSlotsChildren(item)) {
      results.push(...getAllElements(item.children.default?.(), includeText));
    } else if (isArray(item)) {
      results.push(...getAllElements(item, includeText));
    }
  }
  return results;
};

总结

本文带大家分析了如何实现获取 VUE 插槽里的元素的方法。如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!

如果有不对、可以优化的地方欢迎在评论区指出,谢谢。