异步 JavaScript

有些事情发生得很快:两个数字相加、读取一个变量、改变页面上的文本。有些事情需要时间。一个计时器倒计时三秒。一个到服务器的请求穿过网络后片刻才回来。如果 JavaScript 停下来等待每一个这样的事情,整个页面都会冻结:没有点击、没有滚动、什么都没有,直到那个缓慢的事情完成。它不是这样运作的。本章讲的是 JavaScript 如何启动某个缓慢的事情、保持页面的响应性,以及在结果准备好时回到那里处理结果。
为什么需要异步
大多数 JavaScript 从上到下运行,一行在下一行开始之前完成。这对快速工作来说很好。但是有些任务需要真实的时间,JavaScript 不会坐在那里等待。它启动缓慢的任务,然后继续下一行,在缓慢任务完成时回到它。这就是**异步**的意思:不是按顺序发生,不是现在发生,而是稍后,当结果准备好时发生。
最简单的例子就是计时器。setTimeout 在延迟后运行一段代码:
console.log("开始");
setTimeout(() => {
console.log("3 秒后");
}, 3000);
console.log("结束");
// 开始
// 结束
// 3 秒后注意顺序。"结束" 在 "3 秒后" 之前打印,即使它在代码中 setTimeout 之后。JavaScript 没有在那一行等待三秒。它设置了计时器,继续运行,并在时间到了时运行延迟的代码。
"结束" 在 setTimeout 消息之前打印,即使计时器在代码中先出现。 Promise
setTimeout 对于延迟很好,但大多数异步工作产生一个你关心的值:你请求的数据,或者如果出错了一个错误。为此,JavaScript 使用一个**Promise**。一个 Promise 是一个还没有准备好的值,为它预留了一个位置。它就像一张收据:你没有有饭,但你有会变成饭的东西。
一个 Promise 以两种方式结束。当工作成功时它解决一个值,或当它失败时它拒绝一个错误。你用 .then() 和 .catch() 说出在每种情况下做什么:
loadUser()
.then((user) => {
console.log(`已加载 ${user.name}`);
})
.catch((error) => {
console.log("出错了");
});.then() 在 Promise 解决时运行,它接收这个值。.catch() 在它拒绝时运行,它接收错误。它们内的代码稍后运行,一旦结果到达。
.then() 说对值做什么,用 .catch() 对错误做什么,两者都在稍后一旦结果进来时运行。 async / await
Promise 链有效,但有一个更清晰的方式写它们,读起来几乎像普通的从上到下的代码。它使用两个关键字,async 和**await**。
你标记一个函数 async,在它内部你可以 await 一个 Promise。await 暂停函数直到 Promise 解决,然后给你值,不需要 .then():
async function showUser() {
const user = await loadUser();
console.log(`已加载 ${user.name}`);
}从上到下读那个:得到用户,然后记录名字。await 行等待 loadUser() 完成并给你 user。函数在那里暂停,但页面的其余部分继续运行,所以什么都不冻结。
为了处理错误,用 try 和 catch 包裹 await:
async function showUser() {
try {
const user = await loadUser();
console.log(`已加载 ${user.name}`);
} catch (error) {
console.log("无法加载用户");
}
}如果 Promise 失败,代码跳到 catch 块而不是崩溃。
async,你可以在它内部 await 一个 Promise。await 暂停那个函数直到 Promise 解决并直接给你值,所以代码从上到下读取,没有 .then()。用 try 和 catch 包裹它处理失败,页面在函数等待时继续运行。 获取数据
你将写的最常见的异步任务是从服务器加载数据。浏览器给你 fetch 为此。你传给它一个 URL,它为响应返回一个 Promise:
async function loadUsers() {
const response = await fetch("https://api.example.com/users");
const users = await response.json();
console.log(users);
}有两个 await,那开始让人惊讶。第一个等待服务器响应。第二个读响应的主体并用 .json() 将其变为 JavaScript 数据,那本身是异步的。所以获取是两个步骤:得到响应,然后读它。
服务器也可以用一个错误响应,像"不找到"。fetch 自己不把那当作一个失败,所以你检查 response.ok 自己:
async function loadUsers() {
const response = await fetch("https://api.example.com/users");
if (!response.ok) {
console.log("请求失败");
return;
}
const users = await response.json();
console.log(users);
}fetch(url) 为服务器的响应返回一个 Promise,加载数据是两个步骤:await fetch(...) 为响应,然后 await response.json() 读数据出它。一个服务器错误像"不找到"不自己失败,所以检查 response.ok 在你读主体之前。两个步骤都使用 await 因为两个都需要时间。 事件循环和并行运行工作
到目前为止的所有东西都有一个主题:JavaScript 启动缓慢的工作,继续运行,稍后回到它。这一部分是那背后的机制,加上为好地运行几个异步任务的模式。
这里是一张 JavaScript 如何追踪异步工作的图。你运行中的代码坐在调用栈上,正在执行什么的列表。当你调用 setTimeout 时,计时器被交给浏览器去倒计时,所以它不在栈上阻塞任何东西。当计时器完成时,它的回调进入一个任务队列,一个准备运行的代码的等待线。
**事件循环**是连接它们的部分。它有一个规则:先运行调用栈上的所有东西,只有当栈是空的时从队列拉下一个任务。这就是为什么 setTimeout(fn, 0) 仍然在你其他的代码之后运行,即使有一个零延迟。回调必须在队列中等待直到当前代码完成:
console.log("第一");
setTimeout(() => console.log("第三"), 0);
console.log("第二");
// 第一
// 第二
// 第三0 的延迟不意味着"现在"。它意味着"一旦当前的代码完成"。
setTimeout(fn, 0) 仍然在你代码的其余之后运行。0 的延迟意味着"一旦当前代码完成",不"现在"。 异步从这里去往何处
异步是 JavaScript 的部分保持一个页面活着当缓慢工作在背景发生时。形状总是相同的:启动需要时间的东西,让你代码的其余部分运行,当它到达时处理结果。Promise 给那个结果一个你可以传递的值,async/await 让你用平常的从上到下代码写它,fetch 是你将最使用所有这些的地方,从服务器加载数据。
从这里,异步连接到语言的部分反应到外面的世界。你将通过DOM将获取的数据连线到页面,你将启动异步工作来响应人们在页面上做什么,那就是事件。你回得的结果几乎总是对象,所以读和重塑它们是配对所有这个的下一个技能。

