js
const appendChildren = (
nodes: DataNode[],
targetKey: React.Key,
children: DataNode[],
): DataNode[] =>
nodes.map((node) => {
if (node.key === targetKey) {
return {
...node,
children,
};
}
if (node.children && node.children.length) {
return {
...node,
children: appendChildren(node.children, targetKey, children),
};
}
return node;
});
export const DepartmentTree = forwardRef<
DepartmentTreeRef,
DepartmentTreeProps
>(function DepartmentTree(props, ref) {
const treeDataRef = useRef<DataNode[]>([]);
treeDataRef.current = treeData;
// useEffect(() => {
// treeDataRef.current = treeData;
// }, [treeData]);
useEffect(() => {
let shouldIgnore = false;
const loadRootData = async () => {
try {
const rootRequest = newRequest({
cache: true,
...(searchParams ?? {}),
});
const rootRes: any = await rootRequest;
if (shouldIgnore) return;
const finalTreeData = normalizeNodes(rootRes?.data?.data || []);
setTreeData(finalTreeData);
} catch (error) {
console.warn( error);
}
};
loadRootData();
return () => {
shouldIgnore = true;
};
}, [searchParams, getAllRootDeptFullPaths]);
// 异步加载子节点
const onLoadData: TreeProps['loadData'] = ({ key, children }) =>
new Promise<void>((resolve) => {
if (!key || (children && children.length > 0)) {
resolve();
return;
}
const newSearchParams = _cloneDeep(searchParams ?? {});
_set(newSearchParams, 'data.code', key);
newRequest({ cache: true, ...newSearchParams })
.then((res: any) => {
const childrenNodes = normalizeNodes(res?.data?.data || []);
setTreeData((prev) => {
const newData = appendChildren(prev, key, childrenNodes);
// 关键
treeDataRef.current = newData;
return newData;
});
})
.catch((error) => {
console.warn( error);
})
.finally(() => resolve());
});
const onExpand = (nextExpandedKeys: React.Key[]) => {
setExpandedKeys(nextExpandedKeys);
setAutoExpandParent(false);
};
const findNodeByKey = (
nodes: DataNode[],
targetKey: React.Key,
): DataNode | null => {
for (const node of nodes) {
// 如果当前节点的ID就是我们要找的,直接返回这个节点
if (node.key === targetKey) return node;
// 如果当前节点有子节点,就递归查找子节点
if (node.children && node.children.length) {
const child = findNodeByKey(node.children as DataNode[], targetKey);
if (child) return child;
}
}
return null;
};
const expandPath = useCallback(
async (pathKeys: string[]) => {
// 遍历路径中的所有节点ID(除了最后一个,因为最后一个不需要展开)
for (const key of pathKeys.slice(0, -1)) {
const current = findNodeByKey(treeDataRef.current, key);
if (!current) continue;
// 根据pathKeys顺序,一层层加载子节点数据
if (!current.children || current.children.length === 0) {
await onLoadData({ key, children: current.children } as any);
}
}
setExpandedKeys(pathKeys.slice(0, -1));
setAutoExpandParent(true);
},
[onLoadData],
);
const handleSearchSelect = useCallback(
async (value: string, option: any) => {
const targetKey = option?.deptId ?? option?.value ?? value;
const path = String(option?.deptFullPath ?? '')
.split('/')
.filter(Boolean);
if (path.length > 0) {
await expandPath(path);
}
setSelectedKeys([targetKey]);
setSearchValue(value);
},
[onSelect, expandPath],
);
return (
<Tree
loadData={onLoadData}
treeData={displayTreeData}
blockNode
onSelect={handleTreeSelect}
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
selectedKeys={selectedKeys}
/>
);
});
这是一个非常经典且高阶的 "React 渲染周期 vs JavaScript 执行顺序" 的问题。
简单来说:JavaScript 的代码执行速度(微任务队列)快于 React 的重新渲染速度。
仅仅在组件主体(或 useEffect)中写 treeDataRef.current = treeData 是不够的,因为这两者之间存在一个致命的时间差。
下面拆解这个"时间差"到底发生在哪里:
1. 场景重现:如果没有手动赋值
假设 expandPath 正在循环处理 ['A', 'B'] 两个节点(先加载 A,再加载 B)。
第一步:加载节点 A
expandPath循环开始,找到节点 A。- 调用
await onLoadData(A)。 - 请求回来,数据有了。
- 执行
setTreeData(newData)。- 注意: 此时 React 仅仅是**"收到了更新通知"**,它把这次更新放入了队列,准备稍后重新渲染组件。
- 关键点: 组件还没有重新渲染!所以组件最上面的
treeDataRef.current = treeData这行代码还没有运行!Ref里存的还是旧树。
onLoadData的 Promise 结束(resolve)。
第二步:灾难发生(加载节点 B)
await结束,JS 引擎立刻继续执行下一行代码(for循环进入下一次迭代)。- 循环尝试处理节点 B。
- 执行
const current = findNodeByKey(treeDataRef.current, 'B')。 - 问题来了: 此时 React 还没有来得及 执行下一次渲染。
treeDataRef.current依然是旧的(里面还没有 A 的子节点)。 - 结果:找不到节点 B,逻辑中断,展开失败。
第三步:马后炮(React 终于渲染了)
- JS 的同步代码和微任务跑完后,React 终于开始 Re-render。
- 组件函数重新运行。
- 执行
treeDataRef.current = treeData(这时候 Ref 才变成新的)。 - 但此时
expandPath的循环早就跑完了,黄花菜都凉了。
2. 为什么手动赋值能解决?
当写成这样时:
javascript
setTreeData((prev) => {
const newData = appendChildren(prev, key, childrenNodes);
// 【关键操作】强行插队
// 不等 React 渲染,直接在 JS 层面立刻更新 Ref
treeDataRef.current = newData;
return newData;
});
流程变成了:
expandPath调用onLoadData。- 请求回来,执行
setTreeData的回调。 - 立刻 更新
treeDataRef.current = newData(这是同步的 JS 操作,不需要等 React 渲染)。 onLoadDataPromise 结束。expandPath继续循环。- 循环取
treeDataRef.current,此时它已经是最新的了(因为我们在第3步强行更新了它)。 - 成功找到下一个节点。
总结
treeDataRef.current = treeData(写在组件体内) :依赖于 React 的渲染周期。只有等 React 画完下一帧,Ref 才会变。expandPath中的await循环 :依赖于 JS 的 Promise 队列。它的速度极快,不会等待 React 的渲染。
结论: 因为 for 循环跑得太快,不等 React 渲染就要用新数据,所以必须在数据产生的源头(setTreeData 回调里)手动、同步地把最新数据塞进 Ref 里,才能追上循环的脚步。