递归:别再"展开脑补"了,学会"信任"才是关键

最近在做 LeetCode 的深度对象过滤题(2823)时,我遇到了一个困扰很久的问题:虽然知道递归是"函数调用自己",也能照着模板写出代码,但每次遇到递归都要在脑子里"展开"整个调用过程------factorial(3) 调用 factorial(2)factorial(2) 又调用 factorial(1)......稍微复杂一点就会晕。

后来我意识到,这种思维方式本身就是错的。真正的递归思维不是"展开脑补",而是"相信递归"。这个转变让我重新理解了递归的本质。

这篇文章是我重新学习递归的总结,不是权威教程,而是从识别、模式、实战、思想四个维度的完整梳理。如果你也在递归上遇到困难,希望这篇文章能帮到你。

一、准确识别:什么时候需要递归?

在学习具体技巧之前,我发现最重要的能力是:快速判断一个问题是否需要递归

递归问题的三大特征

根据我的观察,递归问题通常有以下特征:

特征 1:问题具有"自相似性"

大问题 = 小问题 + 小问题 + ...(结构相同)

例子:

  • 计算阶乘:n! = n × (n-1)!
  • 遍历文件夹:遍历文件夹 = 遍历文件 + 遍历子文件夹
  • 汉诺塔:移动 n 个盘子 = 移动 (n-1) 个盘子 + 移动 1 个盘子 + 移动 (n-1) 个盘子

这种"自相似性"是递归最核心的特征------问题可以分解成和自己结构相同但规模更小的子问题。

特征 2:数据结构是递归的

javascript 复制代码
// 环境: JavaScript
// 场景: 典型的递归数据结构

// 树结构:节点包含子节点
const tree = {
  value: 1,
  children: [
    {
      value: 2,
      children: [
        { value: 4, children: [] },
        { value: 5, children: [] }
      ]
    },
    { value: 3, children: [] }
  ]
};

// 嵌套对象:对象包含对象
const nested = {
  user: {
    profile: {
      settings: {
        theme: 'dark'
      }
    }
  }
};

// 嵌套数组:数组包含数组
const comments = [
  {
    id: 1,
    text: '评论',
    replies: [
      { id: 2, text: '回复', replies: [] }
    ]
  }
];

关键特征

  • 不知道有多少层
  • 每一层结构相同
  • 循环无法处理(层数未知

特征 3:有明确的"最小单元"(终止条件)

能回答这两个问题,就是递归问题:

问题 1:最简单的情况是什么?

  1. 阶乘:n = 0 或 n = 1 时,直接返回 1

  2. 遍历树:节点没有子节点时,停止遍历

问题 2:如何缩小问题规模?

  1. 阶乘:n! = n × (n-1)!,问题从 n 缩小到 n-1

  2. 遍历树:处理当前节点,然后遍历每个子节点

识别清单:一眼判断

遇到问题时,我会用这个清单快速判断:

✅ 强烈提示需要递归

  • 题目出现"深度"、"嵌套"、"层级"、"树"、"图"
  • 数据结构是嵌套对象/嵌套数组/树/图
  • 题目要求"遍历所有可能"(组合、排列、路径)
  • 不知道数据有多少层

⚠️ 可能需要递归

  • 题目涉及"分治"思想(二分、归并)
  • 需要"回溯"(尝试-撤销-再尝试)
  • 问题可以对半分解

❌ 不需要递归

  • 简单的顺序遍历(一个 for 循环搞定)
  • 已知固定层数(几个嵌套循环搞定)
  • 数组/字符串的简单处理(map/filter/reduce)

常见递归问题分类

我把常见的递归问题归为四类:

分类 应用场景
数/图遍历 1) DOM 树遍历(查找特定元素) 2) 文件系统遍历(统计文件大小) 3) 组织架构遍历(查找员工) 4) 评论楼层(无限嵌套评论) 5) 菜单树(多级菜单展开)
数据转换 1) 深拷贝/深度克隆 2) 扁平化嵌套数组/对象 3) 对象深度过滤/映射 4) JSON 格式转换 5) 数据清洗(移除 null/undefined)
搜索/回溯 1) 路径查找(面包屑导航) 2) 组合生成(选择题所有答案) 3) 排列生成(密码破解) 4) 迷宫求解
分治算法 1) 二分查找(搜索优化) 2) 归并排序(大数据排序) 3) 快速排序 4) 计算次方(快速幂)

