如何根据数据量批量创建XMLHttpRequest并正确处理回调?
问题
我有一段JavaScript代码,通过XMLHttpRequest(简称xhr)发起请求,收到响应后会通过第二个XMLHttpRequest(xhr2)获取剩余数据。现在需要将第二次请求拆分为批量调用,根据数据量决定调用次数。首次请求后可以计算出需要调用的次数:
var batches = Math.ceil((counter.innerText.substring(0,counter.innerText.indexOf(" ")) - 100) / 1000);
但直接复用xhr2多次调用无法正常工作,因为第一次调用后它会关闭。我意识到需要根据batches的值初始化多个XMLHttpRequest,但不知道如何简洁地定义onreadystatechange函数。
最初的代码如下:
function get_tracklist_data(path, cid, title) { var xhr = new XMLHttpRequest(); var xhr2 = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var counter = document.getElementById("counter"); counter.innerHTML = xhr.responseText.substring(0, xhr.responseText.indexOf(":")); var data = document.getElementById("data"); data.innerHTML = xhr.responseText.substring(xhr.responseText.indexOf(":") + 1); //计算需要调用的次数 var batches = Math.ceil((counter.innerText.substring(0,counter.innerText.indexOf(" ")) - 100) / 1000); for(i=1; i<batches;i++) { xhr2.open('GET',path + '?cid=' + cid + "&title=" + title+"&batch=" + i, true); xhr2.send(); } } }; xhr2.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var data = document.getElementById("tbody"); data.innerHTML+=xhr2.responseText; } }; xhr.open('GET',path + '?cid=' + cid + "&title=" + title +"&batch=0", true); xhr.send(); };
我尝试将xhr2改为循环内的局部变量,但仍无法正常工作,会多次返回相同数据:
function get_tracklist_data(path, cid, title) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var counter = document.getElementById("counter"); counter.innerHTML = xhr.responseText.substring(0, xhr.responseText.indexOf(":")); var data = document.getElementById("data"); data.innerHTML = xhr.responseText.substring(xhr.responseText.indexOf(":") + 1); //计算需要调用的次数 var batches = Math.ceil((counter.innerText.substring(0,counter.innerText.indexOf(" ")) - 100) / 1000); for(i=1; i<batches;i++) { var xhr2 = new XMLHttpRequest(); xhr2.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var data = document.getElementById("tbody"); data.innerHTML+=xhr2.responseText; } }; xhr2.open('GET',path + '?cid=' + cid + "&title=" + title+"&batch=" + i, true); xhr2.send(); } } }; xhr.open('GET',path + '?cid=' + cid + "&title=" + title +"&batch=0", true); xhr.send(); };
请问如何正确实现批量XMLHttpRequest调用并处理回调,避免重复数据问题?
解决方案
问题根源
重复数据问题的核心是闭包变量捕获和变量作用域的问题:
- 循环中使用
var i声明变量,i属于函数级作用域,所有回调函数共享同一个i引用,当回调执行时,循环已经结束,i的值变成了batches,导致所有请求的batch参数都是同一个值。 - 回调里直接引用
xhr2,但xhr2是循环内的变量,当回调异步执行时,xhr2已经被循环覆盖为最后一次创建的实例,导致所有回调都用同一个xhr2的响应数据。
修复方案
方案1:使用ES6的let声明变量(推荐)
let会创建块级作用域,每次循环都会生成独立的i和xhr2实例,回调会捕获当前循环的变量:
function get_tracklist_data(path, cid, title) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var counter = document.getElementById("counter"); counter.innerHTML = xhr.responseText.substring(0, xhr.responseText.indexOf(":")); var data = document.getElementById("data"); data.innerHTML = xhr.responseText.substring(xhr.responseText.indexOf(":") + 1); //计算需要调用的次数 var batches = Math.ceil((counter.innerText.substring(0,counter.innerText.indexOf(" ")) - 100) / 1000); //用let声明i,创建块级作用域 for(let i=1; i<batches;i++) { let xhr2 = new XMLHttpRequest(); xhr2.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var tbody = document.getElementById("tbody"); //用this指代当前的xhr实例,避免引用覆盖问题 tbody.innerHTML += this.responseText; } }; xhr2.open('GET', `${path}?cid=${cid}&title=${title}&batch=${i}`, true); xhr2.send(); } } }; xhr.open('GET', `${path}?cid=${cid}&title=${title}&batch=0`, true); xhr.send(); };
方案2:使用立即执行函数(IIFE)创建闭包(兼容旧浏览器)
如果需要兼容不支持ES6的环境,可以用IIFE把每次循环的i和xhr2封装到独立作用域中:
function get_tracklist_data(path, cid, title) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var counter = document.getElementById("counter"); counter.innerHTML = xhr.responseText.substring(0, xhr.responseText.indexOf(":")); var data = document.getElementById("data"); data.innerHTML = xhr.responseText.substring(xhr.responseText.indexOf(":") + 1); //计算需要调用的次数 var batches = Math.ceil((counter.innerText.substring(0,counter.innerText.indexOf(" ")) - 100) / 1000); for(var i=1; i<batches;i++) { //用IIFE封装当前循环的i和xhr2 (function(batchNum){ var xhr2 = new XMLHttpRequest(); xhr2.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { var tbody = document.getElementById("tbody"); tbody.innerHTML += this.responseText; } }; xhr2.open('GET', path + '?cid=' + cid + "&title=" + title + "&batch=" + batchNum, true); xhr2.send(); })(i); } } }; xhr.open('GET', path + '?cid=' + cid + "&title=" + title + "&batch=0", true); xhr.send(); };
额外优化建议
- 回调中使用
this.responseText代替xhr2.responseText,this在onreadystatechange回调中就是当前的XMLHttpRequest实例,更安全。 - 可以用模板字符串拼接URL,让代码更易读(如方案1中的
${path}?cid=${cid}&title=${title}&batch=${i})。 - 如果需要保证请求顺序,可以把批量请求改成串行执行,但如果顺序不敏感,并行请求效率更高。
内容的提问来源于stack exchange,提问作者Paul Taylor
相关产品推荐
相关产品推荐

