第3章 3.1 组件插槽 slot

前置章节回顾

上一章我们用 Composition API 完整地手撕了一个 Todo List,把「数据驱动视图」这个核心思想掰碎了讲。你现在应该已经能熟练运用 refreactive,以及 watch/computed 这些组合拳了。

但是,写到这里有个问题冒出来了:我们写的组件都是「铁板一块」,子组件内部的 DOM 结构完全由自己决定,父组件想自定义一点点内部结构都做不到。这就好比你买了一个快递,快递盒的外观、大小、甚至里面的泡沫填充物都是出厂定死的,你只能被动接受——想要换个包装?门都没有。

本章目标

学完这一章,你就能打破这个「铁板组件」的魔咒——用 slot(插槽) 这个神器,让父组件可以往子组件里「塞」任何自己想放的内容。


1. 🎯 开场 3 分钟:为什么要学这个?

场景还原:你的第一个痛点

假设你写了一个「卡片组件」UserCard

# 模拟 Vue 的组件概念
class UserCard\n\n![Simple tech illustration expla](https://blog.xxyye.com/wp-content/uploads/2026/06/17f926aee932575.png)\n\n![AI comic creation scene, creat](https://blog.xxyye.com/wp-content/uploads/2026/06/82402bf6012379c.png)\n\n:
def render(self):
    return f"""
    <div class="card">
        <h3>{self.username}</h3>
        <p>用户等级:{self.level}</p>
    </div>
    """

产品经理突然说:「有些卡片需要加头像,有些需要加关注按钮,有些啥都不加。」

你的第一反应是什么?

  • 写多个版本的 UserCardV1UserCardV2UserCardV3?→ 代码爆炸
  • UserCard 里加一堆 if 判断?→ 组件越来越臃肿
  • 用 slot:让父组件决定卡片的「坑」里放什么 → 优雅!

第二个痛点:数据从哪来?

有时候子组件有一堆内部数据,但父组件需要根据这些数据决定怎么渲染

比如一个下拉列表组件,子组件知道「所有选项列表」,但具体某个选项显示成什么样,父组件想自己控制。

这在 Vue 里叫作用域插槽——字面意思就是「插槽有作用域,子组件的数据能传给父组件用」。

学完本文你能解决

  1. 让任何组件变得「可定制」——父组件想塞什么就塞什么
  2. 实现默认内容——如果父组件没传,就显示默认值
  3. 让子组件的数据流向父组件——父组件根据数据渲染不同内容

2. 🧱 基础 25 分钟:核心概念

2.1 生活类比:插槽就像「留白」

想象你买了一套精装书,书壳上有个固定的「书签孔」,但你塞进去的书签可以是:
- 纸质的
- 金属的
- 卡通的
- 甚至是一张银行卡

插槽就是那个「孔」,它规定了位置和大小,但里面具体放什么,由你决定。

2.2 默认插槽:最简单的「留白」

是什么:子组件挖一个坑,父组件往里填内容。如果父组件没填,就显示子组件自己准备的默认内容。

为什么要用:组件大部分地方固定,只有少部分需要「可定制」。

怎么用(用 Python 代码模拟 Vue 的 slot 机制):

# 子组件:定义了一个默认插槽
class BaseCard:
def __init__(self):
    self.slot_content = None  # 插槽内容,父组件传入

def set_slot(self, content):
    """父组件调用这个方法来设置插槽内容"""
    self.slot_content = content

def render(self):
    # 如果父组件传了内容,就用父组件的;否则用默认内容
    if self.slot_content:
        inner = self.slot_content
    else:
        inner = "<p>这是默认内容,没人来填充我 😢</p>"

    return f"""
    <div class="card">
        <div class="card-header">我是卡片头部</div>
        <div class="card-body">{inner}</div>
    </div>
    """

# 父组件:使用默认插槽
card = BaseCard()
card.set_slot("<p>我自己塞进来的内容 ✨</p>")
print(card.render())

输出

<div class="card">
<div class="card-header">我是卡片头部</div>
<div class="card-body"><p>我自己塞进来的内容 ✨</p></div>
</div>

这行代码在干嘛:子组件先检查有没有人往插槽里塞内容,有就用父组件的,没有就用默认的。

2.3 具名插槽:不同位置,不同内容

是什么:子组件挖多个「命名」的坑,父组件可以往指定名字的坑里填不同内容。

为什么要用:一个组件有多个可定制区域,每个区域要放不同的东西。

怎么用

from collections import defaultdict

# 具名插槽容器(模拟 Vue 的 <slot name="xxx"> 机制)
class NamedSlots:
def __init__(self):
    self.slots = defaultdict(lambda: None)


def set_slot(self, name, content):
    self.slots[name] = content

def get_slot(self, name, default=""):
    return self.slots[name] if self.slots[name] else default

# 子组件:定义多个具名插槽
class ArticleCard:
def __init__(self):
    self.slots = NamedSlots()
    self.title = "默认标题"
    self.content = "默认内容"

def set_slot(self, name, content):
    self.slots.set_slot(name, content)

def render(self):
    header = self.slots.get_slot("header", "<h3>默认标题</h3>")
    footer = self.slots.get_slot("footer", "<small>没有更多信息</small>")

    return f"""
    <article>
        <header>{header}</header>
        <div class="content">
            <h1>{self.title}</h1>
            <p>{self.content}</p>
        </div>
        <footer>{footer}</footer>
    </article>
    """

# 父组件:向具名插槽填充内容
article = ArticleCard()
article.set_slot("header", "<div class='breadcrumb'>首页 > 技术文章</div>")
article.set_slot("footer", """
<div class="actions">
    <button>点赞 👍</button>
    <button>收藏 ⭐</button>
    <button>分享 📤</button>
</div>
""")
print(article.render())

输出

<article>
<header><div class='breadcrumb'>首页 > 技术文章</div></header>
<div class="content">
    <h1>默认标题</h1>
    <p>默认内容</p>
</div>
<footer>
<div class="actions">
    <button>点赞 👍</button>
    <button>收藏 ⭐</button>
    <button>分享 📤</button>
</div>
</footer>
</article>

这行代码在干嘛:子组件为 headerfooter 两个区域预留了坑位,父组件可以精准地往指定坑位填内容。

2.4 作用域插槽:子组件的数据,父组件来渲染

是什么:子组件把自己内部的数据「暴露」给插槽,让父组件能根据这些数据决定怎么渲染。

为什么要用:子组件有数据,但具体显示成什么样子想交给父组件决定。比如一个列表组件知道「所有数据」,但每条数据长什么样,父组件说了算。

怎么用

# 子组件:暴露数据给父组件
class DataList:
def __init__(self, items):
    self.items = items  # 子组件有自己的数据

def render_with_slot(self, slot_fn):
    """
    slot_fn 是一个函数,接收子组件的数据,返回父组件定义的渲染结果
    这就是「作用域插槽」的核心机制
    """
    results = []
    for item in self.items:
        # 子组件把数据传给父组件,父组件决定怎么渲染
        results.append(slot_fn(item))
    return "\n".join(results)

# 父组件:根据子组件的数据自定义渲染方式
data_list = DataList([
{"name": "小明", "score": 85},
{"name": "小红", "score": 92},
{"name": "小刚", "score": 78}
])

# 父组件定义的「插槽模板函数」:接收一个 item,返回渲染字符串
def my_slot_template(item):
is_pass = item["score"] >= 60
status = "✅ 及格" if is_pass else "❌ 不及格"
return f"<li>{item['name']}:{item['score']}分 {status}</li>"

result = data_list.render_with_slot(my_slot_template)
print(f"<ul>\n{result}\n</ul>")

输出

<ul>
<li>小明:85分 ✅ 及格</li>
<li>小红:92分 ✅ 及格</li>
<li>小刚:78分 ✅ 及格</li>
</ul>

这行代码在干嘛:子组件把自己的 items 数据逐条传给父组件的 slot_fn,父组件每收到一条数据就返回一个渲染结果,最终拼成完整的 HTML。

2.5 动态插槽名:插槽名也能是变量

是什么:插槽的名字不是写死的,而是通过变量动态决定。

怎么用

# 模拟动态插槽名
class DynamicSlotDemo:
def __init__(self):
    self.slots = {}
    self.active_tab = "tab1"

def set_slot(self, name, content):
    self.slots[name] = content

def render(self):
    # 根据当前激活的 tab 决定显示哪个插槽的内容
    current_content = self.slots.get(self.active_tab, "<p>空</p>")
    return f"""
    <div class="tabs">
        <div class="tab-header">
            <button class="{'active' if self.active_tab=='tab1' else ''}">标签1</button>
            <button class="{'active' if self.active_tab=='tab2' else ''}">标签2</button>
        </div>
        <div class="tab-content">{current_content}</div>
    </div>
    """

demo = DynamicSlotDemo()
demo.set_slot("tab1", "<p>这是标签1的内容</p>")
demo.set_slot("tab2", "<p>这是标签2的内容</p>")
demo.active_tab = "tab2"  # 动态切换
print(demo.render())

输出

<div class="tabs">
<div class="tab-header">
    <button class="">标签1</button>
    <button class="active">标签2</button>
</div>
<div class="tab-content"><p>这是标签2的内容</p></div>
</div>

3. 🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):可复用的消息提示框

