阅读视图
你以为 Props 只是传参? 不,它是 React 组件设计的“灵魂系统”
90% 的 React 初学者,都低估了 Props。
他们以为它只是“从父组件往子组件传点数据”。
但真正写过复杂组件、设计过通用组件的人都知道一句话:
Props 决定了一个组件“好不好用”,而不是“能不能用”。
这篇文章,我们不讲 API 清单、不背概念,
而是围绕 Props 系统的 5 个核心能力,一次性讲透 React 组件化的底层逻辑:
- Props 传递
- Props 解构
- 默认值(defaultProps / 默认参数)
- 类型校验(PropTypes)
- children 插槽机制(React 的核武器)
👉 看完你会明白:
React 真正厉害的不是 JSX,而是 Props 设计。
一、Props 的本质:组件的“对外接口”
先抛一个结论:
React 组件 ≈ 一个函数 + 一套 Props 接口
来看一个最简单的组件 👇
function Greeting(props) {
return <h1>Hello, {props.name}</h1>
}
使用时:
<Greeting name="白兰地" />
很多人到这里就停了,但问题是:
❓
name到底是什么?
答案是:
name 不是变量,是组件对外暴露的能力。
Props 本质上是:
- 父组件 👉 子组件的输入
- 组件作者 👉 使用者的约定
二、Props 解构:不是语法糖,而是“设计声明”
对比两种写法 👇
❌ 不推荐
function Greeting(props) {
return <h1>Hello, {props.name}</h1>
}
✅ 推荐
function Greeting({ name }) {
return <h1>Hello, {name}</h1>
}
为什么?
解构不是为了少写字,而是为了表达意图。
当你看到函数签名:
function Greeting({ name, message, showIcon }) {}
你立刻就知道:
- 这个组件“需要什么”
- 组件的“输入边界”在哪里
👉 好的组件,从函数签名就能读懂。
三、Props 默认值:组件“健壮性”的第一步
看这个组件 👇
function Greeting({ name, message }) {
return (
<div>
<h1>Hello, {name}</h1>
<p>{message}</p>
</div>
)
}
如果使用者这么写:
<Greeting name="空瓶" />
会发生什么?
message === undefined
这时候就轮到 默认值 出场了。
方式一:defaultProps(经典)
Greeting.defaultProps = {
message: 'Welcome!'
}
方式二:解构默认值(更推荐)
function Greeting({ name, message = 'Welcome!' }) {}
💡默认值不是兜底,而是组件设计的一部分。
它代表的是:
- “在你不配置的情况下”
- “组件应该表现成什么样”
四、Props 类型校验:组件的“自说明文档”
来看一段很多人忽略、但非常值钱的代码 👇
import PropTypes from 'prop-types'
Greeting.propTypes = {
name: PropTypes.string.isRequired,
message: PropTypes.string,
showIcon: PropTypes.bool,
}
很多人会说:
“这不是可有可无吗?”
但在真实项目里,它解决的是:
- ❌ 参数传错没人发现
- ❌ 新人不知道组件怎么用
- ❌ 组件一多,全靠猜
🔍 PropTypes 的真正价值
不是防 bug,而是“降低理解成本”。
当你看到 propTypes,就等于看到一份说明书:
- 哪些 props 必须传?
- 哪些是可选?
- 类型是什么?
👉 一个没有 propTypes 的通用组件,本质上是“黑盒”。
五、children:React Props 系统的“王炸”
如果只能选一个 Props 机制,我会毫不犹豫选:
🧨 children
来看一个 Card 组件 👇
const Card = ({ children, className = '' }) => {
return (
<div className={`card ${className}`}>
{children}
</div>
)
}
使用时:
<Card className="user-card">
<h2>张三</h2>
<p>高级前端工程师</p>
<button>查看详情</button>
</Card>
这里发生了一件非常重要的事情:
组件不再关心“内容是什么”。
🧠 children 的设计哲学
组件负责“骨架”,使用者负责“填充”。
- Card 只负责:边框、阴影、间距
- children 决定:展示什么内容
这让组件具备了两个特性:
- ✅ 高度复用
- ✅ 永不过期
六、children + Props = 通用组件的终极形态
再看一个更高级的例子:Modal 👇
<Modal HeaderComponent={MyHeader} FooterComponent={MyFooter}>
<p>这是一个弹窗</p>
<p>你可以在这里显示任何 JSX</p>
</Modal>
Modal 的实现:
function Modal({ HeaderComponent, FooterComponent, children }) {
return (
<div>
<HeaderComponent />
{children}
<FooterComponent />
</div>
)
}
这背后是一个非常高级的思想:
Props 不只是数据,也可以是组件。
七、请记住这 5 条 Props 设计铁律
🔥 如果你只能记住一段话,请记住这里
- Props 是组件的“对外接口”,不是随便传的变量
- 解构 Props,是在声明组件的能力边界
- 默认值,决定组件的“基础体验”
- 类型校验,让组件自带说明书
- children,让组件从“可用”变成“好用”
八、写在最后
当你真正理解 Props 之后,你会发现:
- React 不只是 UI 库
- 它在教你如何设计 API
- 如何让别人“用得爽”
Props 写得好不好,决定了一个人 React 水平的上限。
🏒 前端 AI 应用实战:用 Vue3 + Coze,把宠物一键变成冰球运动员!
不是 AI 不够强,而是你还没把它“接进前端”
这是一篇真正「前端视角」的 AI 应用落地实战,而不是模型科普。
🤔 为什么我要做这个「宠物冰球员」AI 应用?
最近刷掘金,你一定发现了一个现象 👇
- AI 很火
- 大模型很强
- 但真正能跑起来的 前端 AI 应用很少
很多同学卡在这一步:
❌ 会 Vue / React
❌ 会调接口
❌ 但不知道 AI 项目整体该怎么搭
于是我做了这个项目。
🎯 项目一句话介绍
上传一张宠物照片,生成一张专属“冰球运动员形象照”
而且不是随便生成,而是可控的 AI👇
- 🧢 队服编号
- 🎨 队服颜色
- 🏒 场上位置(守门员 / 前锋 / 后卫)
- ✋ 持杆方式(左 / 右)
- 🎭 绘画风格(写实 / 日漫 / 国漫 / 油画 / 素描)
📌 这是一个典型的「活动型 AI 应用」
非常适合:
- 冰球协会宣传
- 宠物社区裂变
- 活动拉新
- 朋友圈分享
🧠 整体架构:前端 + AI 是怎么配合的?
先上结论👇
前端负责“意图”,AI 负责“生成”
整体流程非常清晰:
Vue3 前端
↓
图片上传(Coze 文件 API)
↓
调用 Coze 工作流
↓
AI 生成图片
↓
前端展示结果
🧩 技术选型一览
| 模块 | 技术 |
|---|---|
| 前端 | Vue3 + Composition API |
| AI 编排 | Coze 工作流 |
| 网络 | fetch / HTTP |
| 上传 | FormData |
| 状态 | ref 响应式 |
🖼️ 前端第一难点:图片上传 & 预览
AI 应用里,最容易被忽略的不是 AI,而是用户体验。
❓ 一个问题
图片很大,用户点「生成」之后什么都没发生,会怎样?
答案是:
他以为你的网站卡死了
✅ 解决方案:本地预览(不等上传)
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = e => {
imgPreview.value = e.target.result
}
📌 这里的关键点是:
FileReaderreadAsDataURL- base64 直接渲染
图片还没上传,用户已经“看见反馈”了
🎛️ 表单不是表单,而是「AI 参数面板」
很多人写表单是为了提交数据
但 AI 应用的表单,本质是 Prompt 的一部分
<select v-model="style">
<option value="写实">写实</option>
<option value="日漫">日漫</option>
<option value="油画">油画</option>
</select>
最终在调用工作流时,变成:
parameters: {
style,
uniform_color,
uniform_number,
position,
shooting_hand
}
💡 前端的职责不是“生成 AI”
💡 而是“让 AI 更听话”
🤖 AI 真正干活的地方:Coze 工作流
一个非常重要的认知👇
❌ AI 逻辑不应该写在前端
✅ AI 逻辑应该写在「工作流」里
🧩 我的 Coze 工作流结构(核心)
你搭建的工作流大致包含:
- 📷 图片理解(imgUnderstand)
- 🔍 特征提取
- 📝 Prompt 生成
- 🎨 图片生成
- 🔗 输出图片 URL
👉 工作流地址(可直接参考)
🔗 www.coze.cn/work_flow?w…
📌 工作流 = AI 后端
前端只需要做一件事👇
fetch('https://api.coze.cn/v1/workflow/run', {
method: 'POST',
headers: {
Authorization: `Bearer ${patToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
workflow_id,
parameters
})
})
📤 文件上传:前端 AI 项目的必修课
❓ 为什么不能直接把图片传给工作流?
因为:
- 工作流不能直接接收本地文件
- 必须先上传,换一个
file_id
✅ 正确姿势:FormData
const formdata = new FormData()
formdata.append('file', input.files[0])
返回结果中会拿到:
{
"data": {
"id": "file_xxx"
}
}
然后在工作流参数里传👇
picture: JSON.stringify({ file_id })
📌 AI 应用用的还是老朋友:HTTP + 表单
⏳ 状态管理:AI 应用的“信任感来源”
AI ≠ 秒出结果
所以状态提示非常重要👇
status.value = "图片上传中..."
status.value = "正在生成..."
如果出错👇
if (ret.code !== 0) {
status.value = ret.msg
}
一个没有状态提示的 AI 应用 = 不可用
⚠️ AI 应用的三个“隐藏坑”
1️⃣ AI 是慢的
- loading 必须有
- 按钮要禁用
- 用户要知道现在在干嘛
2️⃣ AI 是不稳定的
- 可能失败
- 可能生成不符合预期
- 可能 URL 为空
📌 前端必须兜底,而不是假设 AI 永远成功
3️⃣ AI 应用 ≠ CRUD
它更像一次:
用户意图 → AI 理解 → 内容生成 → 结果反馈
✅ 做完这个项目,你真正掌握了什么?
如果你完整跑通这套流程,你至少学会了👇
- ✅ Vue3 Composition API 实战
- ✅ 文件上传 & 图片预览
- ✅ AI 工作流的正确使用方式
- ✅ 前端如何“驱动 AI”
- ✅ 一个完整 AI 应用的工程思路
✍️ 写在最后:前端 + AI 的真正价值
很多人担心👇
「前端会不会被 AI 取代?」
我的答案是:
❌ 只会写页面的前端会被取代
✅ 会设计 AI 交互体验的前端不会
AI 很强
但AI 不知道用户要什么
而前端,正是连接「用户意图」和「AI 能力」的桥梁。
JavaScript 列表转树(List to Tree)详解:前端面试中如何从递归 O(n²) 优化到一次遍历 O(n)
前言:Offer 是怎么没的?
在前端面试的江湖里,「列表转树(List to Tree)」 是一道妥妥的高频题。
很多同学一看到这道题,内心 OS 都是:
😎「简单啊,递归!」
代码写完,自信抬头。
面试官却慢悠悠地问了一句:
🤨「如果是 10 万条数据 呢?
👉 时间复杂度多少?
👉 会不会栈溢出?」
空气突然安静。
今天这篇文章,我们就把这道题彻底拆开:
从「能写」到「写得对」,再到「写得漂亮」。
一、为什么面试官总盯着这棵“树”?
因为在真实业务中,后端给你的几乎永远是扁平数据。
例如:
const list = [
{ id: 1, parentId: 0, name: '北京市' },
{ id: 2, parentId: 1, name: '顺义区' },
{ id: 3, parentId: 1, name: '朝阳区' },
{ id: 4, parentId: 2, name: '后沙峪' },
{ id: 121, parentId: 0, name: '江西省' },
{ id: 155, parentId: 121, name: '抚州市' }
];
而前端组件(Menu、Tree、Cascader)要的却是👇
省
└─ 市
└─ 区
🎯 面试官的真实考点
-
数据结构理解:是否真正理解
parentId - 递归意识 & 代价:不只会写,还要知道坑在哪
-
性能优化能力:能否从
O(n²)优化到O(n) - JS 引用理解:是否理解对象在内存中的表现
二、第一重境界:递归法(能写,但不稳)
1️⃣ 最基础的递归写法
function list2tree(list, parentId = 0) {
return list
.filter(item => item.parentId === parentId)
.map(item => ({
...item,
children: list2tree(list, item.id)
}));
}
逻辑非常直观:
- 找当前
parentId的所有子节点 - 对每个子节点继续递归
- 没有子节点时自然退出
三、进阶:ES6 优雅写法(看起来很高级)
如果你在面试中写出下面这段代码👇
面试官大概率会先点头。
const list2tree = (list, parentId = 0) =>
list
.filter(item => item.parentId === parentId)
.map(item => ({
...item, // 解构赋值,保持原对象纯净
children: list2tree(list, item.id)
}));
这一版代码:
- ✅ 箭头函数
- ✅
filter + map链式调用 - ✅ 解构赋值,不污染原数据
- ✅ 可读性很好,看起来很“ES6”
👉 很多同学到这一步就觉得稳了。
🤔 面试官的经典追问
「这个方案,有什么问题?」
🎯 标准回答(一定要说出来)
「这个方案的本质是 嵌套循环。
每一层递归,都会遍历一次完整的list。👉 时间复杂度是
O(n²)
👉 如果层级过深,还可能导致 栈溢出(Stack Overflow) 。」
📌 一句话总结:
ES6 写法只是“看起来优雅”,
性能问题不会因为代码好看就自动消失。
四、第二重境界:Map 优化(面试及格线)
既然慢,是因为反复遍历找父节点,
那就用 Map 建立索引。
👉 典型的:空间换时间
核心思路
- 第一遍:把所有节点放进
Map - 第二遍:通过
parentId直接挂载 - 利用 JS 对象引用,自动同步树结构
代码实现
function listToTreeWithMap(list) {
const map = new Map();
const tree = [];
// 初始化
for (const item of list) {
map.set(item.id, { ...item, children: [] });
}
// 构建树
for (const item of list) {
const node = map.get(item.id);
if (item.parentId === 0) {
tree.push(node);
} else {
const parent = map.get(item.parentId);
parent && parent.children.push(node);
}
}
return tree;
}
⏱ 复杂度分析
- 时间复杂度:
O(n) - 空间复杂度:
O(n)
📌 到这一步,已经可以应付大多数面试了。
五、终极奥义:一次遍历 + 引用魔法(Top Tier)
面试官:
「能不能只遍历一次?」
答案是:能,而且这才是天花板解法。
核心精髓:占位 + 引用同步
- 子节点可能先于父节点出现
- 先在 Map 里给父节点 占位
- 后续再补全数据
- 引用地址始终不变,树会“自己长好”
代码实现(一次遍历)
function listToTreePerfect(list) {
const map = new Map();
const tree = [];
for (const item of list) {
const { id, parentId } = item;
if (!map.has(id)) {
map.set(id, { children: [] });
}
const node = map.get(id);
Object.assign(node, item);
if (parentId === 0) {
tree.push(node);
} else {
if (!map.has(parentId)) {
map.set(parentId, { children: [] });
}
map.get(parentId).children.push(node);
}
}
return tree;
}
🏆 为什么这是王者解法?
- ✅ 一次遍历,
O(n) - ✅ 支持乱序数据
- ✅ 深度理解 JS 引用机制
- ✅ 面试官一眼就懂你是“真会”
六、真实开发中的应用场景
- 🔹 权限 / 菜单树(Ant Design / Element)
- 🔹 省市区 / Cascader
- 🔹 文件目录结构(云盘、编辑器)
七、面试总结 & 避坑指南
| 方案 | 时间复杂度 | 评价 |
|---|---|---|
| 递归 | O(n²) |
能写,但危险 |
| Map 两次遍历 | O(n) |
面试合格 |
| 一次遍历 | O(n) |
面试加分 |
面试加分表达
- 主动提 空间换时间
- 点出 JS 对象是引用类型
- 询问
parentId是否可能为null - 说明是否会修改原数据(必要时深拷贝)
结语
算法不是为了为难人,
而是为了在复杂业务中,
选出那条最稳、最优雅的路。
如果这篇文章对你有帮助👇
👍 点个赞
💬 评论区聊聊你在项目里遇到过的奇葩数据结构
React Hooks 深度理解:useState / useEffect 如何管理副作用与内存
🤯你以为 React Hooks 只是语法糖?
不——它们是在帮你对抗「副作用」和「内存泄漏」
如果你只把 Hooks 当成“不用 class 了”,
那你可能只理解了 React 的 10%。
🚀 一、一个“看起来毫无问题”的组件
我们先从一个你我都写过无数次的组件开始:
function App() {
const [num, setNum] = useState(0)
return (
<div onClick={() => setNum(num + 1)}>
{num}
</div>
)
}
看起来非常完美:
- ✅ 没有 class
- ✅ 没有 this
- ✅ 就是一个普通函数
但问题是:
React 为什么要发明 Hooks?
useState / useEffect 到底解决了什么“本质问题”?
答案其实只有一个关键词👇
💣 二、React 世界的终极敌人:副作用(Side Effect)
React 背后有一个很少被明说,但极其重要的信仰:
组件 ≈ 纯函数
🧠 什么是纯函数?
- 相同输入 → 永远相同输出
- 不依赖外部变量
- 不产生额外影响(I/O、定时器、请求)
function add(a, b) {
return a + b
}
而理想中的 React 组件是:
(props + state) → JSX
React 希望你“只负责算 UI”,
而不是在渲染时干别的事。
⚠️ 但现实是:你必须干“坏事”
真实业务中,你不可避免要做这些事:
- 🌐 请求接口
- ⏱️ 设置定时器
- 🎧 事件监听
- 📦 订阅 / 取消订阅
- 🧱 操作 DOM
这些行为有一个共同点👇
❌ 它们都不是纯函数行为
✅ 它们都是副作用
如果你直接把副作用写进组件函数,会发生什么?
function App() {
fetch('/api/data') // ❌
return <div />
}
👉 每一次 render 都请求
👉 状态更新 → 再 render → 再请求
👉 组件直接失控
🧯 三、useEffect:副作用的“隔离区”
useEffect 的存在,本质只干一件事:
把副作用从“渲染阶段”挪走
useEffect(() => {
// 副作用逻辑
}, [])
💡 一句话理解:
render 阶段必须纯,
effect 阶段允许脏。
📦 四、依赖数组不是细节,而是“副作用边界”
1️⃣ 只执行一次(挂载)
useEffect(() => {
console.log('mounted')
}, [])
- 只在组件挂载时执行
- 类似 Vue 的
onMounted
2️⃣ 依赖变化才执行
useEffect(() => {
console.log(num)
}, [num])
-
num变化 → 执行 - 不变 → 不执行
依赖数组的本质是:
“这个副作用依赖谁?”
3️⃣ 不写依赖项?
useEffect(() => {
console.log('every render')
})
👉 每次 render 都执行
👉 99% 的时候是性能陷阱
💥 五、90% 新手都会踩的坑:内存泄漏
来看一个极其经典的 Hooks 错误写法👇
useEffect(() => {
const timer = setInterval(() => {
console.log(num)
}, 1000)
}, [num])
你觉得这段代码有问题吗?
有,而且非常致命。
❌ 问题在哪里?
-
num每变一次 - effect 重新执行
- 新建一个定时器
- ❗旧定时器还活着
结果就是:
- ⏱️ 定时器越来越多
- 📈 内存持续上涨
- 💥 控制台疯狂打印
- 🧠 内存泄漏
🧹 六、useEffect return:副作用的“善终机制”
React 给你准备了一个官方清理通道👇
useEffect(() => {
const timer = setInterval(() => {
console.log(num)
}, 1000)
return () => {
clearInterval(timer)
}
}, [num])
⚠️ 重点来了
return 的函数不是“卸载时才执行”
而是:
下一次 effect 执行前,一定会先执行它
React 内部顺序是这样的:
- 执行上一次 effect 的 cleanup
- 再执行新的 effect
👉 这就是 Hooks 防内存泄漏的核心设计
🧠 七、useState:为什么初始化不能异步?
你在学习 Hooks 时,一定问过这个问题👇
❓ 我能不能在 useState 初始化时请求接口?
useState(async () => {
const data = await fetchData()
return data
})
答案很干脆:
❌ 不行
🤔 为什么不行?
因为 React 必须保证:
- 首次 render 立即有确定的 state
- 异步结果是不确定的
- state 一旦初始化,必须是同步值
React 允许的只有这种👇
useState(() => {
const a = 1 + 2
const b = 2 + 3
return a + b
})
💡 这叫 惰性初始化
💡 但前提是:同步 + 纯函数
🌐 八、那异步请求到底该写哪?
答案只有一个地方:
useEffect
useEffect(() => {
async function query() {
const data = await queryData()
setNum(data)
}
query()
}, [])
🎯 这是 React 官方推荐模式
- state 初始化 → 确定
- 异步请求 → 副作用
- 数据回来 → 更新状态
🔄 九、为什么 setState 可以传函数?
setNum(prev => prev + 1)
这不是“花里胡哨”,而是并发安全设计。
React 内部可能会:
- 合并多次更新
- 延迟执行 setState
如果你直接用 num + 1,很可能拿到的是旧值。
函数式 setState = 永远安全
🏁 十、Hooks 的真正价值(总结)
如果你只把 Hooks 当成:
“不用写 class 了”
那你只看到了表面。
Hooks 真正解决的是:
- 🧩 状态如何在函数中稳定存在
- 🧯 副作用如何被精确控制
- 🧠 生命周期如何显式建模
- 🔒 内存泄漏如何被主动规避
✨ 最后的掘金金句
useState 解决的是:数据如何“活着”
useEffect 解决的是:副作用如何“善终”React Hooks 不只是语法升级,
而是一场从“命令式生命周期”
到“声明式副作用管理”的革命。