识别练习

这是我整理的一些练习题,可以测试一下判断能力:

markdown 复制代码
1. 反转字符串                          → ❌ 用 reverse() 或循环
2. 计算嵌套对象的最大【深度】               → ✅ 递归(不知道多少层)
3. 数组求和                            → ❌ 用 reduce
4. 查找【二叉搜索树】中的节点                → ✅ 递归(树结构)
5. 打印 1-100                         → ❌ 循环
6. 获取【所有子孙评论】(无限嵌套)           → ✅ 递归(层数未知)
7. 对象数组按某字段排序                  → ❌ 用 sort
8. 【遍历】 React 组件树找某个组件           → ✅ 递归(组件树)
9. 扁平化三层嵌套数组                   → ❌ 已知层数,可以循环
10. 扁平化【任意深度】数组                  → ✅ 递归(深度未知)

判断的关键 :如果能用固定次数的循环 解决,就不需要递归。递归是为了处理"层数未知"的情况。

二、常用模式:递归函数库

通过整理常见问题,我发现大部分递归都可以归纳为几个固定模式。掌握这些模式,就能快速写出递归代码。

核心模板

模板 1:单路径递归(最常见)

javascript 复制代码
// 环境: JavaScript
// 场景: 处理线性递归问题

function recursion(data) {
  // 1. 终止条件(base case)
  if (isBaseCase(data)) {
    return baseResult;
  }
  
  // 2. 处理当前层(可选)
  const currentResult = process(data);
  
  // 3. 递归子问题
  const subResult = recursion(getSubProblem(data));
  
  // 4. 合并结果
  return combine(currentResult, subResult);
}

// 例子:计算阶乘
function factorial(n) {
  if (n === 0) return 1;              // 终止条件
  return n * factorial(n - 1);        // 递归 + 合并
}

模板 2:多路径递归(树/图)

javascript 复制代码
// 环境: JavaScript
// 场景: 处理树形结构

function recursion(node) {
  // 1. 终止条件
  if (!node) return baseResult;
  
  // 2. 处理当前节点
  const currentResult = process(node);
  
  // 3. 递归所有子节点
  const childResults = node.children.map(child => 
    recursion(child)
  );
  
  // 4. 合并结果
  return combine(currentResult, ...childResults);
}

// 例子:计算树的最大深度
function maxDepth(node) {
  if (!node) return 0;
  
  const depths = node.children.map(child => maxDepth(child));
  return 1 + Math.max(...depths, 0);
}

模板 3:回溯递归(搜索)

javascript 复制代码
// 环境: JavaScript
// 场景: 生成所有可能的组合

function backtrack(path, choices, result) {
  // 1. 终止条件:路径完整
  if (isComplete(path)) {
    result.push([...path]);
    return;
  }
  
  // 2. 遍历所有选择
  for (const choice of choices) {
    // 做选择
    path.push(choice);
    
    // 递归(缩小选择范围)
    backtrack(path, getNextChoices(choices, choice), result);
    
    // 撤销选择(回溯)
    path.pop();
  }
}

// 例子:生成所有子集
function subsets(nums) {
  const result = [];
  
  function backtrack(path, start) {
    result.push([...path]);
    
    for (let i = start; i < nums.length; i++) {
      path.push(nums[i]);
      backtrack(path, i + 1);
      path.pop();
    }
  }
  
  backtrack([], 0);
  return result;
}

实用递归函数库

下面是我整理的 5 个实用递归函数,几乎覆盖了日常开发的所有场景:

函数 1:深度遍历(DFS)

javascript 复制代码
// 环境: JavaScript
// 场景: 遍历树/图的所有节点

function dfs(node, visit) {
  if (!node) return;
  
  // 访问当前节点
  visit(node);
  
  // 递归访问子节点
  if (node.children) {
    node.children.forEach(child => dfs(child, visit));
  }
}

// 使用:统计所有节点
let count = 0;
dfs(tree, () => count++);
console.log(count);