目标:写一个 Toast 组件,支持传入自定义内容。

"""项目1:可复用的消息提示框"""
class Toast:
def __init__(self, message="默认消息", toast_type="info"):
    self.message = message
    self.toast_type = toast_type
    self.slot_content = None

def set_slot(self, content):
    self.slot_content = content

def render(self):
    # 使用插槽内容或默认消息
    display_message = self.slot_content if self.slot_content else self.message

    icons = {
        "success": "✅",
        "error": "❌",
        "warning": "⚠️",
        "info": "ℹ️"
    }
    icon = icons.get(self.toast_type, "ℹ️")

    return f"""
    <div class="toast toast-{self.toast_type}">
        <span class="icon">{icon}</span>
        <span class="message">{display_message}</span>
    </div>
    """

# 使用
toast1 = Toast("操作成功!", "success")
print(toast1.render())

toast2 = Toast()
toast2.set_slot("<strong>自定义内容</strong>:带有 <em>HTML 样式</em> 的消息")
print(toast2.render())

预期输出

    <div class="toast toast-success">
        <span class="icon">✅</span>
        <span class="message">操作成功!</span>
    </div>

    <div class="toast toast-info">
        <span class="icon">ℹ️</span>
        <span class="message"><strong>自定义内容</strong>:带有 <em>HTML 样式</em> 的消息</span>
    </div>

