我理解这个反对使用同步 ajax 调用的一般建议,因为同步调用会阻塞 UI 渲染。
通常给出的另一个原因是同步AJAX的内存泄漏问题。
从MDN文档 -
注意:您不应该使用同步 XMLHttpRequests,因为由于网络固有的异步特性,使用同步请求时内存和事件可能会以多种方式泄漏。唯一的例外是同步请求在 Workers 内部运行良好。
同步调用如何导致内存泄漏?
我正在寻找一个实际的例子。任何指向有关该主题的任何文献的指针都会很棒。
如果 XHR按照规范正确实现,那么它不会泄漏:
如果一个 XMLHttpRequest 对象的状态是 OPENED 并且设置了 send() 标志,它的状态是 HEADERS_RECEIVED,或者它的状态是 LOADING,并且以下情况之一为真,则不得对 XMLHttpRequest 对象进行垃圾回收:
它注册了一个或多个事件侦听器,其类型为 readystatechange、progress、abort、error、load、timeout 或 loadend。
上传完成标志未设置,并且关联的 XMLHttpRequestUpload 对象注册了一个或多个事件侦听器,其类型为进度、中止、错误、加载、超时或加载结束。
如果一个 XMLHttpRequest 对象在其连接仍处于打开状态时被垃圾收集,则用户代理必须取消该对象打开的任何获取算法实例,丢弃为它们排队的所有任务,并丢弃从网络接收到的任何进一步数据。
因此,在您点击.send()
XHR 对象(以及它引用的任何东西)之后,就会对 GC 免疫。但是,任何错误或成功都会使 XHR 进入 DONE 状态,并再次成为 GC 的对象。XHR 对象是同步还是异步都没有关系。如果再次出现长同步请求,这并不重要,因为您只会停留在发送语句上,直到服务器响应。
然而,根据这张幻灯片,它至少在 2012 年的 Chrome/Chromium 中没有正确实现。根据规范,没有必要调用.abort()
,因为 DONE 状态意味着 XHR 对象应该已经是正常的 GCd。
我找不到任何证据来支持 MDN 的声明,我已经通过推特联系了作者。
我认为内存泄漏的发生主要是因为垃圾收集器无法完成它的工作。即你有一个东西的引用,GC 不能删除它。我写了一个简单的例子:
var getDataSync = function(url) {
console.log("getDataSync");
var request = new XMLHttpRequest();
request.open('GET', url, false); // `false` makes the request synchronous
try {
request.send(null);
if(request.status === 200) {
return request.responseText;
} else {
return "";
}
} catch(e) {
console.log("!ERROR");
}
}
var getDataAsync = function(url, callback) {
console.log("getDataAsync");
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(xhr.responseText);
} else {
callback("");
}
}
};
xhr.onerror = function (e) {
callback("");
};
xhr.send(null);
}
var requestsMade = 0
var requests = 1;
var url = "http://missing-url";
for(var i=0; i<requests; i++, requestsMade++) {
getDataSync(url);
// getDataAsync(url);
}
除了同步函数阻塞了很多东西之外,还有另一个很大的区别。错误处理。如果您使用getDataSync并删除 try-catch 块并刷新页面,您将看到抛出错误。那是因为 url 不存在,但现在的问题是当抛出错误时垃圾收集器是如何工作的。它是否清除了与错误相关的所有对象,是否保留了错误对象或类似的东西。如果有人对此有更多了解并在这里写信,我会很高兴。
从 GC 同步 XHR 块线程执行和该线程的函数执行堆栈中的所有对象。
例如:
function (b) {
var a = <big data>;
<work with> a and b
sync XHR
}
变量 a 和 b 在这里被阻塞(以及整个堆栈)。因此,如果 GC 开始工作,则同步 XHR 已阻塞堆栈,所有堆栈变量将被标记为“幸存 GC”并从早期堆移动到更持久的堆。并且即使单个 GC 也不应该存在的对象的基调将存在许多垃圾收集,甚至来自这些对象的引用也将存在 GC。
关于声明堆栈块 GC,并且该对象被标记为长寿命对象:请参阅 Clawing Our Way Back To Precision中的 Conservative Garbage Collection 部分。此外,在通常的堆被 GC之后,“标记”的对象被 GC,并且通常只有在仍然需要释放更多内存时(因为收集标记和清除的对象需要更多时间)。
更新:这真的是泄漏,而不仅仅是早期堆无效的解决方案?有几件事情需要考虑。
如果同步调用在完成之前被中断(即由用户事件重新使用 XMLHttpRequest 对象),则未完成的网络查询可能会挂起,无法被垃圾收集。
这是因为,如果在请求返回时发起请求的对象不存在,则返回无法完成,但(如果浏览器不完善)保留在内存中。您可以轻松地使用 setTimeout 在发出请求后但在它返回之前删除请求对象。
我记得在 2009 年左右,我在 IE 中遇到了一个大问题,但我希望现代浏览器不会受到它的影响。当然,现代库(即 JQuery)防止了它可能发生的情况,允许在不考虑它的情况下发出请求。
使用同步 AJAX 请求的内存泄漏通常由以下原因引起:
当浏览器出于某种原因没有从不再需要的对象中释放内存时,就会发生内存泄漏。
这可能是由于浏览器错误、浏览器扩展问题以及更罕见的是我们在代码架构中的错误。
这是在新上下文中运行 setInterval 时导致内存泄漏的示例:
var
Context = process.binding('evals').Context,
Script = process.binding('evals').Script,
total = 5000,
result = null;
process.nextTick(function memory() {
var mem = process.memoryUsage();
console.log('rss:', Math.round(((mem.rss/1024)/1024)) + "MB");
setTimeout(memory, 100);
});
console.log("STARTING");
process.nextTick(function run() {
var context = new Context();
context.setInterval = setInterval;
Script.runInContext('setInterval(function() {}, 0);',
context, 'test.js');
total--;
if (total) {
process.nextTick(run);
} else {
console.log("COMPLETE");
}
});