背景:作为使用三年 react.js 的搬运工,目前正在积极学习 vue.js 语法,而在学习的过程中,总是喜欢和 react.js 进行对比,这里也不是说谁好谁坏,而是怀有他有我也有,他没我还有的思想去学习,去总结。
- React18
- Vue3
插槽 这一概念,我最早接触就是在学 vue2 的时候。怎么理解呢?就是组件定义的时候预留一个入口(槽) ,在使用组件的时候插入内容到入口(槽) 中。
到了 vue3,强化了插槽这一功能,使其更为强大。看一看吧。
这次就不先写 React 了,因为 Vue 中有插槽这一概念,而 React 中只是模拟插槽类似的功能,所以先看看 vue 中的插槽是怎么样的。
react-router-dom V6
中的 outlet 跟插槽用法太相似了。`
Vue: 插槽
先借鉴一张官网上的图片,我感觉理解插槽就非常的形象了。
父组件传递代码片段给子组件,子组件就在 slot 的地方,插入代码片段。
基本使用
ts
// SlotSon.vue
<template>
<div class="son">
<h3>我是子组件</h3>
<!-- 插槽 -->
<slot></slot>
</div>
</template>
ts
// Father.vue
<script setup lang="ts">
import SlotSon from "./SlotSon.vue";
function btn() {
console.log("我是父组件的方法");
}
</script>
<template>
<div class="father">
<h3>我是父组件</h3>
<SlotSon>
<!-- 传递内容 -->
<button @click="btn">我是父组件传递下面的按钮</button>
</SlotSon>
</div>
</template>
为了更好地理解,还可以看成是函数形式。
ts
// SlotSon.ts (传递的参数,解析为 html 模版即可)
function SlotSon(slotContent) {
return `
<div class="son">
<h3>我是子组件</h3>
${slotContent}
</div>
`;
}
// Father.ts (调用 SlotSon 函数)
SlotSon(`<button @click="btn">我是父组件传递下面的按钮</button>`);
插槽默认值
在上面提及到了,插槽可以看成是一个函数参数,参数也就会默认值。那么怎么设置默认值呢? 写在 slot 标签内部的子节点就是默认值
ts
// SlotSon.vue
<template>
<div class="son">
<h3>我是子组件</h3>
<slot>
<button>我是插槽的默认值</button>
</slot>
</div>
</template>
ts
// Father.vue
<script setup lang="ts">
import SlotSon from "./SlotSon.vue";
</script>
<template>
<div class="father">
<h3>我是父组件</h3>
<!-- 这里没有给子组价传递任何内容,就会显示默认值 -->
<SlotSon></SlotSon>
</div>
</template>
具名插槽
所谓的具名插槽,就是给插槽取一个名字。在一个组件中可以存在多个插槽,那么该插入到哪里就是根据名字来决定的。 默认名字为 default
ts
// SlotSon.vue
<template>
<div class="son">
<h3>我是子组件</h3>
<slot name="james">
<span>我是 james 的默认值</span>
</slot>
<slot>
<span>我是 default 的默认值</span>
</slot>
<slot name="curry">
<span>我是 curry 的默认值</span>
</slot>
</div>
</template>
使用了三个插槽,名字分别为:james、default、curry。
ts
// Father.vue
<template>
<div class="father">
<h3>我是父组件</h3>
<SlotSon>
<template #james>
<h5>我是真正的 james</h5>
</template>
<template #curry>
<h5>我是真正的 curry</h5>
</template>
<template>
<h5>我是真正的 default</h5>
</template>
</SlotSon>
</div>
</template>
在传入的时候,使用 v-slot
指定名字,在对应点进行渲染。
还别说,这里的智能提示还挺友好
注意要点:
v-slot can only be used on components or <template> tags
v-slot 只能使用在组件和 template 标签上- v-slot 简写为
#
- 无论是定义还是传入时,default 都是可写可不写的
动态插槽名
在上面定义插槽名的,是一个明确的值,固定的。但是其名字也可以是动态的。
例如:插槽名称是父组件的变量,在使用子组件的时候,通过 props 的形式传递给子组件,然后子组件内部定义插槽名称使用 props 中的变量。
ts
// Father.vue
<script setup lang="ts">
import SlotSon from "./SlotSon.vue";
import { ref } from "vue";
// 插槽名称变量
const slotName = ref("title");
</script>
<template>
<div class="father">
<h3>我是父组件</h3>
<!-- 传递给子组件 -->
<SlotSon :slotName="slotName">
<!-- 动态插槽名 -->
<template #[slotName]>
<p>插入指定的位置</p>
</template>
</SlotSon>
</div>
</template>
ts
// SlotSon.vue
<script setup lang="ts">
const props = defineProps<{ slotName: string }>();
</script>
<template>
<div class="son">
<h3>我是子组件</h3>
<!-- 定义插槽名称使用 props 中的变量 -->
<slot :name="props.slotName"></slot>
</div>
</template>
这样动态的插槽名就实现了,功能也能正常运行。
我是在想象不出来动态插槽名的使用场景。在网上搜了一下:说什么减少 template 模板量,我也实在想象不出来。
这就是没有 vue 实际开发经验的,缺少场景使用,,,有哪位好人心可以说说使用场景呗。
渲染作用域
渲染作用域怎么理解呢?就是 Vue 模板中的表达式 只能访问其定义时所处的作用域 。这和[JavaScript 的词法作用域](#JavaScript 的词法作用域 "#javascript-%E8%AF%8D%E6%B3%95%E4%BD%9C%E7%94%A8%E5%9F%9F")规则是一致的。
简单的来说:父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
ts
// SlotSon.vue
<script setup lang="ts">
import { ref } from "vue";
const message = ref("我是子组件的消息");
</script>
<template>
<div class="son">
<slot></slot>
</div>
</template>
ts
// Father.vue
<script setup lang="ts">
import SlotSon from "./SlotSon.vue";
import { ref } from "vue";
const message = ref("我是父组件的消息");
</script>
<template>
<div class="father">
<h3>我是父组件</h3>
<SlotSon>
<p>{{ message }}</p>
</SlotSon>
</div>
</template>
父子组件中都存在 message 的变量,但是最终的实际效果就是 message 使用的是父组件中的变量。
作用域插槽
在上面的插槽作用域中,组件模板只能访问当前的作用域。但是就是有一种需求,就是父组件的插槽模板就是要访问子组件的变量,那该如何做呢?
vue 内部已经考虑到了这点,利用 slot 标签上的 props 进行传值,父组件通过拿取 v-slot 的值在模板中使用。
ts
// Father.vue
<script setup lang="ts">
import SlotSon from "./SlotSon.vue";
</script>
<template>
<div class="father">
<h3>我是父组件</h3>
<SlotSon>
<!-- slotProps 就是 slot 标签上的 props -->
<template #james="slotProps">
<p>{{ slotProps.message }}</p>
</template>
</SlotSon>
</div>
</template>
注意:slot 上的 name 是一个 Vue 特别保留的 attribute,是不会传递给父组件的。
ts
// SlotSon.vue
<script setup lang="ts">
import { ref } from "vue";
const message = ref("子组件的信息");
</script>
<template>
<div class="son">
<h3>我是子组件</h3>
<!-- 传递 message 给父组件 -->
<slot name="james" :message="message"></slot>
</div>
</template>
这样就实现插槽传值给父组件使用了。
React: 插槽
上面通过六个方面来学习了 Vue 的插槽使用:
- 基本使用
- 插槽默认值
- 具名插槽
- 动态插槽名
- 渲染作用域
- 作用域插槽
那么从 React 视角来实现以上功能,看是否能成功呢?拭目以待。
在开始之前,肯定需要了解 React 的两个知识点,为后面打好基础。
- props 的使用:[React&Vue 系列:props](#React&Vue 系列:props "#https://juejin.cn/post/7259298176524009533")
- children 的理解:[额外知识:React 中的 children 属性](#额外知识:React 中的 children 属性 "#react-%E4%B8%AD%E7%9A%84-children-%E5%B1%9E%E6%80%A7")
理解了上面的两点,正题开始了。
基本使用
利用 children 属性,父组件就可以把代码片段,插入到子组件中。
tsx
// Father.tsx
import type { FC } from "react";
import Son from "./Son";
const Father: FC = () => {
return (
<div>
<h3>我是父组件</h3>
<Son>
<div>我是父组件插入的代码片段</div>
</Son>
</div>
);
};
export default Father;
tsx
// Son.tsx
import type { FC, ReactNode } from "react";
interface Props {
children?: ReactNode | undefined;
}
const Son: FC<Props> = (props) => {
const { children } = props;
return (
<>
<h2>我是子组件</h2>
{children}
</>
);
};
export default Son;
插槽默认值
当父组件没有传递代码片段的时候,子组件的 props 中是没有 children 属性的,判断一波即可。
tsx
// Father.tsx
import type { FC } from "react";
import Son from "./Son";
const Slot: FC = () => {
return (
<div>
<h3>我是父组件</h3>
<Son></Son>
</div>
);
};
export default Slot;
tsx
// Son.tsx
import type { FC, ReactNode } from "react";
interface Props {
children?: ReactNode | undefined;
}
// 子组件的默认代码片段
const DefaultSon = () => <div>子组件的默认值</div>;
const Son: FC<Props> = (props) => {
const { children } = props;
return (
<>
<h2>我是子组件</h2>
{children ?? <DefaultSon />}
</>
);
};
export default Son;
具名插槽
这里存在两种思路:
- 方式一:利用自定义数据属性(data-*) ,子组件对 children 的 props 属性中的值进行判断(不推荐,比较麻烦)
ts
const Father: FC = () => {
return (
<div>
<h3>我是父组件</h3>
<Son>
<div data-name="left">我是父组件插入的代码片段1</div>
<div data-name="right">我是父组件插入的代码片段2</div>
</Son>
</div>
);
};
- 方式二: 直接利用 props 进行传递(推荐)
ts
// Father.tsx
import type { FC } from "react";
import Son from "./Son";
const Slot: FC = () => {
return (
<div>
<h3>我是父组件</h3>
<Son
leftSlot={<div>我是父组件插入的代码片段1</div>}
rightSlot={<div>我是父组件插入的代码片段2</div>}
></Son>
</div>
);
};
export default Slot;
ts
// Son.tsx
import type { FC } from "react";
interface Props {
leftSlot?: JSX.Element;
rightSlot?: JSX.Element;
}
const DefaultSon = () => <div>子组件的默认值</div>;
const Son: FC<Props> = (props) => {
const { leftSlot, rightSlot } = props;
return (
<>
<h2>我是子组件</h2>
<div className="left">{leftSlot ?? <DefaultSon />}</div>
<div className="right">{rightSlot ?? <DefaultSon />}</div>
</>
);
};
export default Son;
这样具名插槽就实现好了。
动态插槽名
插槽名称是父组件的变量,在使用子组件的时候,通过 props 的形式传递给子组件,然后子组件内部定义插槽名称使用 props 中的变量。
ts
// Father.tsx
import type { FC } from "react";
import { useState } from "react";
import Son from "./Son";
const Father: FC = () => {
const [slotName] = useState("leftSlot");
// 利用对象,绑定动态的key值,然后再使用组件时,解构
const sonProps = {
[slotName]: <div>我是父组件插入的代码片段2</div>,
};
return (
<div>
<h3>我是父组件</h3>
<Son {...sonProps} slotName={slotName}></Son>
</div>
);
};
export default Father;
ts
// Son.tsx
import type { FC } from "react";
interface Props {
slotName: string;
[key: string]: any; // ?? 这里的类型写 any,写具体的类型,报错
}
const DefaultSon = () => <div>子组件的默认值</div>;
const Son: FC<Props> = (props) => {
const { slotName } = props;
return (
<>
<h2>我是子组件</h2>
<div>{props[slotName] ?? DefaultSon}</div>
</>
);
};
export default Son;
动态插槽名就这样简单的实现了。
渲染作用域
这个肯定是一样的,因为无论是 React 还是 Vue,最终都是 JS 语法,肯定都遵循 JS 的词法作用域规则。
ts
// Father.tsx
import type { FC } from "react";
import { useState } from "react";
import Son from "./Son";
const Father: FC = () => {
const [message] = useState("父组件的信息");
return (
<div>
<h3>我是父组件</h3>
<Son>
{/* 代码片段使用变量 message */}
<div>{message}</div>
</Son>
</div>
);
};
export default Father;
ts
// Son.tsx
import type { FC, ReactNode } from "react";
import { useState } from "react";
interface Props {
children: ReactNode;
}
const DefaultSon = () => <div>子组件的默认值</div>;
const Son: FC<Props> = (props) => {
const { children } = props;
const [message] = useState("子组件的信息");
return (
<>
<h2>我是子组件</h2>
<div>{children ?? <DefaultSon />}</div>
</>
);
};
export default Son;
会发现,渲染出来,还是使用的父组件的变量。
作用域插槽
在上面的渲染作用域中,父组件渲染父组件中的变量,子组件渲染子组件中的变量。现在特殊,父组件中的代码片段想要渲染子组件中的变量。
以下的代码思路来源于 vue 官网中的 【作用域插槽类比为一个传入子组件的函数】
ts
// Father.tsx
import type { FC } from "react";
import Son from "./Son";
const Father: FC = () => {
// 传递插槽函数
const slot = {
leftSlot: (slotProps: { message: string }) => {
return <div>子组件的变量{slotProps.message}</div>;
},
rightSlot: (slotProps: { name: string }) => {
return <div>子组件的变量{slotProps.name}</div>;
},
};
return (
<div>
<h3>我是父组件</h3>
<Son {...slot}></Son>
</div>
);
};
export default Father;
ts
// Son.tsx
import type { FC } from "react";
import { useState } from "react";
interface Props {
leftSlot: (slotProps: { message: string }) => JSX.Element;
rightSlot: (slotProps: { name: string }) => JSX.Element;
}
const DefaultSon = () => <div>子组件的默认值</div>;
const Son: FC<Props> = (props) => {
const { leftSlot, rightSlot } = props;
const [message] = useState("子组件的message");
const [name] = useState("子组件的name");
return (
<>
<h2>我是子组件</h2>
<div>{leftSlot({ message }) ?? <DefaultSon />}</div>
<div>{rightSlot({ name }) ?? <DefaultSon />}</div>
</>
);
};
export default Son;
这样就实现了 react 版本的作用域插槽。
额外知识
JavaScript 词法作用域
JavaScript 的词法作用域,又称为 静态作用域。
静态作用域:函数的作用域在函数定义的时候就决定了。看下面的示例:
js
let message = "james";
function foo() {
let message = "copyer";
bar();
}
function bar() {
console.log(message); // 打印 ?
}
foo();
代码简单吧,就只定义了一个变量,两个函数,然后执行函数。那么 bar()
打印什么呢?
JS 引擎在解析代码的时候,如果遇到了函数,就会在内存中开辟一个新的空间,并且会在内存中保存两点信息:
- 确定父级作用域(parentScope)
- 保存函数体的内容
根据作用域链的规则,函数只能访问当前的作用域 及上层作用域。那么适用在这里:
- foo 函数定义,就确定自身的作用域和上层作用域(全局作用域)
- bar 函数定义,就确定自身的作用域和上层作用域(全局作用域)
那么 bar()
执行,发现要 message 变量,找自身作用域没有,就上层作用域(全局作用域)去找,找到 message,那么就使用打印出来,所以这里的结果是 james
。
所以,针对 JavaScript 词法作用域只需要注意一个点,函数在定义的时候就已经确定了作用域。
React 中的 children 属性
在 React 的 props 中有个特殊的属性:children。
什么时候会存在呢?
ts
const Father = () => {
return <Son>{/* 没有插入内容 */}</Son>;
};
当使用子组件的时候, 【没有插入内容】 ,那么 Son 组件中的 props 就没有 children 属性。
ts
const Father = () => {
return <Son>{/* 存在插入内容 */}</Son>;
};
当使用子组件的时候, 【存在插入内容】 ,那么 Son 组件中的 props 就会存在一个 children 属性。
- 当插入的内容为一个节点的时候,就是一个对象。
- 当插入的内容为多个节点的时候,就是一个数组。
也可以简单看一下:
ts
type PropsWithChildren<P = unknown> = P & { children?: ReactNode | undefined };
type ReactNode =
| ReactElement
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined
| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES[keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES];
children 的类型为 ReactNode, 而 ReactNode 的类型基本上指的任意类型,也就是说 children 的类型是多样的。所以,React 提供了一系列的函数助手来使得操作 children 更加方便,为了防止直接操作 children 报错的风险。
React.Children.map()
:循环React.Children.forEach()
:循环React.Children.count()
:children 的个数React.Children.only()
:单一 childrenReact.Children.toArray()
:转为数组
具体使用的话,就自己研究,不要太懒
总结
在这篇,先分析和学习了 vue 插槽六个方面的知识点,然后再使用 React 的代码方式来一一实现对应的知识点。也发现了,每个点都能通过 React 代码实现,只是利用的知识点不同而已。
-
vue 采用内部固定的语法来实现
-
react 采用灵活的思路来实现(上面的案例不是唯一的写法)
比如: react-router-dom v6 中的 outlet 插槽,是利用 Context 实现的,层层嵌套
其实吧,针对插槽这个知识点,我感觉只需要掌握插槽的基本使用 和具名插槽即可,使用场景广;其他的知识点理解即可,在一定的场景可能使用。
上面存在错误,请多多指教哟,一起学习。