// 使用:查找特定节点
let found = null;
dfs(tree, node => {
  if (node.id === targetId) found = node;
});

函数 2:深度映射(Deep Map)

javascript 复制代码
// 环境: JavaScript
// 场景: 转换嵌套结构的每个值

function deepMap(obj, fn) {
  // 终止条件:基础类型
  if (typeof obj !== 'object' || obj === null) {
    return fn(obj);
  }
  
  // 数组:递归映射
  if (Array.isArray(obj)) {
    return obj.map(item => deepMap(item, fn));
  }
  
  // 对象:递归映射
  const result = {};
  for (const key in obj) {
    result[key] = deepMap(obj[key], fn);
  }
  return result;
}

// 使用:所有数字乘以 2
const data = { a: 1, b: { c: 2, d: [3, 4] } };
const doubled = deepMap(data, value => 
  typeof value === 'number' ? value * 2 : value
);
console.log(doubled);
// { a: 2, b: { c: 4, d: [6, 8] } }

函数 3:深度过滤(Deep Filter)

javascript 复制代码
// 环境: JavaScript
// 场景: 过滤嵌套结构,移除不符合条件的值(LeetCode 2823)

function deepFilter(obj, fn) {
  // 终止条件:基础类型
  if (typeof obj !== 'object' || obj === null) {
    return fn(obj) ? obj : undefined;
  }
  
  // 数组:递归过滤
  if (Array.isArray(obj)) {
    const filtered = obj
      .map(item => deepFilter(item, fn))
      .filter(item => item !== undefined);
    return filtered.length > 0 ? filtered : undefined;
  }
  
  // 对象:递归过滤
  const result = {};
  for (const key in obj) {
    const filtered = deepFilter(obj[key], fn);
    if (filtered !== undefined) {
      result[key] = filtered;
    }
  }
  return Object.keys(result).length > 0 ? result : undefined;
}

// 使用:移除所有负数
const data = { a: 1, b: -2, c: { d: 3, e: -4 } };
const positive = deepFilter(data, x => x > 0);
console.log(positive); // { a: 1, c: { d: 3 } }

函数 4:深度克隆(Deep Clone)

javascript 复制代码
// 环境: JavaScript
// 场景: 完全复制嵌套对象(处理循环引用)

function deepClone(obj, map = new WeakMap()) {
  // 处理循环引用
  if (map.has(obj)) return map.get(obj);
  
  // 终止条件:基础类型
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  // 特殊对象
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  // 数组/对象:递归克隆
  const clone = Array.isArray(obj) ? [] : {};
  map.set(obj, clone);
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], map);
    }
  }
  
  return clone;
}

// 使用:克隆复杂对象
const original = {
  date: new Date(),
  regex: /test/g,
  nested: { a: 1 }
};
const cloned = deepClone(original);
cloned.nested.a = 999;
console.log(original.nested.a); // 1(没有被修改)

函数 5:路径查找(Find Path)

javascript 复制代码
// 环境: JavaScript
// 场景: 找到目标节点的路径

function findPath(tree, targetId, path = []) {
  // 找到目标
  if (tree.id === targetId) {
    return [...path, tree.id];
  }
  
  // 递归查找子节点
  if (tree.children) {
    for (const child of tree.children) {
      const found = findPath(child, targetId, [...path, tree.id]);
      if (found) return found;
    }
  }
  
  return null;
}

// 使用:查找组织架构中的员工路径
const org = {
  id: 1, name: 'CEO',
  children: [
    {
      id: 2, name: 'CTO',
      children: [
        { id: 4, name: '研发经理' }
      ]
    }
  ]
};

console.log(findPath(org, 4)); // [1, 2, 4]

递归优化技巧

掌握了基础模式后,还需要了解一些优化技巧:

技巧 1:记忆化(Memoization)

javascript 复制代码
// 环境: JavaScript
// 场景: 避免重复计算

// 斐波那契(慢,大量重复计算)
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

console.time('fib(40)');
fib(40); // 几秒钟
console.timeEnd('fib(40)');

// 记忆化版本(快)
function fibMemo(n, memo = {}) {
  if (n <= 1) return n;
  if (memo[n]) return memo[n]; // 已计算过,直接返回
  
  memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
  return memo[n];
}