一句话解释:Toast 组件挖了一个「内容坑」,父组件可以传自定义内容进来,也可以直接传默认消息。


项目 2(15 分钟):博客文章列表(从 JSON 读取数据)

目标:写一个 PostList 组件,从 JSON 数据中读取文章列表,用作用域插槽让父组件自定义每篇文章的渲染方式。

"""项目2:博客文章列表"""
import json

# 模拟从 API/文件获取的博客数据
POSTS_DATA = json.dumps([
{
    "id": 1,
    "title": "Python 入门的 5 个技巧",
    "author": "小明",
    "views": 1520,
    "tags": ["Python", "入门"]
},
{
    "id": 2,
    "title": "深入理解 Vue3 响应式原理",
    "author": "小红",
    "views": 3200,
    "tags": ["Vue3", "进阶"]
},
{
    "id": 3,
    "title": "前端面试必备的 10 个手写题",
    "author": "小刚",
    "views": 8900,
    "tags": ["面试", "JavaScript"]
}
])

class PostList:
def __init__(self, json_data):
    self.posts = json.loads(json_data)

def render_with_slot(self, slot_fn):
    """作用域插槽:把每篇文章的数据传给父组件"""
    results = []
    for post in self.posts:
        results.append(slot_fn(post))
    return "\n".join(results)

