Svelte结合jQuery DataTable响应式更新报parentNode null错误
问题背景
本人正逐步将jQuery项目迁移至Svelte,考虑到DataTables等组件在jQuery生态中成熟度更高,参考官方Svelte html示例,测试通过Promise调用服务端接口更新对象数组、重绘表格的Svelte响应式特性,期望实现更贴近Svelte原生的开发方式。
目前表格可正常初始渲染,但将jQuery DataTable的搜索功能与Svelte响应式声明变量绑定后,仅能完成一次表格更新,随后抛出错误:Uncaught (in promise) TypeError: e.parentNode is null。
尝试通过自定义Svelte可写store变量onReady判断Promise执行状态、拦截重复请求后,问题依然存在。
相关代码
Load.svelte(axios远程数据拉取模块)
<script context="module"> import {env} from './EnvUtils.js'; import axios from "axios"; const wait = delay => new Promise(resolve => setTimeout(resolve, delay)); let perPage = 10; console.log(env); const BASE_URL = env.host + 'blogged-posts'; const config = { headers: { "X-Requested-With": "XMLHttpRequest", "Accept": "application/json", 'Content-Type': 'application/json', "X-CSRF-Token": document.querySelector('meta[name="csrf-token"][content]').content } } export async function fetchData(page, search = undefined) { await wait(500); console.log(page, search); try { const getUrl = (search!==undefined) ? `${BASE_URL}?page=${page}&per_page=${perPage}&search=${search}&delay=1` : `${BASE_URL}?page=${page}&per_page=${perPage}&delay=1`; const response = await axios.get(getUrl, config); const { data } = await response; console.log("data:", data); return data && data['data']!==null && data['data']!==undefined ? data.data : []; } catch (error) { console.error("Error: ", error); } } </script>
Blog.svelte(表格组件)
<script> import { onMount, tick } from "svelte"; import {env} from './utils/EnvUtils.js'; import {fetchData} from "./utils/Load.svelte"; import jQuery from "jquery/dist/jquery"; import initDt from 'datatables.net-dt'; console.log(env, jQuery, initDt); let search; let currentPage = 1; let el // table element let table // table object (API) $: dataPromise = fetchData(currentPage, search); onMount(() => { dataPromise.then(tick).then(() => { table = jQuery(el).DataTable(); // search input function table.on('search.dt', function() { var input = jQuery('.dataTables_filter input')[0]; dataPromise = fetchData(table.page.info().page, input.value); dataPromise.then(tick).then(() => { // 输入搜索字符超过1个时,会在该Promise内抛出错误 console.log("redraw"); }); }); }) }); </script> <table bind:this={el} class="display" style="width:100%"> <thead> <tr> <th>Title</th> <th>Updated</th> </tr> </thead> <tbody> {#await dataPromise} <tr><td>...fetching</td></tr> {:then rows} {#each rows as row} <tr> <td>{row.title}</td> <td>{row.updated_at}</td> </tr> {/each} {/await} </tbody> <tfoot> <tr> <th>Title</th> <th>Updated</th> </tr> </tfoot> </table>
无效的请求拦截尝试代码
// Store.svelte <script context="module"> import { writable } from 'svelte/store' export const onReady = writable(true) </script> // Load.svelte 修改部分 export async function fetchData(page, search = undefined) { onReady = false; // ... 数据拉取逻辑 // return语句前 onReady = true; } // Blog.svelte 修改部分 // 输入事件中调用Promise前先检查onReady状态 if (!onReady) return false; dataPromise = fetchData(table.page.info().page, input.value);
错误原因
- 核心问题是DOM控制权冲突:jQuery DataTable初始化完成后,会完全接管表格内部DOM的增删、重绘逻辑,内部缓存了所有表格行节点的引用;而Svelte的响应式更新同样会直接操作
tbody内部的DOM——比如请求加载时插入...fetching提示行、数据返回后替换所有行节点,这个过程会删掉DataTable已经持有引用的旧DOM节点,后续DataTable操作找不到对应节点的父元素,就会抛出e.parentNode is null错误。 - 自定义的
onReady拦截逻辑本身无效:Svelte writable store不能直接通过=赋值修改状态,必须调用.set()方法才能触发更新,原本的判断逻辑永远拿不到正确的加载状态,根本无法拦截重复请求。 - 缺少竞态处理:搜索输入连续触发时,先发起的旧请求如果晚于新请求返回,会覆盖最新的表格数据,同时触发不必要的DOM重绘,加剧DOM冲突问题。
- 事件触发逻辑存在循环风险:DataTable的
search.dt事件会在表格内部搜索状态变化时触发,在回调中修改数据触发Svelte DOM更新,会间接再次触发DataTable的状态检测,形成重复更新循环。
修复方案
优先选择DOM控制权分离的方案,性能最好、逻辑最稳定,修改步骤如下:
- 移除Svelte对
tbody内部内容的直接渲染,tbody标签留空,完全交给DataTable接管,从根源避免DOM操作冲突。 - 所有数据更新通过DataTable原生API完成,不要依赖Svelte的
{#each}渲染行:拿到接口返回数据后,先调用table.clear()清空旧数据,再调用table.rows.add(rows)插入新数据,最后调用table.draw()重绘表格。 - 给搜索输入事件加300ms防抖,避免输入过程中频繁发起接口请求;同时修正store的赋值方式,使用
onReady.set()修改状态,做基础的请求锁防止重复提交。 - 组件销毁时调用
table.destroy()清除DataTable实例和绑定的事件,避免内存泄漏。
核心修改后的Blog.svelte逻辑参考:
<script> import { onMount, onDestroy } from "svelte"; import {env} from './utils/EnvUtils.js'; import {fetchData} from "./utils/Load.svelte"; import {onReady} from "./utils/Store.svelte"; import jQuery from "jquery/dist/jquery"; import initDt from 'datatables.net-dt'; let search; let currentPage = 1; let el; let table; let searchTimer = null; const updateTable = async (page, searchVal) => { if (!$onReady) return; onReady.set(false); try { const rows = await fetchData(page, searchVal); table.clear(); table.rows.add(rows); table.draw(false); // false表示不重置分页位置 } finally { onReady.set(true); } } onMount(() => { table = jQuery(el).DataTable({ // 关闭DataTable自带的前端搜索、排序、分页,全部用接口实现 searching: true, serverSide: false, // 如果是全服务端分页改成true paging: false }); // 首次加载数据 updateTable(currentPage, search); // 绑定搜索事件,加防抖 jQuery('.dataTables_filter input').on('input', function() { clearTimeout(searchTimer); const val = this.value; searchTimer = setTimeout(() => { updateTable(currentPage, val); }, 300); }) }); onDestroy(() => { if (table) table.destroy(true); clearTimeout(searchTimer); }) </script> <table bind:this={el} class="display" style="width:100%"> <thead> <tr> <th>Title</th> <th>Updated</th> </tr> </thead> <tbody></tbody> <tfoot> <tr> <th>Title</th> <th>Updated</th> </tr> </tfoot> </table>
如果一定要保留Svelte渲染行的逻辑,可以在每次dataPromise更新前先调用table.destroy(true)销毁旧的DataTable实例,等Svelte完成DOM更新(await tick())后再重新初始化DataTable,这种方式实现简单但重复初始化有性能开销,不适合数据量大的表格场景。
内容的提问来源于stack exchange,提问作者Hmerman6006
相关产品推荐
相关产品推荐