console.time('fibMemo(40)');
fibMemo(40); // 瞬间完成
console.timeEnd('fibMemo(40)');

技巧 2:转迭代(避免栈溢出)

javascript 复制代码
// 环境: JavaScript
// 场景: 深度很深的情况,转为迭代

// 递归版本(深度 > 10000 可能栈溢出)
function traverseRecursive(tree, visit) {
  visit(tree);
  if (tree.children) {
    tree.children.forEach(child => traverseRecursive(child, visit));
  }
}

// 迭代版本(用栈模拟递归)
function traverseIterative(tree, visit) {
  const stack = [tree];
  
  while (stack.length > 0) {
    const node = stack.pop();
    visit(node);
    
    if (node.children) {
      // 注意:倒序入栈,保证顺序一致
      for (let i = node.children.length - 1; i >= 0; i--) {
        stack.push(node.children[i]);
      }
    }
  }
}

三、真实场景:递归在业务中的应用

理论总是抽象的,让我看看实际开发中哪些地方会用到递归。根据我的经验,前端开发中递归最常见的场景是处理树形数据。

场景 1:无限层级评论系统

这可能是我遇到最多的递归场景。

javascript 复制代码
// 环境: React
// 场景: 递归渲染评论列表

// 数据结构
const comments = [
  {
    id: 1,
    text: '一级评论',
    author: 'Alice',
    replies: [
      {
        id: 2,
        text: '二级评论',
        author: 'Bob',
        replies: [
          {
            id: 3,
            text: '三级评论',
            author: 'Charlie',
            replies: []
          }
        ]
      }
    ]
  }
];

// 递归组件
function CommentList({ comments, level = 0 }) {
  return (
    <div style={{ marginLeft: level * 20 }}>
      {comments.map(comment => (
        <div key={comment.id}>
          <div className="comment">
            <strong>{comment.author}</strong>: {comment.text}
          </div>
          
          {/* 递归渲染子评论 */}
          {comment.replies && comment.replies.length > 0 && (
            <CommentList 
              comments={comment.replies} 
              level={level + 1} 
            />
          )}
        </div>
      ))}
    </div>
  );
}

// 常见操作:统计总评论数
function countComments(comments) {
  let count = comments.length;
  
  comments.forEach(comment => {
    if (comment.replies) {
      count += countComments(comment.replies); // 递归
    }
  });
  
  return count;
}

场景 2:组织架构树

javascript 复制代码
// 环境: JavaScript
// 场景: 处理公司组织架构

const org = {
  id: 1,
  name: 'CEO',
  title: '首席执行官',
  children: [
    {
      id: 2,
      name: 'CTO',
      title: '技术总监',
      children: [
        { id: 4, name: '张三', title: '研发经理', children: [] }
      ]
    }
  ]
};

// 操作:查找某个员工的上级链路
function findManagerChain(org, employeeId, chain = []) {
  if (org.id === employeeId) {
    return [...chain, org.name];
  }
  
  if (org.children) {
    for (const child of org.children) {
      const found = findManagerChain(
        child, 
        employeeId, 
        [...chain, org.name]
      );
      if (found) return found;
    }
  }
  
  return null;
}

console.log(findManagerChain(org, 4)); 
// ['CEO', 'CTO', '张三']

// 操作:扁平化组织架构(用于导出 Excel)
function flattenOrg(org, level = 0, result = []) {
  result.push({
    id: org.id,
    name: org.name,
    title: org.title,
    level: level
  });
  
  if (org.children) {
    org.children.forEach(child => {
      flattenOrg(child, level + 1, result);
    });
  }
  
  return result;
}

场景 3:菜单权限过滤

javascript 复制代码
// 环境: JavaScript
// 场景: 根据用户权限过滤菜单树

const allMenus = [
  {
    id: 1,
    name: '用户管理',
    permission: 'user',
    children: [
      { id: 2, name: '用户列表', permission: 'user.list' },
      { id: 3, name: '添加用户', permission: 'user.add' }
    ]
  },
  {
    id: 5,
    name: '系统设置',
    permission: 'system',
    children: [
      { id: 6, name: '角色管理', permission: 'system.role' }
    ]
  }
];