# 父组件:定义如何渲染每一篇文章
def post_card_template(post):
# 根据浏览量决定热度标签
if post["views"] > 5000:
    hot_tag = "<span class='hot'>🔥 热门</span>"
elif post["views"] > 1000:
    hot_tag = "<span class='trending'>📈 上升中</span>"
else:
    hot_tag = ""

tags_html = " ".join([f"<span class='tag'>{tag}</span>" for tag in post["tags"]])

return f"""
<div class="post-card">
    <h3>{post['title']} {hot_tag}</h3>
    <div class="meta">
        <span>👤 {post['author']}</span>
        <span>👁️ {post['views']} 次阅读</span>
    </div>
    <div class="tags">{tags_html}</div>
</div>
"""

# 渲染
posts = PostList(POSTS_DATA)
print("<div class='post-list'>")
print(posts.render_with_slot(post_card_template))
print("</div>")

预期输出

<div class='post-list'>
<div class="post-card">
    <h3>Python 入门的 5 个技巧 </h3>
    <div class="meta">
        <span>👤 小明</span>
        <span>👁️ 1520 次阅读</span>
    </div>
    <div class="tags"><span class='tag'>Python</span><span class='tag'>入门</span></div>
</div>
<div class="post-card">
    <h3>深入理解 Vue3 响应式原理 <span class='trending'>📈 上升中</span></h3>
    <div class="meta">
        <span>👤 小红</span>
        <span>👁️ 3200 次阅读</span>
    </div>
    <div class="tags"><span class='tag'>Vue3</span><span class='tag'>进阶</span></div>
</div>
<div class="post-card">
    <h3>前端面试必备的 10 个手写题 <span class='hot'>🔥 热门</span></h3>
    <div class="meta">
        <span>👤 小刚</span>
        <span>👁️ 8900 次阅读</span>
    </div>
    <div class="tags"><span class='tag'>面试</span><span class='tag'>JavaScript</span></div>
</div>
</div>

一句话解释PostList 只管「数据」,具体长什么样由父组件的 post_card_template 决定——这就是作用域插槽的威力。


项目 3(15 分钟):可配置的数据表格

目标:把项目 2 组合起来,做一个「可配置的表格组件」,支持:
1. 自定义表头(具名插槽)
2. 自定义每行数据(作用域插槽)
3. 支持空数据时的默认提示

"""项目3:可配置的数据表格"""
import json

class DataTable:
def __init__(self, columns, data):
    """
    columns: 列配置,如 [{"key": "name", "label": "姓名"}, ...]
    data: 数据列表
    """
    self.columns = columns
    self.data = data
    self.header_slot = None
    self.row_slot_fn = None
    self.empty_slot = None

def set_header_slot(self, content):
    self.header_slot = content

def set_row_slot(self, slot_fn):
    self.row_slot_fn = slot_fn

def set_empty_slot(self, content):
    self.empty_slot = content

def render(self):
    # 渲染表头
    if self.header_slot:
        header_html = self.header_slot(self.columns)
    else:
        header_html = "".join([f"<th>{col['label']}</th>" for col in self.columns])

    # 渲染数据行
    if not self.data:
        empty_content = self.empty_slot if self.empty_slot else "<tr><td colspan='100%'>暂无数据</td></tr>"
        body_html = empty_content
    else:
        rows = []
        for row in self.data:
            if self.row_slot_fn:
                rows.append(self.row_slot_fn(row, self.columns))
            else:
                # 默认渲染:简单拼接所有列的值
                cells = [f"<td>{row.get(col['key'], '')}</td>" for col in self.columns]
                rows.append(f"<tr>{''.join(cells)}</tr>")
        body_html = "\n".join(rows)

    return f"""
<table class="data-table">
<thead>
    <tr>{header_html}</tr>
</thead>
<tbody>
    {body_html}
</tbody>
</table>"""

# ============ 使用示例 ============

