必须先按parent_id归类建立映射表,再从顶级(parent_id === 0 或 null)递归查找子节点;函数应返回新数组而非传引用,严格判空并限制递归深度防爆栈。

递归函数怎么写才能正确拼出树形结构
直接用 foreach 遍历原始扁平数组是拼不出层级的。关键在于:必须先按 parent_id 归类,再从顶级(parent_id === 0 或 null)开始逐层向下找子节点。否则会漏数据、重复嵌套、甚至无限递归。
常见错误是把“查数据库”和“建树”混在一起写——每次递归都查一次 DB,性能崩盘;或者没缓存子节点列表,导致同一级反复遍历全量数组。
- 先用一次循环建立
$map[pid] = [item1, item2, ...]的映射表 - 递归函数只接收当前节点和该映射表,不查库、不搜数组
- 递归出口是:当前节点没有子节点(即
!isset($map[$id]) || empty($map[$id]))
为什么传引用或返回数组结果差别很大
用引用(&$tree)累加结果看似省事,但容易在多层调用中污染变量、难以调试;更推荐函数返回新数组。PHP 7.4+ 对数组拷贝优化得不错,性能差异可忽略,但代码清晰度和可测性高得多。
示例中若用引用方式,常出现子节点被塞进父节点两次——因为上层调用未清空临时容器,或递归后忘记 unset 已处理项。
立即学习“PHP免费学习笔记(深入)”;
- 返回值方式:
return array_merge([$node], $children) - 引用方式需确保每次调用前
$children = [],且仅在确认有子节点时才array_push($tree, ...) - 如果后续要 JSON 输出,返回值方式天然支持
json_encode();引用方式若误传了资源或对象,会直接报错
如何避免 MySQL 里 parent_id 为 NULL 和 0 混用导致的断层
很多老项目里,顶级分类的 parent_id 有时存 NULL,有时存 0,而 PHP 中 0 == null 为 true,一用 empty($row['parent_id']) 就把 id=1, parent_id=0 的二级分类也当成顶级了。
- 判断顶级必须严格:
$row['parent_id'] === 0 || $row['parent_id'] === null - 归类时统一转成字符串键:
$map[(string)$row['parent_id']][] = $row,避免0和null被当同一个 key - 导出前加一行校验:
var_dump(array_keys($map));看看有没有意外的 key(比如''或'null')
导出大分类时内存爆掉怎么办
1000+ 条数据递归嵌套,每个节点都复制一份数组,很容易触发 Fatal error: Allowed memory size exhausted。这不是递归本身的问题,而是树太深或节点数据太重(比如带冗余字段、大文本)。
- 构造树时只保留必要字段:
['id', 'name', 'parent_id', 'level'],去掉content、created_at等非展示字段 - 用
gc_disable()+gc_enable()控制垃圾回收时机(慎用,仅限已确认瓶颈在此) - 真正超大分类(如电商类目 >5000 节点),放弃一次性递归,改用栈模拟(
while + array_pop),避免 PHP 默认 100 层递归限制
最常被忽略的是:没在递归函数里加深度保护。加一行 if ($depth > 20) { throw new Exception("Category depth too high"); },比等 OOM 强得多。



