// 递归过滤菜单
function filterMenus(menus, permissions) {
  return menus
    // 1. 过滤当前层
    .filter(menu => permissions.includes(menu.permission))
    // 2. 递归过滤子菜单
    .map(menu => ({
      ...menu,
      children: menu.children 
        ? filterMenus(menu.children, permissions)
        : undefined
    }))
    // 3. 移除没有子菜单的父菜单
    .filter(menu => 
      !menu.children || menu.children.length > 0
    );
}

// 使用
const userPermissions = ['user', 'user.list', 'user.add'];
const visibleMenus = filterMenus(allMenus, userPermissions);

什么时候不该用递归?

在实际开发中,我也踩过一些坑:

javascript 复制代码
// 环境: JavaScript
// 场景: 不适合递归的情况

// ❌ 错误示例:深度很深的数据
function createDeepNesting() {
  let comment = { id: 1, replies: [] };
  let current = comment;
  
  // 创建 10000 层嵌套
  for (let i = 2; i <= 10000; i++) {
    current.replies[0] = { id: i, replies: [] };
    current = current.replies[0];
  }
  
  return comment;
}

const deepComment = createDeepNesting();
// countComments([deepComment]); // 栈溢出!

// ✅ 正确做法:转为迭代
function countCommentsIterative(comments) {
  const stack = [...comments];
  let count = 0;
  
  while (stack.length > 0) {
    const comment = stack.pop();
    count++;
    
    if (comment.replies) {
      stack.push(...comment.replies);
    }
  }
  
  return count;
}

console.log(countCommentsIterative([deepComment])); // 正常运行

判断标准

  • 数据深度 < 100 层 → 安全使用递归
  • 数据深度 100-1000 层 → 谨慎使用,考虑加深度限制
  • 数据深度 > 1000 层 → 必须转为迭代

四、递归思想:从"展开脑补"到"相信递归"

在掌握了递归的识别、模式、实战之后,我发现最大的障碍其实是思维方式

初学者的误区:"展开脑补"

这是我之前的思考方式:

