Svelte拖拽网格与添加元素时过度重计算的优化问询
解决Svelte中不必要的重计算问题
我正在实现一款应用,用户可点击2D网格放置元素,也能拖拽网格移动。功能原型代码如下:
<script> let project = { offsetX: 0, offsetY: 0, circles: [{ id: 1, x: 50, y: 50, }] } let dragInfo = null function onBackgroundDragStart(event){ dragInfo = { action: "moveBackground", dragStartProjectOffsetX: project.offsetX, dragStartProjectOffsetY: project.offsetY, startMouseX: event.clientX, startMouseY: event.clientY } const dragImage = document.createElement("img") dragImage.style.display = "none" event.dataTransfer.setDragImage(dragImage, 0, 0) } function onBackgroundDragOver(event){ switch(dragInfo.action){ case "moveBackground": const dragDistanceX = event.clientX - dragInfo.startMouseX const dragDistanceY = event.clientY - dragInfo.startMouseY project.offsetX = dragInfo.dragStartProjectOffsetX + dragDistanceX project.offsetY = dragInfo.dragStartProjectOffsetY + dragDistanceY break } } // Create new circle on click. function onClick(event){ project.circles.push({ id: project.circles.length + 1, x: event.clientX - project.offsetX, y: event.clientY - project.offsetY, }) project.circles = project.circles } function getText(circle){ console.log("getText() is called.") return circle.x.toString()[0] } </script> <!-- svelte-ignore a11y-click-events-have-key-events --> <div class="app" draggable="true" on:dragstart={onBackgroundDragStart} on:dragover|preventDefault={onBackgroundDragOver} on:click={onClick} style:background-position-x={`${project.offsetX}px`} style:background-position-y={`${project.offsetY}px`} > {#each project.circles as circle (circle.id)} <div class="circle" style:transform={`translate(${project.offsetX + circle.x}px, ${project.offsetY + circle.y}px) translate(-50%, -50%)`} > {getText(circle)} </div> {/each} </div> <style> .app{ position: relative; width: 100vw; height: 100vw; background-color: silver; background-image: linear-gradient(rgba(0, 0, 0, .1) .1em, transparent .1em), linear-gradient(90deg, rgba(0, 0, 0, .1) .1em, transparent .1em); background-size: 2em 2em; overflow: hidden; } .circle{ position: absolute; width: 50px; height: 50px; background-color: red; border-radius: 50%; text-align: center; } </style>
当前原型功能正常,但存在两个过度重计算问题:
- 拖拽背景时,仅更新
project.offsetX和project.offsetY,但{#each}循环中每个circle的getText(circle)都会被重新调用,而该函数仅依赖circle.x,与offset参数无关; - 添加新circle时,所有已有circle的
getText(circle)也会被重新调用。
实际场景中getText()是计算成本极高的函数,过度调用会导致应用卡顿。由于需要将项目信息整体保存为JSON,不想拆分project对象,请问如何让Svelte避免不必要的更新?
解决方案
1. 组件隔离状态依赖(最推荐)
将单个circle的渲染逻辑抽离成独立组件,组件内部只绑定自身依赖的状态,这样只有当circle本身变化时才会触发内部的getText计算,offset变化或新增其他circle时不会影响已有组件。
创建Circle.svelte组件:
<script> export let circle; export let offsetX; export let offsetY; function getText(circle){ console.log("getText() is called.") return circle.x.toString()[0] } </script> <div class="circle" style:transform={`translate(${offsetX + circle.x}px, ${offsetY + circle.y}px) translate(-50%, -50%)`} > {getText(circle)} </div> <style> .circle{ position: absolute; width: 50px; height: 50px; background-color: red; border-radius: 50%; text-align: center; } </style>
主组件中替换{#each}部分:
{#each project.circles as circle (circle.id)} <Circle {circle} offsetX={project.offsetX} offsetY={project.offsetY} /> {/each}
效果:
- 拖拽背景时,offset变化只会触发每个
Circle组件的样式更新,不会调用getText; - 新增circle时,只有新创建的
Circle组件会调用getText,已有组件不会重复计算。
2. 预计算并缓存结果
如果不想拆组件,可以在添加circle时就预计算好文本内容,存储在circle对象中,后续直接使用缓存值,彻底避免重复计算。
修改onClick函数:
function onClick(event){ const newX = event.clientX - project.offsetX const newCircle = { id: project.circles.length + 1, x: newX, y: event.clientY - project.offsetY, text: getText({x: newX}) // 预计算并缓存 } project.circles.push(newCircle) project.circles = project.circles }
模板中直接使用缓存值:
{#each project.circles as circle (circle.id)} <div class="circle" style:transform={`translate(${project.offsetX + circle.x}px, ${project.offsetY + circle.y}px) translate(-50%, -50%)`} > {circle.text} </div> {/each}
注意:如果后续circle的x值可能修改,需要在修改时同步更新text字段。
3. 结合反应式语句与缓存函数
如果getText需要根据circle的动态变化重新计算,但又要避免不必要的调用,可以用缓存对象结合Svelte的反应式语句,仅在circle属性变化时更新计算结果。
主组件中添加缓存逻辑:
const textCache = new Map() function getText(circle){ if(textCache.has(circle.id)){ return textCache.get(circle.id) } // 实际场景中的高成本计算 const result = circle.x.toString()[0] textCache.set(circle.id, result) return result } // 监听circle的x变化,仅当x改变时更新缓存 $: project.circles.forEach(circle => { const currentResult = circle.x.toString()[0] if(textCache.get(circle.id) !== currentResult){ textCache.set(circle.id, currentResult) } })
模板中仍使用{getText(circle)},此时只有当circle的x变化时才会重新计算,offset变化或新增circle不会触发已有circle的getText调用(新增circle会调用一次)。
内容的提问来源于stack exchange,提问作者Peppe L-G
相关产品推荐
相关产品推荐

