React&Vue 系列:vue 我有插槽,react 我也有插槽

背景:作为使用三年 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 指定名字,在对应点进行渲染。

还别说,这里的智能提示还挺友好
注意要点:

  1. v-slot can only be used on components or <template> tags v-slot 只能使用在组件和 template 标签上
  2. v-slot 简写为 #
  3. 无论是定义还是传入时,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;

具名插槽

这里存在两种思路:

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 引擎在解析代码的时候,如果遇到了函数,就会在内存中开辟一个新的空间,并且会在内存中保存两点信息:

  1. 确定父级作用域(parentScope)
  2. 保存函数体的内容

根据作用域链的规则,函数只能访问当前的作用域上层作用域。那么适用在这里:

  • 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():单一 children
  • React.Children.toArray():转为数组

具体使用的话,就自己研究,不要太懒

总结

在这篇,先分析和学习了 vue 插槽六个方面的知识点,然后再使用 React 的代码方式来一一实现对应的知识点。也发现了,每个点都能通过 React 代码实现,只是利用的知识点不同而已。

  • vue 采用内部固定的语法来实现

  • react 采用灵活的思路来实现(上面的案例不是唯一的写法)

    比如: react-router-dom v6 中的 outlet 插槽,是利用 Context 实现的,层层嵌套

其实吧,针对插槽这个知识点,我感觉只需要掌握插槽的基本使用具名插槽即可,使用场景广;其他的知识点理解即可,在一定的场景可能使用。

上面存在错误,请多多指教哟,一起学习。

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-1 小时前
验证码机制
前端·后端
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235243 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240254 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar4 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试