# 模拟用户数据
users_data = [
{"id": 1, "name": "小明", "role": "管理员", "status": "active"},
{"id": 2, "name": "小红", "role": "编辑", "status": "active"},
{"id": 3, "name": "小刚", "role": "访客", "status": "inactive"}
]

columns = [
{"key": "id", "label": "ID"},
{"key": "name", "label": "用户名"},
{"key": "role", "label": "角色"},
{"key": "status", "label": "状态"}
]

# 自定义表头的渲染方式
def custom_header(columns):
return "".join([
    f"<th class='col-{col['key']}'>{col['label']} 📊</th>" 
    for col in columns
])

# 自定义每一行的渲染方式
def custom_row(row, columns):
status_display = {
    "active": "🟢 活跃",
    "inactive": "⚫ 停用"
}
cells = []
for col in columns:
    value = row.get(col['key'], '')
    if col['key'] == 'status':
        value = status_display.get(value, value)
    cells.append(f"<td>{value}</td>")
return f"<tr class='row-{row['id']}'>{''.join(cells)}</tr>"

# 组装表格
table = DataTable(columns, users_data)
table.set_header_slot(custom_header)
table.set_row_slot(custom_row)
print(table.render())

# 测试空数据情况
print("\n--- 空数据测试 ---\n")
empty_table = DataTable(columns, [])
empty_table.set_empty_slot("<tr><td colspan='100%'>🚫 没有任何数据,快去添加吧!</td></tr>")
print(empty_table.render())

预期输出

<table class="data-table">
<thead>
    <tr><th class='col-id'>ID 📊</th><th class='col-name'>用户名 📊</th><th class='col-role'>角色 📊</th><th class='col-status'>状态 📊</th></tr>
</thead>
<tbody>
    <tr class='row-1'><td>1</td><td>小明</td><td>管理员</td><td>🟢 活跃</td></tr>
<tr class='row-2'><td>2</td><td>小红</td><td>编辑</td><td>🟢 活跃</td></tr>
<tr class='row-3'><td>3</td><td>小刚</td><td>访客</td><td>⚫ 停用</td></tr>
</tbody>
</table>

--- 空数据测试 ---

<table class="data-table">
<thead>
    <tr><th>ID</th><th>用户名</th><th>角色</th><th>状态</th></tr>
</thead>
<tbody>
    <tr><td colspan='100%'>🚫 没有任何数据,快去添加吧!</td></tr>
</tbody>
</table>

一句话解释:这个表格组件把「表头怎么写」「每行怎么渲染」「没数据时显示什么」全都开放给父组件配置——一个组件,多种外观,代码复用率拉满。


4. 💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:插槽内容只在父组件作用域解析

❌ 错误示例

# 子组件定义了一个变量
class Child:
def __init__(self):
    self.message = "子组件的数据"

def render_with_slot(self, slot_fn):
    # 父组件的 slot_fn 拿不到 self.message,因为不在同一个作用域
    return slot_fn()  # 报错:message 未定义

# 父组件
def my_slot():
return f"<p>{message}</p>"  # message 从哪来?子组件作用域没有!

child = Child()
child.render_with_slot(my_slot)  # ❌ NameError

✅ 正确示例

class Child:
def __init__(self):
    self.message = "子组件的数据"

def render_with_slot(self, slot_fn):
    # 子组件把数据传出去,父组件才能用
    return slot_fn(self.message)

def my_slot(message):  # ✅ 接收子组件传来的数据
return f"<p>{message}</p>"

child = Child()
child.render_with_slot(my_slot)  # ✅ 正常工作

解释:作用域插槽的「作用域」指的是数据在哪个组件定义,就只能在那个组件解析。子组件的数据要「暴露」出来,父组件才能用到。


坑 2:默认插槽和具名插槽混用时的优先级

❌ 错误示例

# 子组件同时定义了默认插槽和具名插槽
# 但父组件传的内容都跑到默认插槽去了
class ConfusedComponent:
def render(self, slot_content, named_slot_content):
    # 父组件以为往 named_slot 塞内容会显示在对应位置
    # 但实际上都混在一起了
    return f"""
    <div>
        {slot_content}  <!-- 默认插槽 -->
        {named_slot_content}  <!-- 具名插槽 -->
    </div>
    """

