JavaScript 异步编程
最近开始不断学习和实践 JavaScript,由于对性能测试的敏感性,首先研究了 JavaScript 的异步编程。目前来看,和之前学过的 Java 和 Go 存在显著差异。JavaScript 异步编程的语法相对复杂,可能也是因为我还没有足够的实践经验。
异步编程
异步编程是一种不阻塞主线程的任务处理方式。相较于同步编程,异步编程允许程序在等待某个任务(如网络请求或文件读写)完成的同时,继续执行其他操作。这样的机制极大地提高了程序的效率,尤其是在处理大量 I/O 操作时,表现尤为出色。
JavaScript 的单线程特性使得异步编程尤为重要。当 JavaScript 在浏览器环境中运行时,只有一个主线程负责执行代码。如果某个耗时任务未使用异步处理,主线程将被阻塞,导致页面的响应性大幅下降。因此,JavaScript 提供了多种异步处理方式,如回调函数、Promise 和 async/await
,以避免主线程阻塞,保证页面的流畅性。
回调函数(Callback)
回调函数是作为参数传递给另一个函数,并在该函数执行完毕后被调用的函数。在 JavaScript 异步编程中,回调函数是最早期也是最基础的实现方式。当某个异步操作完成时,运行时环境会调用提供的回调函数,继续执行后续逻辑。回调函数通常与浏览器或 Node.js 的事件循环机制相结合来实现异步行为。
示例
function fetchData(callback) {
console.log("Fetching data...");
setTimeout(() => {
const data = "Hello FunTester!";
callback(data); // 调用回调函数,传递数据
}, 1000);
}
function processData(data) {
console.log("Data received:", data); // 处理数据的逻辑
}
console.info("start --------");
fetchData(processData); // 调用 fetchData 函数,传入回调函数
console.info("end --------");
执行输出结果:
start --------
Fetching data...
end --------
Data received: Hello FunTester!
Promise
Promise 是一种用于处理异步操作的对象,它代表了异步操作的最终结果。Promise 提供了一种更为清晰且可读的方式来管理多个异步操作,避免回调地狱,使代码结构更加扁平化和易于维护。
Promise 的三种状态
- Pending(待定):初始状态,表示异步操作尚未完成。
- Fulfilled(已完成):异步操作成功完成,并返回一个值。
- Rejected(已拒绝):异步操作失败,并返回一个原因。
示例
const fetchData = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("hello, FunTester");
} else {
reject("sorry, error occurred");
}
}, 1000);
});
};
console.info("start--------------");
fetchData()
.then((data) => {
console.log("received:", data);
return data.length;
})
.then((length) => {
console.log("length:", length);
})
.catch((error) => {
console.error("Error:", error);
});
console.info("end--------------");
执行输出结果:
start--------------
end--------------
received: hello, FunTester
length: 16
async/await
async/await
是基于 Promise 的语法糖,使异步代码更像同步代码。它简化了 Promise 链式调用的复杂性,提升了代码的可读性。
示例:使用 async/await
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("hello funtester");
} else {
reject("sorry, error");
}
}, 1000);
});
};
async function getData() {
try {
const data = await fetchData();
console.log("Data received:", data);
} catch (error) {
console.error("Error:", error);
}
}
console.info("start----------------");
let data = getData();
console.log("data:", data);
console.info("end----------------");
执行输出结果:
start----------------
data: Promise { <pending> }
end----------------
Data received: hello funtester
异步编程的重要性
JavaScript 的单线程特性使得异步编程在保持用户体验流畅性和性能优化方面显得至关重要。异步编程允许 JavaScript 在执行 I/O 密集型任务时保持高效,不阻塞主线程,从而确保应用程序的良好交互性。
Promise.all 和 Promise.race
在异步编程中,Promise.all
和 Promise.race
是非常有用的工具,特别是在处理多个并发的异步操作时。它们都可以帮助我们同时处理多个 Promise,但行为和用途有所不同。下面我将详细说明它们的用法和实际场景。
Promise.all
Promise.all
接受一个包含多个 Promise 的数组(或可迭代对象),当数组中的所有 Promise 都成功时,Promise.all
返回一个新 Promise,其值是一个包含所有 Promise 结果的数组。如果其中任何一个 Promise 失败,Promise.all
会立即返回一个被拒绝的 Promise,错误信息是第一个失败的 Promise 的原因。
这种方法非常适合需要并行处理多个异步任务,并且只有在所有任务都成功完成时,才继续处理后续操作的场景。
使用场景
- 执行并发的 API 请求并等待所有请求完成后再处理数据。
- 在同时处理多个文件读写或数据库查询时,可以并行化处理并在所有任务完成后执行操作。
示例:
const fetchData1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("fetchData1 complete");
resolve("data1");
}, 1000);
});
};
const fetchData2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("fetchData2 complete");
resolve("data2");
}, 2000);
});
};
const fetchData3 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("fetchData3 complete");
resolve("data3");
}, 3000);
});
};
console.info("start--------------");
Promise.all([fetchData1(), fetchData2(), fetchData3()])
.then((results) => {
console.log("All Promises resolved:", results);
})
.catch((error) => {
console.error("One of the Promises failed:", error);
});
console.info("end--------------");
输出结果:
start--------------
fetchData1 complete
fetchData2 complete
fetchData3 complete
All Promises resolved: [ 'data1', 'data2', 'data3' ]
end--------------
在这个例子中,Promise.all
等待所有的异步操作完成后才会进入 .then
,并且结果是每个异步任务的返回值数组。如果任何一个任务失败,Promise.all
将进入 .catch
。
Promise.race
Promise.race
与 Promise.all
的行为不同,它会在第一个 Promise 完成(无论是成功还是失败)时立即返回该 Promise 的结果。这意味着 Promise.race
的主要作用是返回最快完成的异步任务。
使用场景
- 竞态条件:需要在多个异步任务中选择最快完成的一个,例如多个 API 请求中选最快响应的。
- 超时机制:可以通过
Promise.race
实现异步任务的超时控制。
示例:
const fetchData1 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("fetchData1 complete");
resolve("data1");
}, 1000);
});
};
const fetchData2 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("fetchData2 complete");
resolve("data2");
}, 3000);
});
};
const fetchData3 = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("fetchData3 complete");
resolve("data3");
}, 500);
});
};
console.info("start--------------");
Promise.race([fetchData1(), fetchData2(), fetchData3()])
.then((result) => {
console.log("Fastest Promise resolved:", result);
})
.catch((error) => {
console.error("Fastest Promise rejected:", error);
});
console.info("end--------------");
输出结果:
start--------------
fetchData3 complete
Fastest Promise resolved: data3
end--------------
在这个例子中,Promise.race
返回最快完成的 fetchData3
的结果,因为它只用了 500 毫秒完成。即使其他 Promise 还在进行,Promise.race
也会立即返回第一个完成的 Promise。
超时控制示例
Promise.race
也可以用于实现异步任务的超时控制。例如,我们希望某个异步任务在 2 秒内完成,否则就返回超时错误。
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("data fetched");
}, 3000); // 模拟一个耗时 3 秒的操作
});
};
const timeout = (ms) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("Timeout error");
}, ms);
});
};
console.info("start--------------");
Promise.race([fetchData(), timeout(2000)])
.then((result) => {
console.log("Result:", result);
})
.catch((error) => {
console.error("Error:", error);
});
console.info("end--------------");
输出结果:
start--------------
Error: Timeout error
end--------------
在这个例子中,fetchData
耗时 3 秒,但我们设定的超时时间为 2 秒。因此,Promise.race
会返回超时错误。
总结
- Promise.all:用于并行处理多个异步任务,并且只在所有任务都完成时继续执行。适合所有任务必须完成才能进行下一步操作的场景。
- Promise.race:返回第一个完成的 Promise(无论成功或失败)。适合竞态条件或需要实现超时控制的场景。
通过灵活运用 Promise.all
和 Promise.race
,我们可以更高效地处理复杂的异步操作,提高代码的可读性和性能。