javascript 复制代码
// 计算 factorial(3)
function factorial(n) {
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

// 我的思路(错误):
// factorial(3) 调用 factorial(2)
// factorial(2) 调用 factorial(1)
// factorial(1) 调用 factorial(0)
// factorial(0) 返回 1
// factorial(1) = 1 * 1 = 1
// factorial(2) = 2 * 1 = 2
// factorial(3) = 3 * 2 = 6

// 问题:
// 1. 脑子里要记住整个调用栈
// 2. 稍微复杂一点就晕了
// 3. 根本不理解递归的本质

正确的思维:"相信递归"

后来我意识到,应该这样思考:

javascript 复制代码
// 正确的思路:
// factorial(3) = 3 * factorial(2)
// 
// 我不需要知道 factorial(2) 内部怎么实现
// 我只需要"相信"它会返回正确结果(2)
// 所以 factorial(3) = 3 * 2 = 6
//
// 这就是"信任"的力量

关键洞察

写递归时,不要试图"展开"整个过程,只需要:

  1. 定义最简单的情况(终止条件)
  2. 假设递归调用会正确处理子问题
  3. 用子问题的结果构造当前问题的解

这种"信任"让复杂问题变简单

递归与数学归纳法

我发现递归和高中学的数学归纳法本质上是一回事:

数学归纳法

证明命题 P(n) 对所有 n 成立:

  1. 基础:证明 P(1) 成立
  2. 归纳:假设 P(k) 成立,证明 P(k+1) 也成立

递归

解决问题 Problem(n):

  1. 终止条件:处理最简单的情况
  2. 递归调用:假设 Problem(n-1) 已解决,构造 Problem(n) 的解

例子

javascript 复制代码
// 环境: JavaScript
// 场景: 数组求和

// 数学归纳法思路:
// 基础:空数组的和是 0
// 归纳:假设 sum(arr[1:]) 正确
//      则 sum(arr) = arr[0] + sum(arr[1:])

// 递归实现:
function sum(arr) {
  // 基础情况
  if (arr.length === 0) return 0;
  
  // 归纳步骤:相信 sum(arr.slice(1)) 会返回正确结果
  return arr[0] + sum(arr.slice(1));
}

console.log(sum([1, 2, 3, 4])); // 10

递归的局限与权衡

虽然递归很优雅,但也有代价:

javascript 复制代码
// 环境: JavaScript
// 场景: 斐波那契数列

// 朴素递归(慢)
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

// 问题:大量重复计算
// fib(5) = fib(4) + fib(3)
//        = (fib(3) + fib(2)) + fib(3)
// fib(3) 被计算了两次!

console.time('fib(40)');
fib(40); // 几秒钟才能算完
console.timeEnd('fib(40)');

// 优化:记忆化
function fibMemo(n, memo = {}) {
  if (n <= 1) return n;
  if (memo[n]) return memo[n];
  
  memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
  return memo[n];
}

console.time('fibMemo(40)');
fibMemo(40); // 瞬间完成
console.timeEnd('fibMemo(40)');

性能权衡

维度 递归 迭代
可读性 ⭐⭐⭐⭐⭐ ⭐⭐⭐
性能 ⭐⭐⭐ ⭐⭐⭐⭐⭐
内存 ⭐⭐ ⭐⭐⭐⭐⭐
适用场景 嵌套结构 所有场景

何时选择

适合递归:

✅ 问题天然递归(树、图、嵌套数据)

✅ 代码清晰度 > 性能

✅ 数据规模不大

✅ 可以用记忆化优化

不适合递归:

❌ 简单循环能解决

❌ 深度很深(栈溢出风险)

❌ 性能关键路径

❌ 大量重复计算且无法记忆化

延伸思考

在整理这些内容的过程中,我产生了一些新的疑问:

1. 尾递归优化在 JavaScript 中的支持?

javascript 复制代码
// 环境: JavaScript (严格模式)
// 场景: 尾递归优化

'use strict';

// 普通递归
function sum(n) {
  if (n === 0) return 0;
  return n + sum(n - 1); // 不是尾递归
}

// 尾递归
function sumTail(n, acc = 0) {
  if (n === 0) return acc;
  return sumTail(n - 1, acc + n); // 最后一步是递归调用
}

// 问题:
// ES6 规范要求支持尾递归优化
// 但实际上只有 Safari 支持
// Chrome、Firefox 都不支持

// 所以在生产环境中,尾递归优化靠不住

我的理解是,虽然尾递归优化很优雅,但在 JavaScript 中实用价值有限。如果真的需要处理深度很深的递归,还是转为迭代更靠谱。

2. 递归与函数式编程

递归是函数式编程的基石。在 JavaScript 中如何平衡递归的优雅性和实用性?这是一个值得深入思考的问题。

小结

通过这次系统梳理,我对递归有了全新的认识:

核心要点

  1. 识别递归问题:三大特征、识别清单、四大分类
  2. 掌握递归模式:三个核心模板、实用函数库、优化技巧
  3. 真实场景应用:评论系统、组织架构、菜单权限等
  4. 理解递归思想:"信任"比"展开"更重要

递归不只是一种编程技巧,更是一种思维方式。从"展开脑补"到"信任递归",这个转变需要时间和练习。

如果这篇文章对你有帮助,欢迎交流讨论。如果你有不同的理解或补充,也请不吝赐教。

参考资料

相关推荐
左夕2 小时前
最基础的类型检测工具——typeof, instanceof
前端·javascript
用户5757303346244 小时前
🐱 从“猫厂”倒闭到“鸭子”横行:一篇让你笑出腹肌的 JS 面向对象指南
javascript
Moment4 小时前
腾讯终于对个人开放了,5 分钟在 QQ 里养一只「真能干活」的 AI 😍😍😍
前端·后端·github
码路飞5 小时前
GPT-5.4 Computer Use 实战:3 步让 AI 操控浏览器帮你干活 🖥️
java·javascript
比尔盖茨的大脑5 小时前
AI Agent 架构设计:从 ReAct 到 Multi-Agent 系统
前端·人工智能·全栈
天才熊猫君5 小时前
使用 Vite Mode 实现客户端与管理端的物理隔离
前端
HelloReader5 小时前
React Hook 到底是干嘛的?
前端
用户60572374873085 小时前
OpenSpec 实战:从需求到代码的完整工作流
前端·后端·程序员