confused = ConfusedComponent()
confused.render(
slot_content="<p>默认内容</p>",
named_slot_content="<p>具名内容</p>"
)

✅ 正确示例

# 用不同的属性分开管理不同插槽的内容
class ClearComponent:
def __init__(self):
    self.slots = {}

def set_slot(self, name, content):
    self.slots[name] = content

def render(self):
    return f"""
    <div>
        <header>{self.slots.get('header', '')}</header>
        <main>{self.slots.get('default', '默认内容')}</main>
        <footer>{self.slots.get('footer', '')}</footer>
    </div>
    """

clear = ClearComponent()
clear.set_slot("header", "表头")
clear.set_slot("default", "主体内容")  # 明确指定是默认插槽
clear.set_slot("footer", "页脚")

解释:具名插槽和默认插槽是两个独立的「坑」,要明确告诉子组件「这个内容是往哪个坑里填的」。


坑 3:忘记处理空数据

❌ 错误示例

class BadList:
def __init__(self, items):
    self.items = items

def render(self):
    # 没考虑 items 为空的情况
    return "\n".join([f"<li>{item}</li>" for item in self.items])

bad = BadList([])
print(bad.render())  # 输出空,什么都没显示,用户以为坏了

✅ 正确示例

class GoodList:
def __init__(self, items):
    self.items = items

def render(self, empty_message="暂无数据"):
    if not self.items:
        return f"<p class='empty'>{empty_message}</p>"
    return "\n".join([f"<li>{item}</li>" for item in self.items])

good = GoodList([])
print(good.render())  # 输出:<p class='empty'>暂无数据</p>

解释:组件要有「防御性编程」意识,永远假设数据可能是空的。


坑 4:插槽内容被重复渲染

❌ 错误示例

class WastefulComponent:
def __init__(self):
    self.slot_content = None

def set_slot(self, content):
    self.slot_content = content

def render_multiple(self, count=3):
    results = []
    for _ in range(count):
        # 如果 slot_content 是复杂的渲染结果,每次循环都要重新算
        results.append(self.slot_content)
    return "\n".join(results)

✅ 正确示例

class EfficientComponent:
def __init__(self):
    self.slot_content = None
    self._cached_render = None

def set_slot(self, content):
    self.slot_content = content
    self._cached_render = None  # 清空缓存,下次用时重新计算

def render_multiple(self, count=3):
    if self._cached_render is None:
        # 只计算一次
        self._cached_render = self.slot_content
    return "\n".join([self._cached_render for _ in range(count)])

解释:如果插槽内容是「纯函数」(相同输入总能得到相同输出),可以考虑缓存结果避免重复计算。


性能小贴士:避免在插槽中创建新函数

# ❌ 不推荐:每次渲染都创建新函数
class BadParent:
def render(self, child):
    for item in items:
        child.set_slot(lambda: f"<p>{item}</p>")  # 闭包陷阱!
        # 所有 lambda 引用的是同一个 item(最后一个值)

# ✅ 推荐:把数据作为参数传进去
class GoodParent:
def render(self, child):
    def make_template(item):
        return f"<p>{item}</p>"

    for item in items:
        child.set_slot(make_template(item))  # 每次循环传入不同的 item

调试技巧:用 repr 看清数据结构

# 打印插槽收到的内容,看看是什么类型
def debug_slot(*args, **kwargs):
print(f"[DEBUG] 插槽收到参数: args={args}, kwargs={kwargs}")
return f"收到 {len(args)} 个参数"

# 在组件里
class DebugComponent:
def render_with_slot(self, slot_fn):
    # 打印传给插槽的数据
    sample_data = {"name": "测试", "value": 123}
    result = slot_fn(sample_data)
    print(f"[DEBUG] 插槽返回: {repr(result)}")
    return result

