《在异步的河流里,打捞被冻结的 Ajax 请求》

40 阅读4分钟

在异步的河流里,打捞被冻结的Ajax请求


🎭 场景重现:两段几乎一样的代码,命运却天差地别

我们来看两个 HTML 页面,它们都试图从 GitHub API 获取 lemoncode 组织的成员列表,并渲染到页面上。

❌ 版本 A(错误示范):

<body>
    <ul id="members"></ul>
    <script>
        const xhr = new XMLHttpRequest();
        console.log(xhr.readyState,'------');
        xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members',false); //false!
        
        
        console.log(xhr.readyState,'|------');
        xhr.send();
        console.log(xhr.readyState,'|---|---');
        console.log(xhr.responseText);
        xhr.onreadystatechange = function() {
            console.log(xhr.readyState,'|---|---|');
            if(xhr.status === 200 && xhr.readyState === 4) {
                console.log(xhr.responseText);
                const data = JSON.parse(xhr.responseText);
                console.log(data);
                document.getElementById('members').innerHTML = data.map(item => `<li>${item.login}</li>`).join('');
            }
        }
    </script>
</body>

✅ 版本 B(正确写法):

<body>
    <ul id="members"></ul>
    <script>
        const xhr = new XMLHttpRequest();
        console.log(xhr.readyState,'------');
        xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members',true); //true !
        
        
        console.log(xhr.readyState,'|------');
        xhr.send();
        console.log(xhr.readyState,'|---|---');
        console.log(xhr.responseText);
        xhr.onreadystatechange = function() {
            console.log(xhr.readyState,'|---|---|');
            if(xhr.status === 200 && xhr.readyState === 4) {
                console.log(xhr.responseText);
                const data = JSON.parse(xhr.responseText);
                console.log(data);
                document.getElementById('members').innerHTML = data.map(item => `<li>${item.login}</li>`).join('');
            }
        }
    </script>
</body>

表面看,只是 open() 的第三个参数不同(false vs true),但背后隐藏着前端开发中最经典的误区之一误用同步请求

今天,我们就以这两段代码为线索,揭开 Ajax 的神秘面纱,搞懂:

  • 什么是同步/异步?
  • readyState 到底怎么变?
  • 为什么“立刻读取 responseText”是危险操作?
  • 如何写出健壮、高效、符合现代标准的 Ajax 请求?

🔍 一、核心差异全景对比表

对比维度❌ 错误写法(同步)✅ 正确写法(异步)
open() 第三个参数falsetrue(默认值)
是否阻塞 JavaScript 主线程✅ 是(页面卡死)❌ 否(非阻塞)
responseText 可读时机发送后立即可用(仅限同步)必须在 readyState === 4 时读取
事件监听器作用几乎无效(请求已结束)核心逻辑所在
用户体验极差(白屏/无响应)流畅(可加载动画等)
浏览器支持已被现代浏览器标记为 deprecated完全支持
适用场景几乎不存在所有真实项目

💡 划重点:Ajax 的“A” 就是 Asynchronous(异步) !用同步请求,等于自废武功!


🧪 二、深入 readyState:状态机的秘密

XMLHttpRequestreadyState 是一个状态机,共 5 个阶段:

状态名含义
0UNSENT初始状态,new XMLHttpRequest() 后
1OPENED调用 open() 后
2HEADERS_RECEIVED调用 send(),且收到响应头
3LOADING正在接收响应体(可能未完成)
4DONE请求完成,响应全部接收完毕 ✅

在你的控制台打印中观察变化:

✅ 异步版本(正确):
console.log(xhr.readyState, '------');        // 0
xhr.open(..., true);
console.log(xhr.readyState, '|------');       // 1
xhr.send();
console.log(xhr.readyState, '|---|---');      // 1(仍为1!因为异步,send 不阻塞)

// 后续由 onreadystatechange 触发:
// |---|---| → 2 → 3 → 4(逐步变化)
❌ 同步版本(错误):

console.log(xhr.readyState, '------');        // 0
xhr.open(..., false);
console.log(xhr.readyState, '|------');       // 1
xhr.send();                                   // ⚠️ 阻塞!直到请求完成
console.log(xhr.readyState, '|---|---');      // 4(直接跳到完成!)

// 但由于你在 xhr.send() 之后才绑定 onreadystatechange,
// 而同步请求在 send() 返回时早已完成(readyState=4),
// 因此监听器永远不会被触发——不是它不能触发,而是你“来晚了”。

💡 关键提醒:无论同步还是异步,事件监听器必须在 send() 之前绑定!否则在快速完成的请求中(尤其是同步),你会错过所有状态变化。

📌 关键洞察

  • 异步:send() 立即返回,后续靠事件驱动。
  • 同步:send() 会“卡住”直到服务器返回,期间整个页面无法交互!

💥 三、“立刻读取 responseText”为何是陷阱?

在错误代码中,有这样一行:

xhr.send();
console.log(xhr.responseText); // 危险!

问题在于

  1. 在异步模式下:此时 readyState 还是 1 或 2,responseText 为空或不完整,读取无意义。
  2. 在同步模式下:虽然此时数据已就绪,但你绕过了状态检查,一旦网络出错(如 404、500),responseText 可能是错误信息而非 JSON,导致 JSON.parse() 崩溃!

正确做法永远在 readyState === 4status === 200 时处理数据

xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      // 成功处理
    } else {
      // 处理错误:404、500、超时等
      console.error('请求失败:', xhr.status);
    }
  }
};

🚫 四、为什么同步 Ajax 是“前端毒药”?

1. 用户体验灾难

  • 页面完全冻结,用户无法点击、滚动、输入。
  • 移动端尤其明显:感觉“App 卡死了”。

2. 已被浏览器废弃

  • Chrome、Firefox 等已对主线程中的同步 XHR 发出警告。
  • MDN 明确指出“不应在主线程中使用同步请求”

3. 无法处理复杂场景

  • 不能设置超时(timeout 属性对同步无效)
  • 不能取消请求(虽然 abort() 可用,但同步时已无意义)
  • 无法监听上传/下载进度

🆕 五、现代替代方案:拥抱 fetch 与 async/await

虽然理解 XMLHttpRequest 很重要,但实际开发中,请优先使用更现代的 API:

✨ 使用 fetch(原生支持):

fetch('https://api.github.com/orgs/lemoncode/members')
  .then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  })
  .then(members => {
    document.getElementById('members').innerHTML = 
      members.map(m => `<li>${m.login}</li>`).join('');
  })
  .catch(err => console.error('请求失败:', err));

✨ 使用 async/await(更优雅):

 
async function loadMembers() {
  try {
    const res = await fetch('https://api.github.com/orgs/lemoncode/members');
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const members = await res.json();
    
    document.getElementById('members').innerHTML = 
      members.map(m => `<li>${m.login}</li>`).join('');
  } catch (err) {
    console.error('加载失败:', err);
  }
}
loadMembers();

💡 建议:学习时理解 xhr,开发时用 fetchaxios


🧠 六、补充:那些你可能忽略的细节

1. CORS 跨域问题

GitHub API 允许跨域,但很多私有 API 不允许。若遇到:

Blocked by CORS policy

说明后端未配置 Access-Control-Allow-Origin,需联系后端同学。

2. GitHub API 速率限制

未认证请求每小时最多 60 次。调试时注意不要频繁刷新!

3. 内存泄漏风险

长期存在的 xhr 对象若未正确释放(尤其在 SPA 中),可能导致内存泄漏。现代框架(React/Vue)通常自动处理,但原生开发需注意。