Svelte中bind:innerHTML为何始终保留过时字符串?
内容显示正常,但bind:innerHTML未跟踪变化
一个包含两个{#each}块的div,通过bind:innerHTML绑定到变量svgDivContentString。这些{#each}块可响应用户输入,根据变量变化动态渲染,组件本身能正确显示变化。
但变量svgDivContentString并未随组件内容变化而更新,仅保留组件首次挂载时的初始信息。无论内容如何变化,该变量都保持不变;即便通过svgDivContentString = ""手动清空,当{#each}块再次触发时,它仍会重置为默认信息(与实际显示内容无关)。此字符串需转换为base64,用作img.src。
<div bind:innerHTML={svgDivContentString} contenteditable id="kbContainer" > <svg id="svgContainer" viewBox="0 0 {$width} {height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > {#each allNotes as note, index (index)} ... {/each} </div>
已尝试的解决方案/变通方法
我曾尝试手动清空该字符串(如上所示)并确保仅引入新信息,但该字符串始终恢复为相同的默认内容。我还尝试用非Svelte风格的方法,通过document.getElementById获取div,但返回undefined。这种方法也不符合Svelte的最佳实践,在相关技术问答中常被提及。
需再次强调:问题并非出在{#each}块上,每次渲染的内容都能正确更新,问题在于bind:innerHTML未同步这些变化。
解决方案
核心原因
bind:innerHTML的双向绑定仅针对用户手动编辑contenteditable区域的内容,组件自身通过框架渲染的DOM变化(比如{#each}更新)不会反向同步到绑定的变量——Svelte设计上认为框架控制的DOM应该由数据驱动,而非从DOM反向同步数据。
方案1:直接从数据源生成HTML字符串(推荐)
既然组件内容完全由allNotes、$width、height等数据驱动,直接基于这些数据构建svgDivContentString,确保数据与字符串完全同步:
<script> let allNotes = []; let height = 0; // 假设$width是从store获取的 import { width } from './stores.js'; // 当依赖数据变化时,自动生成HTML字符串 $: svgDivContentString = ` <div id="kbContainer" contenteditable> <svg id="svgContainer" viewBox="0 0 ${$width} ${height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > ${allNotes.map((note, index) => ` <!-- 这里复制组件中{#each}块内的SVG元素代码,示例: --> <rect x="${note.x}" y="${note.y}" width="${note.width}" height="${note.height}" fill="${note.color}" /> `).join('')} </svg> </div> `; </script> <!-- 组件渲染部分直接使用数据驱动,无需bind:innerHTML --> <div contenteditable id="kbContainer"> <svg id="svgContainer" viewBox="0 0 {$width} {height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > {#each allNotes as note, index (index)} <!-- 和模板字符串中一致的SVG元素 --> <rect x={note.x} y={note.y} width={note.width} height={note.height} fill={note.color} /> {/each} </svg> </div>
这种方式完全遵循Svelte的数据驱动理念,svgDivContentString会随数据源实时更新,且无需依赖DOM操作。
方案2:组件更新后获取DOM内容(适合复杂场景)
如果HTML结构过于复杂,无法直接通过模板字符串生成,可以通过bind:this获取DOM引用,在组件更新完成后同步innerHTML:
<script> import { tick } from 'svelte'; let kbContainer; let svgDivContentString = ''; let allNotes = []; let height = 0; import { width } from './stores.js'; // 当依赖数据变化时,等待DOM更新后获取innerHTML $: async () => { await tick(); // 等待Svelte完成DOM更新 if (kbContainer) { svgDivContentString = kbContainer.innerHTML; } }; </script> <div bind:this={kbContainer} contenteditable id="kbContainer" > <svg id="svgContainer" viewBox="0 0 {$width} {height}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > {#each allNotes as note, index (index)} ... {/each} </svg> </div>
tick()函数会等待当前所有待处理的DOM更新完成,确保获取到的是最新的innerHTML。
为什么之前的方法无效
bind:innerHTML不同步框架渲染的DOM变化,仅响应用户手动编辑;document.getElementById返回undefined是因为组件挂载前或异步渲染阶段调用,而bind:this是Svelte官方推荐的安全获取DOM元素的方式。
内容的提问来源于stack exchange,提问作者drewbob01

