异步:Promise、async/await、fetch
这一章是全教程最重要的一章之一。慢慢来,每段代码都可以点「运行」亲眼看到结果。
一这一章学什么
理解异步和 Promise(承诺),会用 async/await 等结果,会用 fetch 发请求并检查 resp.ok。
学完这一章,你就能看懂 Worker 和脚本里最常见的代码长什么样:为什么有的函数前面有个 async,为什么有的行前面有个 await,以及发网络请求之后为什么要先看一眼 resp.ok。
二概念:为什么要「等」
有些事不是马上就能办完的:发网络请求要等对方服务器回话,读存储(比如读 KV 里的数据)要等数据取回来。慢则几十毫秒,快要等的事情也可能拖到几秒。
JS 遇到这种事不会干等着——它不会傻站着等到天荒地老,而是先去忙别的。需要等的操作会返回一个 Promise,中文可以理解成一张「承诺」:意思是「结果以后给你,先拿着这张条子」。
Cloudflare Workers 和各类平台脚本里,处处都是这种「要等」的操作。异步这套概念就是会写 Workers 和只会写小练习之间的分水岭,这一章不过关,后面的内容都会看不懂。
① 三种写法,只学一种:async/await
「等 Promise 出结果」这件事,历史上出现过三种写法,你只需要认识它们,真正要学的只有第三种:
- 老写法:回调——把「等到了之后要干什么」写成一个函数交出去。平台脚本的 API 里还很常见,见到能认出来就行。
- 过渡写法:.then() 链——用
.then()一节一节往下接。了解即可,不用练。 - 现代写法:async/await——看起来跟普通代码一样,一行一行往下写。Workers 里统一用这种,本章只学这一种。
async function load() { // async 标在函数前
const resp = await fetch("https://example.com/data");
// ↑ await:等 fetch 发完请求,把回来的响应存进 resp
const data = await resp.json();
// ↑ 再等一下:把响应的 body 按 JSON 读出来
return data; // 返回读到的数据
}
async标在函数前面,表示:这个函数里面可以用await,而且这个函数本身返回的也是一个 Promise(记住这点,练习题会考)。await只能用在async函数里面,它的作用是等后面的 Promise 出结果。等待期间 JS 不阻塞,还能去处理别的事。- 忘了写 await 的典型症状:拿到的不是数据,而是
Promise { <pending> }(pending 是「还在等」的意思)。下面动手试的部分你会亲眼看到它。
② fetch:发请求的统一方法
fetch 是发网络请求的统一方法,Workers、浏览器、Node 里都有它,长得一模一样,学会一次到处能用。最简单的用法是 await 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(响应)要这样用:
resp.status是状态码:200 表示成功,404 表示没找到,500 表示对方服务器出错。resp.ok是个省事的判断:状态码在 200–299 之间时它是true(对),否则是false(错)。await resp.text()把 body(响应正文)按纯文本读出来。await resp.json()把 body 按 JSON 读出来,直接变成对象能用。- body 只能读一次:text() 和 json() 只能选一个、读一次,读完就没了,不能重复读。
fetch 遇到 404、500 这类状态码时,照样正常返回,不会报错、不会走 catch。它只管「把响应拿回来」,至于这个响应是好是坏,要你自己查 resp.ok 或 resp.status。只有断网、域名解析失败这类根本发不出去的情况,才会走 catch 报错。这一条记牢,练习题和真实写代码都会用到。
三动手试:点「运行」看结果
下面的代码框点「运行」,会在页面里真正执行这段代码,结果显示在代码框下方。先按顺序做实验一的 A、B 两版,对比一下差别。
实验一 A:不带 await,先看「承诺」长什么样
这段代码用 Promise + setTimeout 模拟了一个「等 1 秒才有结果」的操作(setTimeout 是「过一会儿再执行」的意思)。注意:调用时故意不写 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 秒就是在「等」。
// 还是那个“要等 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() 读数据」的流程。
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 只能读一次。