本文详细介绍了如何将同步XMLHttpRequest请求转换为异步模式,以避免阻塞主线程并提升用户体验。通过XMLHttpRequest的事件监听机制和现代Fetch API,我们将展示如何高效、非阻塞地获取服务器端文件的最后修改时间,并实现页面根据文件状态自动刷新,同时提供示例代码和最佳实践。
1. 同步XMLHttpRequest的问题与危害
在web开发中,通过javascript与服务器进行数据交互是常见的需求。xmlhttprequest(xhr)是实现这一功能的核心api之一。然而,当xhr请求设置为同步模式(即req.open(“head”, url, false);中的第三个参数为false)时,它会阻塞浏览器的主线程,直到请求完成。这意味着在请求期间,用户界面将变得无响应,无法滚动、点击或执行任何其他操作,严重损害用户体验。现代浏览器普遍对此行为发出警告,并强烈建议避免在主线程中使用同步xhr。
原始的同步请求代码示例如下,它用于获取文件最后修改时间并计算其与当前时间的时间差:
<script> var pageload = new Date(); function fetchHeader(url, wch) { try { var req=new XMLHttpRequest(); req.open("HEAD", url, false); // 同步请求,问题所在 req.send(null); if(req.status== 200){ return req.getResponseHeader(wch); } else return false; } catch(er) { return er.message; } } window.setInterval(function(){ var time = Date.parse(fetchHeader("Akut.txt",'Last-Modified')); var d = new Date(); var diffSec = Math.round((d - time) / 1000); document.getElementById("time").innerHTML = Math.trunc(diffSec/60) + " minutes"; // time since it was modified if ((d-pageload)/1000 > 10 && diffSec < 5 ){ location.reload(); } }, 2500); </script>
尽管这段代码能实现功能,但其同步特性会导致页面卡顿,尤其是在网络条件不佳或请求频率较高时。浏览器控制台通常会给出类似“Synchronous XMLHttpRequest on the main thread is deprecated…”的警告。
2. 异步XMLHttpRequest的实现
要解决同步XHR带来的问题,核心在于将其转换为异步模式。异步请求不会阻塞主线程,而是通过回调函数或事件监听器在请求完成后通知我们。
2.1 异步XHR原理
异步XHR通过以下两种方式处理响应:
- onreadystatechange 事件: 这是一个较旧但仍有效的方法。当readyState属性发生变化时触发,当readyState达到XMLHttpRequest.DONE(4)时,表示请求已完成。
- addEventListener(‘load’) 事件: 这是更推荐的现代方法,当XHR请求成功完成时触发。它更简洁,并且只关注请求成功的最终状态。
2.2 异步XHR代码示例
以下是将原同步XHR代码转换为异步的实现,它利用addEventListener(‘load’)来处理响应:
const pageload = new Date(); // 页面加载时间 const url = "Akut.txt"; // 目标文件URL const whichHeader = "Last-Modified"; // 要获取的响应头 // 比较时间并更新UI的函数 const compareTimeToNow = (time) => { let d = new Date(); let diffSec = Math.round((d - time) / 1000); document.getElementById("time").textContent = Math.trunc(diffSec / 60) + " minutes"; // 显示自修改以来的分钟数 // 如果页面加载超过10秒且文件在5秒内被修改过,则刷新页面 if ((d - pageload) / 1000 > 10 && diffSec < 5) { location.reload(); } else { // 请求完成后,等待2.5秒再次发起请求,形成周期性检查 setTimeout(getHeader, 2500); } }; // XHR请求完成时的监听器 function reqListener() { // console.log(this.responseText); // HEAD请求通常responseText为空 // 从响应头获取'Last-Modified'时间,并转换为Date对象,然后传递给compareTimeToNow compareTimeToNow(new Date(this.getResponseHeader(whichHeader)).getTime()); } // 发起HEAD请求的函数 const getHeader = () => { const req = new XMLHttpRequest(); req.addEventListener("load", reqListener); // 监听load事件 req.open("HEAD", url); // 异步请求,默认第三个参数为true req.send(); }; // 首次调用以启动周期性检查 getHeader();
代码解析:
- req.open(“HEAD”, url);:第三个参数默认为true,表示异步请求。
- req.addEventListener(“load”, reqListener);:当请求成功加载时,reqListener函数会被调用。
- reqListener函数内部:通过this.getResponseHeader(whichHeader)获取所需的Last-Modified头信息,并将其转换为时间戳传递给compareTimeToNow函数。
- 周期性检查:这里不再使用setInterval,而是采用setTimeout。在compareTimeToNow函数中,每次处理完数据后,如果不需要刷新页面,就调用setTimeout(getHeader, 2500)来调度下一次请求。这种模式可以确保前一个请求完全完成后才发起下一个请求,避免了请求重叠和资源竞争的问题,尤其适用于请求时间不确定的场景。
3. 使用Fetch API的现代方法
Fetch API是现代Web开发中进行网络请求的推荐方式,它基于Promise,提供了一种更强大、更灵活、更简洁的API来替代XMLHttpRequest。Fetch API的使用可以进一步简化代码,并提供更好的错误处理机制。
3.1 Fetch API的优势
- 基于Promise: 使得异步代码的编写更加直观,避免了回调地狱。
- 更简洁的语法: 相较于XHR,Fetch API的链式调用使得代码更易读。
- 更强大的功能: 支持更多HTTP特性,如CORS、流式响应等。
3.2 Fetch API代码示例
以下是使用Fetch API实现相同功能的代码:
const pageload = new Date(); // 页面加载时间 const url = "Akut.txt"; // 目标文件URL const whichHeader = "Last-Modified"; // 要获取的响应头 // 比较时间并更新UI的函数(与XHR版本相同) const compareTimeToNow = (time) => { let d = new Date(); let diffSec = Math.round((d - time) / 1000); document.getElementById("time").textContent = Math.trunc(diffSec / 60) + " minutes"; // 显示自修改以来的分钟数 if ((d - pageload) / 1000 > 10 && diffSec < 5) { location.reload(); } else { setTimeout(getHeader, 2500); // 周期性调度下一次请求 } }; // 发起Fetch请求的函数 const getHeader = () => { fetch(url, { method: 'HEAD' }) // 发起HEAD请求 .then(rsp => { // 获取响应头,并转换为时间戳传递给compareTimeToNow compareTimeToNow(new Date(rsp.headers.get(whichHeader)).getTime()); }) .catch(error => { console.error("Fetch请求失败:", error); // 失败后也应考虑是否继续调度下一次请求,例如延迟重试 setTimeout(getHeader, 5000); // 失败后延迟更久重试 }); }; // 首次调用以启动周期性检查 getHeader();
代码解析:
- fetch(url, { method: ‘HEAD’ }):发起一个HEAD请求。默认是GET请求,需要通过method选项指定。
- .then(rsp => { … }):当请求成功并接收到响应时,Promise会解析,并提供一个Response对象rsp。
- rsp.headers.get(whichHeader):通过Response对象的headers属性(一个Headers对象)来获取指定的响应头。
- .catch(error => { … }):用于捕获网络错误或请求失败的情况,提供了健壮的错误处理机制。
4. 注意事项与最佳实践
- 错误处理: 无论是XHR还是Fetch,都应加入适当的错误处理逻辑。对于XHR,可以在addEventListener(‘error’, handler)或addEventListener(‘abort’, handler)中处理;对于Fetch,使用.catch()方法捕获Promise的拒绝状态。
- 周期性检查的优化:
- setTimeout vs setInterval: 在需要周期性执行异步操作时,通常推荐使用setTimeout递归调用自身,而不是setInterval。这是因为setInterval会不顾前一个任务是否完成就启动下一个任务,可能导致任务堆叠或资源耗尽。而setTimeout则确保了前一个任务完成后才调度下一个任务。
- 动态间隔: 如果文件修改不频繁,可以考虑根据文件的修改频率动态调整检查间隔,或者在服务器端提供更高效的通知机制(如WebSocket或Server-Sent Events)。
- 浏览器兼容性: Fetch API在现代浏览器中得到了广泛支持(Chrome 42+, Edge 14+, Safari 10.1+, Firefox 39+, Opera 29+),但不支持IE。如果需要兼容IE浏览器,则必须使用异步XMLHttpRequest。
- 服务器负载: 频繁地向服务器发送HEAD请求,即使请求本身很轻量,也可能对服务器造成不必要的负载。在生产环境中,应仔细评估其影响。
- 替代方案: 对于需要高实时性的场景,轮询(Polling)通常不是最佳选择。可以考虑以下替代方案:
- WebSockets: 提供全双工通信,服务器可以在文件修改时主动推送消息给客户端。
- Server-Sent Events (SSE): 允许服务器单向推送数据到客户端,适用于服务器事件驱动的更新。
总结
将同步XMLHttpRequest请求转换为异步模式是优化Web性能的关键一步,它能有效避免主线程阻塞,提升用户体验。通过XMLHttpRequest的事件监听器或更现代、更强大的Fetch API,我们可以轻松实现非阻塞的网络请求。在选择具体实现方式时,应根据项目需求、浏览器兼容性以及对代码简洁性的偏好进行权衡。同时,对于周期性任务,采用setTimeout递归调用而非setInterval,并结合适当的错误处理,是构建健壮Web应用的最佳实践。
评论(已关闭)
评论已关闭