JavaScript 教程 · 第八章

异步:Promise、async/await、fetch

这一章是全教程最重要的一章之一。慢慢来,每段代码都可以点「运行」亲眼看到结果。

一这一章学什么

一句话目标

理解异步和 Promise(承诺),会用 async/await 等结果,会用 fetch 发请求并检查 resp.ok。

学完这一章,你就能看懂 Worker 和脚本里最常见的代码长什么样:为什么有的函数前面有个 async,为什么有的行前面有个 await,以及发网络请求之后为什么要先看一眼 resp.ok。

二概念:为什么要「等」

有些事不是马上就能办完的:发网络请求要等对方服务器回话,读存储(比如读 KV 里的数据)要等数据取回来。慢则几十毫秒,快要等的事情也可能拖到几秒。

JS 遇到这种事不会干等着——它不会傻站着等到天荒地老,而是先去忙别的。需要等的操作会返回一个 Promise,中文可以理解成一张「承诺」:意思是「结果以后给你,先拿着这张条子」。

为什么这一章必须过关

Cloudflare Workers 和各类平台脚本里,处处都是这种「要等」的操作。异步这套概念就是会写 Workers 和只会写小练习之间的分水岭,这一章不过关,后面的内容都会看不懂。

① 三种写法,只学一种:async/await

「等 Promise 出结果」这件事,历史上出现过三种写法,你只需要认识它们,真正要学的只有第三种:

async/await 的标准样子
async function load() {              // async 标在函数前
  const resp = await fetch("https://example.com/data");
  // ↑ await:等 fetch 发完请求,把回来的响应存进 resp
  const data = await resp.json();
  // ↑ 再等一下:把响应的 body 按 JSON 读出来
  return data;                       // 返回读到的数据
}

② fetch:发请求的统一方法

fetch 是发网络请求的统一方法,Workers、浏览器、Node 里都有它,长得一模一样,学会一次到处能用。最简单的用法是 await fetch(网址);要提交数据时,第二个参数可以写清楚方式、请求头和内容:

fetch 提交数据的写法
const resp = await fetch(url, {
  method: "POST",                    // 请求方式:POST 表示提交数据
  headers: {                         // 请求头:向对方说明你发的是什么
    "Content-Type": "application/json",
    "Authorization": "Bearer xxx"    // 身份令牌,xxx 换成你自己的
  },
  body: JSON.stringify({ key: "value" })
  // ↑ body 是要发的内容;JSON.stringify 把对象转成 JSON 文字
});

请求发完,拿到的 resp(响应)要这样用:

最大的坑:404 / 500 不算 fetch 失败

fetch 遇到 404、500 这类状态码时,照样正常返回,不会报错、不会走 catch。它只管「把响应拿回来」,至于这个响应是好是坏,要你自己查 resp.ok 或 resp.status。只有断网、域名解析失败这类根本发不出去的情况,才会走 catch 报错。这一条记牢,练习题和真实写代码都会用到。

三动手试:点「运行」看结果

下面的代码框点「运行」,会在页面里真正执行这段代码,结果显示在代码框下方。先按顺序做实验一的 A、B 两版,对比一下差别。

实验一 A:不带 await,先看「承诺」长什么样

这段代码用 Promise + setTimeout 模拟了一个「等 1 秒才有结果」的操作(setTimeout 是「过一会儿再执行」的意思)。注意:调用时故意不写 await。

实验一 A · 不带 await
// 模拟一个“要等 1 秒才有结果”的操作
function getData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("真正的数据");   // 1 秒后,把结果交给承诺
    }, 1000);
  });
}

const result = getData();  // 注意:这里没写 await
console.log(result);       // 拿到的会是什么?
运行结果
看到了吗?打印出来的是 Promise { <pending> }——一张还在等待中的承诺,不是数据。这就是忘了写 await 时的样子。

实验一 B:带 await,拿到真正的数据

同样的操作,这次把调用包进 async 函数里并写上 await。点运行后等大约 1 秒,结果才出来——那 1 秒就是在「等」。

实验一 B · 带 await
// 还是那个“要等 1 秒才有结果”的操作
function getData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("真正的数据");
    }, 1000);
  });
}

async function main() {
  const result = await getData();  // 写了 await:等它出结果
  console.log(result);             // 这次拿到的是什么?
}

main();  // 调用这个 async 函数
运行结果
这次打印的是 真正的数据。同一个操作,写不写 await,结果天差地别:不写拿到承诺,写了才拿到数据。

实验二:不联网也能练 fetch

下面这段用一个特殊的 data: 网址在本机造了一份 JSON 数据,不用联网就能完整走一遍「fetch 发请求 → 查 resp.ok → resp.json() 读数据」的流程。

实验二 · fetch + json 全流程
async function main() {
  const resp = await fetch('data:application/json,{"ok":true}');
  console.log("resp.ok =", resp.ok);          // 是不是成功(200–299)
  console.log("resp.status =", resp.status);  // 状态码是多少
  const data = await resp.json();             // 按 JSON 读出 body
  console.log("读到的数据 =", data);
}

main();
运行结果
resp.ok 是 true、resp.status 是 200,说明这次请求成功;最后读到的数据是一个对象 { ok: true }。真实项目里把网址换成接口地址,流程一模一样。

四练一练

共 5 道选择题,点一个选项立刻判对错,选错了会告诉你错在哪。

答对 0 / 5

五适用案例:这些概念天天在用

1tg-bot 的定时刷新订阅

你的 tg-bot 里,scheduled(定时任务)到点刷新订阅时,就是一段 async/await 代码:先等订阅内容拉回来,再等它写进 KV,再等下一步……一步一步等结果,全靠这一章的概念撑着。看不懂异步,就看不懂这段代码在干什么。

2Worker 里 fetch 上游接口取数据

Worker 向外面的接口要数据,标准动作是:const resp = await fetch(上游地址),回来先查 resp.ok——是 true 才继续 await resp.json() 读数据;是 false 就说明上游出问题了,走报错处理。顺序反了(不查就读),上游一出故障就会拿到一堆错误页面当数据用。

3忘了 await:Workers 新手最常见的 bug

拿到 Promise 不是数据,往下用就全错——比如把 Promise 直接写进 KV、拿它跟字符串拼接、拿它去判断真假,全都会得到莫名其妙的结果,而且往往不报错、静悄悄地错。以后你写的代码结果不对劲,第一件事就是回头检查:是不是哪一行忘了写 await。

本章小结

异步就是「先拿承诺、以后给结果」;async 让函数里能用 await,await 负责把真正的结果等出来;fetch 发请求后先查 resp.ok,再读 body,而且 body 只能读一次。