React.StrictMode下如何重启D3力模拟解决拖拽失效问题
问题根因
React 18 严格模式在开发环境会执行组件双挂载流程:第一次挂载后立即触发卸载,再执行第二次挂载。当前useEffect没有编写清理逻辑,第一次执行时创建的D3力导向模拟实例、绑定的拖拽事件、追加到SVG内的图形元素都没有被销毁,第二次effect执行时会创建新的模拟实例、重复追加DOM节点,最终拖拽事件实际绑定在被覆盖的旧节点上,触发时操作的是已废弃的旧模拟实例,导致拖拽功能失效。
另外代码里存在几处笔误,也可能引发运行异常:
- 碰撞力名称拼写错误:
colide应为collide - 连线长度常量名拼写错误:
LINK_LENGHT应为LINK_LENGTH - 节点fill属性行多写了一个闭合括号,会触发语法错误
修复方案
在useEffect中返回清理函数,在effect重执行、组件卸载前完成旧实例和DOM的销毁,修正后完整代码如下:
useEffect(function () { if (!svgRef.current) return; const svg = d3.select(svgRef.current); // 清空SVG内残留的旧元素,避免重复渲染 svg.selectAll("*").remove(); const nodes = d3.map( props.nodes, (node): Node => ({ ...node, }) ); const links = d3 .map(props.links, (link) => ({ source: nodes.find((node) => node.id === link.source), target: nodes.find((node) => node.id === link.target), })) .filter( (link): link is { source: Node; target: Node } => link.source !== undefined && link.target !== undefined ); const simulation = d3 .forceSimulation(nodes) .force( "link", d3 .forceLink(links) .id((_, i) => nodes[i].id) .distance(LINK_LENGTH) ) .force("charge", d3.forceManyBody().strength(-NODE_REPULSION)) .force("center", d3.forceCenter()) .force("forceX", d3.forceX().strength(NODE_GRAVITY)) .force("forceY", d3.forceY().strength(NODE_GRAVITY)) .force("collide", d3.forceCollide(NODE_RADIUS * NODE_MARGIN)) .on("tick", ticked); const link = svg .selectAll("line") .data(links) .enter() .append("line") .style("stroke", "#aaa"); const node = svg .selectAll("circle") .data(nodes) .enter() .append("circle") .call( d3 .drag<SVGCircleElement, Node>() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended) ) .attr("fill", (node) => node.color) .attr("r", NODE_RADIUS) .attr("stroke", "#000000") .attr("stroke-width", 1); function ticked() { link .attr("x1", (d) => d.source.x!) .attr("y1", (d) => d.source.y!) .attr("x2", (d) => d.target.x!) .attr("y2", (d) => d.target.y!); node.attr("cx", (d) => d.x!).attr("cy", (d) => d.y!); } function dragstarted(event: any, d: Node) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event: any, d: Node) { d.fx = event.x; d.fy = event.y; } function dragended(event: any, d: Node) { if (!event.active) simulation.alphaTarget(0.0001); d.fx = null; d.fy = null; } // 清理逻辑 return () => { // 停止力模拟,释放实例 simulation.stop(); // 清除所有渲染的图形和绑定的事件 svg.selectAll("*").remove(); }; }, [props.nodes.length, props.links.length] )
后续排查思路
如果修改后仍存在问题,按以下顺序校验:
- 检查effect依赖项:当前仅监听节点、连线数组长度,如果业务中存在数组长度不变但内容更新的场景,依赖项不会触发effect重跑,会导致数据与渲染不一致,这类场景可以直接将
props.nodes、props.links作为依赖项,或使用稳定的序列化值作为依赖 - 校验事件触发:在
dragged回调中打印日志,如果拖拽时没有日志输出,说明事件绑定被其他逻辑覆盖;如果有日志输出但节点不移动,说明当前操作的是错误的模拟实例 - 检查ref占用:确认
svgRef没有被其他组件/逻辑同时操作,避免DOM被意外修改
内容的提问来源于stack exchange,提问作者Jerico
相关产品推荐
相关产品推荐

