仅在循环中调用时XMLHttpRequest返回null的问题排查
问题分析与解决方案
这是异步请求开发里很常见的执行顺序坑,我来帮你拆解问题并给出修复方案:
核心问题:异步代码的执行时机错配
你现在的代码里,table.appendRows(tableData)和doneCallback()是在发送请求后立刻执行的,但request.onload是异步回调——也就是说,请求还没拿到数据,这两行就已经跑完了,此时tableData还是空数组,自然会出现响应为null的情况。
另外,如果你后续要加入for循环,用var x还会触发变量作用域提升的问题:所有异步回调都会共享同一个x变量,最后所有请求都会用循环结束后的x值(也就是2022),完全达不到迭代的效果。
分步修复方案
1. 先搞定单个请求的异步逻辑
把table.appendRows和doneCallback()移到request.onload回调内部,确保只有当请求成功返回数据后,才把数据传给Tableau:
myConnector.getData = function (table, doneCallback) { var tableData = []; // 用var声明避免全局变量污染 var requestURL = "https://api.exchangerate.host/latest"; var request = new XMLHttpRequest(); request.open("GET", requestURL); request.responseType = "json"; request.send(); request.onload = function (resp) { var response = request.response; console.log(response); // 增加空响应判断,避免后续代码崩溃 if (!response) { console.error("请求返回空数据"); doneCallback(); return; } var feat = response.rates; // 用Object.entries同时获取键值对,比分开取keys/values更高效 var currencyEntries = Object.entries(feat); for (var [currencyAbr, currencyPrice] of currencyEntries) { tableData.push({ Currency: currencyAbr, Price: currencyPrice, Base: response.base, // Year: x, 后续循环时启用 }); } // 数据处理完成后再传给Tableau table.appendRows(tableData); doneCallback(); }; // 增加请求错误处理,避免请求失败时整个连接器卡住 request.onerror = function() { console.error("网络请求失败"); doneCallback(); }; };
2. 加入循环并解决闭包陷阱
当你要加入1999到2021的循环时,把var x改成let x(let有块级作用域,每次循环都会创建独立的x变量),同时用Promise封装请求,可以更优雅地处理多个异步请求,确保所有请求完成后再调用doneCallback():
// 先封装一个Promise版的请求函数,方便批量调用 function fetchExchangeRates(year) { return new Promise((resolve, reject) => { // 注意:api.exchangerate.host支持按日期查询,这里用每年1月1日的汇率 var requestURL = `https://api.exchangerate.host/latest?date=${year}-01-01`; var request = new XMLHttpRequest(); request.open("GET", requestURL); request.responseType = "json"; request.onload = () => { if (request.status >= 200 && request.status < 300) { resolve({...request.response, year}); // 把年份和响应数据一起返回 } else { reject(new Error(`请求失败,状态码:${request.status}`)); } }; request.onerror = () => reject(new Error("网络请求出错")); request.send(); }); } myConnector.getData = function (table, doneCallback) { var tableData = []; var requests = []; // 生成所有年份的请求Promise for (let x = 1999; x < 2022; x++) { requests.push(fetchExchangeRates(x)); } // 等待所有请求完成后统一处理数据 Promise.all(requests) .then(responses => { responses.forEach(resp => { var feat = resp.rates; var currencyEntries = Object.entries(feat); for (var [currencyAbr, currencyPrice] of currencyEntries) { tableData.push({ Currency: currencyAbr, Price: currencyPrice, Base: resp.base, Year: resp.year }); } }); table.appendRows(tableData); doneCallback(); }) .catch(err => { console.error("批量请求出错:", err); doneCallback(); // 即使出错也要调用doneCallback,避免Tableau卡住 }); };
额外提示
- 用
Object.entries()代替分别取keys和values,代码更简洁高效。 - 一定要加错误处理逻辑,避免请求失败时整个连接器无响应。
- 注意API的请求频率限制,如果一次性发23个请求,可能会被限流,必要时可以用
setTimeout分批发送请求。
内容的提问来源于stack exchange,提问作者Dan_68
相关产品推荐
相关产品推荐

