前言
我们开发组件时,一般会用 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_CHILDREN 、 ShapeFlags.ARRAY_CHILDREN 和 ShapeFlags.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 插槽里的元素的方法。如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!
如果有不对、可以优化的地方欢迎在评论区指出,谢谢。