debug = DebugComponent()
debug.render_with_slot(debug_slot)

输出

[DEBUG] 插槽收到参数: args=({'name': '测试', 'value': 123},), kwargs={}
[DEBUG] 插槽返回: '收到 1 个参数'

5. ✏️ 练习题 + 作业题

练习题(5 道,10 分钟内完成)

练习 1(2 分钟):默认插槽的基础使用
- 输入:创建一个 Alert 组件,不传插槽内容
- 预期输出:显示「默认提示:操作已执行」
- 提示:用 if self.slot_content 判断是否传入了自定义内容

练习 2(2 分钟):给 Alert 加个类型判断
- 输入:在练习 1 基础上,传入 error 类型的 Alert,给插槽传自定义内容
- 预期输出:显示你自定义的内容,但保留 error 类型的红色样式
- 提示:插槽内容只替换「消息文字」,不替换整个组件结构

练习 3(2 分钟):用具名插槽配置导航栏
- 输入:创建 NavBar 组件,有 leftcenterright 三个插槽
- 预期输出:<nav> 标签内分别显示「返回」「标题」「更多」
- 提示:三个具名插槽分别对应导航栏的三个位置

练习 4(2 分钟):作用域插槽传分数
- 输入:给 ScoreList 组件传入 3 个学生成绩,用插槽判断是否及格
- 预期输出:每个学生名字后面显示「✅ 及格」或「❌ 不及格」
- 提示:子组件把 {name, score} 对象传给父组件,父组件判断 score >= 60

练习 5(2 分钟):找出错误
- 输入:以下代码运行会报什么错?

class Broken:
def render(self, slot_fn):
    return slot_fn()

def my_slot():
return f"<p>{unknown_variable}</p>"

Broken().render(my_slot)
  • 预期输出:NameError: name 'unknown_variable' is not defined
  • 提示:插槽内容在父组件作用域解析,但 unknown_variable 既不在父组件也不在子组件定义

作业题(30 分钟 - 2 小时)

作业:做一个「评论列表组件」

  • 需求描述:做一个可复用的 CommentList 评论列表组件,支持嵌套回复

  • 功能点
    1. CommentList 接收评论数据(JSON 格式),渲染评论列表
    2. 每个评论用作用域插槽渲染,父组件决定评论的显示样式
    3. 支持具名插槽header(评论数统计)、empty(无评论时显示)
    4. 评论数据包含:iduseravatarcontenttimereplies(子回复数组)

  • 加分项
    1. 支持按时间排序(最新在前 / 最旧在前)
    2. 支持过滤只看一级评论(不显示回复)

  • 验收标准

  • 能跑起来,输出完整的 HTML 结构
  • 每个评论的 usercontenttime 都正确显示
  • 如果 replies 非空,能渲染出嵌套的子评论
  • 代码有适当的注释说明

  • 提交方式:评论区贴代码或 GitHub 链接


6. 📚 总结 + 资源

本文学到的 3 个核心点

  1. 插槽是「留白」:子组件挖坑,父组件填内容,实现组件的高度可定制
  2. 具名插槽解决多区域定制:一个组件可以有多个插槽,每个插槽负责一个区域
  3. 作用域插槽让数据流动:子组件暴露数据,父组件决定如何渲染——数据是「向下传递」,渲染是「向上反馈」

延伸学习资源

  1. Vue 官方文档 - 插槽:权威详细的 API 文档
  2. 《Vue3 设计与实现》:从框架设计角度理解插槽的实现原理
  3. Vue Mastery - Slots:视频教程配合作业题效果更佳

互动钩子

你在开发中有没有遇到过「组件内部结构改不了」的痛点?后来是怎么解决的?评论区聊聊,老粉优先回复!👇


下章预告:学会了插槽,你已经能自定义组件的「外形」了。但如果你想在页面上把组件渲染到另一个位置呢?比如写一个 Modal 弹窗,组件逻辑写在子组件里,但想把它渲染到 <body> 下——下一章我们来解决这个「灵魂出窍」的问题。🔮

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。