第3章 3.1 组件插槽 slot
前置章节回顾
上一章我们用 Composition API 完整地手撕了一个 Todo List,把「数据驱动视图」这个核心思想掰碎了讲。你现在应该已经能熟练运用 ref、reactive,以及 watch/computed 这些组合拳了。
但是,写到这里有个问题冒出来了:我们写的组件都是「铁板一块」,子组件内部的 DOM 结构完全由自己决定,父组件想自定义一点点内部结构都做不到。这就好比你买了一个快递,快递盒的外观、大小、甚至里面的泡沫填充物都是出厂定死的,你只能被动接受——想要换个包装?门都没有。
本章目标
学完这一章,你就能打破这个「铁板组件」的魔咒——用 slot(插槽) 这个神器,让父组件可以往子组件里「塞」任何自己想放的内容。
1. 🎯 开场 3 分钟:为什么要学这个?
场景还原:你的第一个痛点
假设你写了一个「卡片组件」UserCard:
# 模拟 Vue 的组件概念
class UserCard\n\n\n\n\n\n:
def render(self):
return f"""
<div class="card">
<h3>{self.username}</h3>
<p>用户等级:{self.level}</p>
</div>
"""
产品经理突然说:「有些卡片需要加头像,有些需要加关注按钮,有些啥都不加。」
你的第一反应是什么?
- 写多个版本的
UserCardV1、UserCardV2、UserCardV3?→ 代码爆炸 - 在
UserCard里加一堆if判断?→ 组件越来越臃肿 - 用 slot:让父组件决定卡片的「坑」里放什么 → 优雅!
第二个痛点:数据从哪来?
有时候子组件有一堆内部数据,但父组件需要根据这些数据决定怎么渲染。
比如一个下拉列表组件,子组件知道「所有选项列表」,但具体某个选项显示成什么样,父组件想自己控制。
这在 Vue 里叫作用域插槽——字面意思就是「插槽有作用域,子组件的数据能传给父组件用」。
学完本文你能解决
- 让任何组件变得「可定制」——父组件想塞什么就塞什么
- 实现默认内容——如果父组件没传,就显示默认值
- 让子组件的数据流向父组件——父组件根据数据渲染不同内容
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>
这行代码在干嘛:子组件为 header 和 footer 两个区域预留了坑位,父组件可以精准地往指定坑位填内容。
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 组件,有 left、center、right 三个插槽
- 预期输出:<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. 评论数据包含:id、user、avatar、content、time、replies(子回复数组) -
加分项:
1. 支持按时间排序(最新在前 / 最旧在前)
2. 支持过滤只看一级评论(不显示回复) -
验收标准:
- 能跑起来,输出完整的 HTML 结构
- 每个评论的
user、content、time都正确显示 - 如果
replies非空,能渲染出嵌套的子评论 -
代码有适当的注释说明
-
提交方式:评论区贴代码或 GitHub 链接
6. 📚 总结 + 资源
本文学到的 3 个核心点
- 插槽是「留白」:子组件挖坑,父组件填内容,实现组件的高度可定制
- 具名插槽解决多区域定制:一个组件可以有多个插槽,每个插槽负责一个区域
- 作用域插槽让数据流动:子组件暴露数据,父组件决定如何渲染——数据是「向下传递」,渲染是「向上反馈」
延伸学习资源
- Vue 官方文档 - 插槽:权威详细的 API 文档
- 《Vue3 设计与实现》:从框架设计角度理解插槽的实现原理
- Vue Mastery - Slots:视频教程配合作业题效果更佳
互动钩子
你在开发中有没有遇到过「组件内部结构改不了」的痛点?后来是怎么解决的?评论区聊聊,老粉优先回复!👇
下章预告:学会了插槽,你已经能自定义组件的「外形」了。但如果你想在页面上把组件渲染到另一个位置呢?比如写一个 Modal 弹窗,组件逻辑写在子组件里,但想把它渲染到 <body> 下——下一章我们来解决这个「灵魂出窍」的问题。🔮

评论(0)