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 实现的,层层嵌套

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

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

相关推荐
HUMHSX1 小时前
Vue 项目启动全流程解析:从入口文件到全局指令注册与页面渲染
前端·javascript·vue.js
有颜有货1 小时前
PMC生产排产的4种算法,一次讲清
java·服务器·前端
小虎牙0071 小时前
Android kotlin图片库Coil源码详解
android·前端
随风一样自由1 小时前
【前端领域】前端开发核心应用场景与落地实践
前端·前端框架
an317422 小时前
弹窗数据流设计的两种高阶架构实践
前端·vue.js·架构
谢尔登2 小时前
【React】 状态管理方案
前端·react.js·前端框架
用户2136610035722 小时前
Vue商品详情与放大镜组件
前端·javascript
半个落月2 小时前
从Tapas小Demo理清localStorage、事件与this
前端·javascript
李明卫杭州2 小时前
Vue2 中 v-model 处理不同数据结构的技巧
前端·javascript·vue.js
李明卫杭州2 小时前
使用 computed 处理 v-model 复杂数据结构
前端·javascript·vue.js