在现代 Web 开发中,异步编程是不可或缺的核心概念。本文将通过几个实际的代码示例,带你深入理解 JavaScript 中的异步操作,从传统的 Ajax 到现代的 Promise 和 Fetch API。
1. 传统 Ajax 与现代 Fetch API
XMLHttpRequest:经典的异步请求方式
html
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', true);
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
}
</script>
XMLHttpRequest 是传统的异步请求方式,基于回调函数实现。需要手动处理 readyState 和 status 状态码,代码相对复杂。
Fetch API:现代化的替代方案
html
<script>
fetch('https://api.github.com/orgs/lemoncode/members')
.then(res => res.json())
.then(data => {
console.log(data);
})
</script>
Fetch API 的优势:
- 基于 Promise 实现,支持链式调用
- 语法简洁,无需手动处理状态码
- 更符合现代 JavaScript 编程风格
2. 封装基于 Promise 的 getJSON 函数
为了将传统的 Ajax 改造成 Promise 风格,我们可以封装一个 getJSON 函数:
javascript
const getJSON = (url) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
resolve(data);
} else {
reject(`HTTP错误: ${xhr.status}`);
}
}
}
xhr.onerror = function() {
reject('网络错误');
}
});
}
// 使用示例
getJSON('https://api.github.com/orgs/lemoncode/members')
.then(data => {
console.log(data);
})
.catch(err => {
console.log(err);
})
关键点说明:
- Promise 构造函数接收一个执行器函数,同步执行
- resolve() 将 Promise 状态改为 fulfilled,触发 then 回调
- reject() 将 Promise 状态改为 rejected,触发 catch 回调
- onerror 只能捕获网络错误,不能捕获 HTTP 错误状态码
3. Promise 状态管理
| 状态 (State) | 含义 | 说明 |
|---|---|---|
| pending (等待中) | 初始状态 | Promise 被创建后,尚未被兑现或拒绝时的状态 |
| fulfilled (已成功) | 操作成功完成 | 异步任务成功结束,调用了 resolve(value) |
| rejected (已失败) | 操作失败 | 异步任务出错,调用了 reject(reason) |
4. 实现 Sleep 函数:控制异步流程
在同步语言中,我们可以使用 sleep 函数暂停程序执行,但在 JavaScript 中需要借助 Promise 模拟这一行为:
javascript
// 基础版本
function sleep(n) {
let p;
p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(p); // pending 状态
resolve(); // 或 reject()
console.log(p); // fulfilled/rejected 状态
}, n);
});
return p;
}
// 简化版本
const sleep = n => new Promise(resolve => setTimeout(resolve, n));
// 使用示例
sleep(3000)
.then(() => {
console.log('3秒后执行');
})
.catch(() => {
console.log('error');
})
.finally(() => {
console.log('finally'); // 无论成功失败都会执行
});
setTimeout vs Sleep:本质区别
setTimeout - 事件调度
js
console.log("第1步");
setTimeout(() => {
console.log("第3步 - 在2秒后执行");
}, 2000);
console.log("第2步 - 不会等待setTimeout");
// 输出顺序:
// 第1步
// 第2步 - 不会等待setTimeout
// 第3步 - 在2秒后执行
Sleep - 流程控制
js
async function demo() {
console.log("第1步");
await sleep(2000); // 真正暂停函数的执行
console.log("第2步 - 在2秒后执行");
}
demo();
// 输出顺序:
// 第1步
// (等待2秒)
// 第2步 - 在2秒后执行
这么来说吧:setTimeout是告诉 JavaScript 引擎:"等 X 毫秒后,帮我执行这个函数。" 它会去做别的同步操作,不会阻塞其他代码的执行。sleep则是在一个连续的异步操作流中,插入一段等待时间。sleep 让你写出"顺序等待"的逻辑,而 setTimeout 只是"未来某个时间点做某事"。
5. JavaScript 内存管理与数据拷贝
理解 JavaScript 的内存管理对于编写高效代码至关重要:
javascript
const arr = [1, 2, 3, 4, 5, 6];
const arr2 = [].concat(arr);
arr2[0] = 0;
console.log(arr, arr2); // arr 不变,arr2 改变
// 深拷贝 - 开销大
const arr3 = JSON.parse(JSON.stringify(arr));
arr3[0] = 100;
console.log(arr, arr3); // arr 不变,arr3 改变
在数组上进行存储的时候我们应该尽量规避
JSON序列化的深拷贝,开销太大,我们可以选择用[].cancat(arr)这种巧妙的方法实现拷贝,虽然创建了一个新数组,但开销仍然小于序列化。
内存管理要点:
- JS 变量在编译阶段分配内存空间
- 简单数据类型存储在栈内存中
- 复杂数据类型存储在堆内存中,栈内存存储引用地址
- 浅拷贝只复制引用,深拷贝创建完全独立的新对象
总结
通过本文的示例和解析,我们深入探讨了:
- Ajax 与 Fetch 的对比:Fetch API 提供了更简洁的 Promise-based 接口
- 手写getJson函数:将传统的 Ajax 改造成 现代的 Promise 风格
- 手写sleep函数:使用 Promise 实现类似同步的编程体验
- 内存管理:理解变量存储方式对编写高效代码的重要性
掌握这些概念将帮助你编写更清晰、更易维护的异步 JavaScript 代码,为学习更高级的 async/await 语法打下坚实基础。