递归函数中jQuery.text()执行后div文本未实时更新问题
编写递归函数读取层级树形结构数据时,使用jQuery操作DOM更新当前处理节点名称,出现DOM更新不实时的问题。
- 测试数据结构如下:
const data = { name: "Quotes", children: [ { name: "Quotes/Quote - Quote 1 - 1", children: [], // 其余字段 }, { name: "Quotes/Quote", children: [/* 子节点列表 */], // 其余字段 }, { name: "Body", children: [/* 子节点列表 */], // 其余字段 }, // 其余同级节点 ], // 其余顶层字段 }
- 原有实现代码:
<!-- index.html --> <div id="label">Start parse data ...</div>
// index.js function parseData(data) { $('#label').text(data.name); console.log(data.name); // 控制台日志跟踪处理进度 // 其余数据处理逻辑 if (data.children.length) { for (const childrenData of data.children) { parseData(childrenData); } } } // 接口请求获取树形数据 const data = $.ajax(/* 请求配置 */) parseData(data); $('#label').text("==== Done ====");
- 实际运行现象:
- id为
label的div初始显示Start parse data ...,数秒后直接跳转为==== Done ==== - 递归过程中不会实时更新显示当前处理节点的
name值 - 控制台
console.log可以正常按递归顺序打印节点名,遍历逻辑无异常 - 尝试将DOM更新改为
setTimeout(() => $('#label').text(data.name), 0)后问题依然存在
- id为
- 预期效果:递归执行过程中,div文本随当前处理节点实时更新。
浏览器JS运行在单线程上,且UI渲染和脚本执行互斥:所有同步代码会按顺序压入执行栈运行,执行栈清空之前浏览器不会触发任何页面重绘,中间对DOM做的所有修改只会暂存在内存中,等同步任务全部执行完成后才会统一渲染到页面上。
你的递归遍历是纯同步阻塞逻辑,从首次调用parseData到最后执行$('#label').text("==== Done ====")全部在同一个同步执行栈中,浏览器全程没有机会渲染中间的DOM修改,等所有同步代码跑完,DOM最后一次修改是设置为==== Done ====,所以你只能看到最终结果。
之前单独给DOM更新加setTimeout 0无效的原因是:你只把DOM修改操作丢到了宏任务队列,但递归本身还是同步瞬间执行完成,所有setTimeout回调会排在最后一行同步设置==== Done ====的逻辑之后才执行,本质还是没给浏览器留出渲染间隙。
额外问题:你代码里const data = $.ajax(.....)的写法是错误的,$.ajax返回的是jqXHR对象,不是接口返回的业务数据,必须在请求的回调函数中拿到响应数据再调用解析逻辑。
方案1:异步递归+setTimeout让出线程(最易实现,效果符合预期)
把递归逻辑改为异步,每处理完一个节点就通过setTimeout让出主线程,给浏览器留出渲染时间,再处理下一个节点:
async function parseData(data) { // 更新当前节点DOM $('#label').text(data.name); console.log(data.name); // 其余数据处理逻辑 // ...... // 让出主线程,等待浏览器完成页面渲染 await new Promise(resolve => setTimeout(resolve, 0)); // 异步递归处理子节点 if (data.children?.length) { for (const childData of data.children) { await parseData(childData); } } } // 正确写法:在ajax回调中拿到数据再执行解析 $.ajax({ // 你的接口请求配置,比如url、type等 success: async function(res) { await parseData(res); $('#label').text("==== Done ===="); } })
方案2:requestAnimationFrame按帧更新(适合节点量大的场景)
如果树形结构节点量很大,逐节点等待渲染会拖慢处理速度,可以用requestAnimationFrame对齐浏览器的渲染频率(约每16ms刷新一次),既保证UI实时更新,又不浪费性能:
async function parseData(data) { // 等待下一帧渲染时机 await new Promise(resolve => requestAnimationFrame(resolve)); $('#label').text(data.name); console.log(data.name); // 其余数据处理逻辑 // ...... if (data.children?.length) { for (const childData of data.children) { await parseData(childData); } } } // 接口调用逻辑同方案1,在success回调中执行parseData即可
内容的提问来源于stack exchange,提问作者Hai Nguyen

