4.6 异步编程与数据请求:不等人的流水线


4.6 异步编程与数据请求:不等人的流水线

本节摘要:脚本引擎是单线程的,耗时操作若同步等待会把整条流水线冻住,于是浏览器把耗时任务交给后台,完成后排回队列——这是异步的全部由来。本节讲事件循环的排队规则、承诺对象的两种结局与链式写法、等待语法的同步化外表,并用请求接口为看板拉取车间数据、处理失败与超时。

一、为什么必须异步:单线程的生存法则

脚本引擎只有一条执行线程,同一时刻只做一件事。这条限制的出发点是简单:不用锁、不会数据竞争、页面状态不会因并发改写而打架。代价是任何耗时操作都会冻结一切——点击没反应、动画卡死、滚动不动。浏览器给的解法是分工:耗时的活(网络请求、定时器、读文件)交给后台部门去做,做完把"回调任务"排进任务队列,线程一空就从队列取下一件执行。

图 1:事件循环的排队与执行

图 1:事件循环的排队与执行

排队规则里有个细节日后会遇到:队列其实有两条,普通任务一条、微任务一条(承诺就绪的回调走这条),微任务优先插队。入门阶段记住结论即可——承诺的回调永远赶在下一次页面任务之前执行。

二、承诺:一个只有两种结局的工单

异步操作用承诺对象表示:提交时立即返回一张"回执",结局只有两种——兑现(拿到数据)或驳回(出了差错)。回执上挂两个回调:兑现时走一个、驳回时走另一个,必然走到且只走其一。相比老式的层层回调嵌套(回调地狱),承诺把"下一步"挂成链,代码的缩进水平恢复了正常人的形状。

// 承诺的两种读法 fetchOrders() .then(function (data) { console.log("拿到工单:", data); // 兑现分支 return renderOrders(data); // 返回值传给下一环 }) .then(function () { console.log("渲染完成"); }) .catch(function (err) { console.error("拉取失败:", err); // 驳回分支:链上任何一环出错都到这 }) .finally(function () { hideLoading(); // 必经分支:无论成败收起加载条 });
// 等待语法:异步代码写成同步的形状 async function loadOrders() { showLoading(); try { const data = await fetchOrders(); // 等待兑现:后面代码自动排到兑现之后 renderOrders(data); console.log("渲染完成"); } catch (err) { console.error("拉取失败:", err); // 出错统一落网 } finally { hideLoading(); } } loadOrders(); // 与上一段完全等价,但阅读顺序即执行顺序——现代代码首选 // 亲手造一个承诺:给老式回调接口穿上承诺外衣 function wait(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); // 定时到点:兑现这张回执 }); } async function demo() { console.log("等零点三秒"); await wait(300); console.log("到了"); } demo();

三、请求接口:看板连上信息源

网络请求是异步的主战场。请求接口收一个地址与配置,返回一个承诺:第一段兑现的是响应对象(状态码、响应头),还没读正文;再调它的读正文方法(按数据格式选择),才拿到数据。两段等待必须分开写,这是新手最常见的翻车点。

// 看板的数据拉取:完整两段式 async function fetchOrders() { const resp = await fetch("/api/orders?workshop=3"); // 第一段:等响应头到达——此刻只知道状态码,正文未读 if (!resp.ok) { throw new Error(`车间接口异常:${resp.status}`); // 状态码非成功段:主动抛错进驳回去——网络层成功不等于业务成功 } const data = await resp.json(); // 第二段:等正文解析成数据——漏掉这一段会拿到一个承诺对象而非数据 return data; } async function boot() { try { const orders = await fetchOrders(); renderOrders(orders); renderStatsBar(summarize(orders)); } catch (err) { showErrorBar("工单加载失败,请检查网络后重试"); // 用户看到的应是这句话,而不是堆栈——报错分层:给用户的与给开发者的 console.error(err); // 开发者看详情去控制台 } } boot();
// 超时与竞态:请求接口的两个进阶护栏 async function fetchWithTimeout(url, ms = 5000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), ms); // 中止控制器:到点主动掐断请求——默认请求可以挂很久 try { return await fetch(url, { signal: controller.signal }); } finally { clearTimeout(timer); // 成功了就撤销定时器 } } let latestId = 0; // 竞态护栏:只认最后一次请求 async function search(keyword) { const myId = ++latestId; const resp = await fetchWithTimeout(`/api/orders?kw=${encodeURIComponent(keyword)}`); // 编码接口:用户输入进地址前必须编码,特殊字符才不会破坏查询串 if (myId !== latestId) return; // 等待期间又发了新请求:本次作废 renderOrders(await resp.json()); } // 结合 4.2 节的防抖:连击只发一次、慢网只认最新——搜索功能的完整护栏

四、并行等待与失败策略

多个互不依赖的请求不必排队等:并行发出、一起等待,总耗时取决于最慢的那个而非全部之和。失败策略要按业务定:统计卡与工单表谁也不能少,全有或全无;侧栏的照片与公告可缺可降级,坏了就藏起来。

// 并行拉取:一次性取齐三路数据 async function loadAll() { const [orders, machines, notices] = await Promise.all([ fetchOrders(), fetch("/api/machines").then((r) => r.json()), fetch("/api/notices").then((r) => r.json()), ]); // 三路并行、一起等待;任何一路失败则整体进驳回 renderOrders(orders); renderMachineBar(machines); renderNotices(notices); } // 可降级的一路:坏了不拖垮全局 async function loadNotices() { try { const r = await fetch("/api/notices"); renderNotices(await r.json()); } catch { document.querySelector(".notices")?.remove(); // 拿不到就整块撤下 } }
异步排错的三个固定动作: - 网络面板看请求:发出去了吗、状态码多少、响应体长什么样 - 控制台看驳回:错误信息与堆栈落在哪一环 - 断点看时序:等待前后的变量值——多数"数据是空的"其实是 在等待完成前就读了数据(漏写等待关键字)

⚠️ 常见坑:在循环里逐个等待串行请求,十张图就等十倍时间。互不依赖就并行一起等;确需串行(后一个的参数来自前一个的结果)才逐个等待。另一个高频坑是漏写第二段等待,把承诺对象当数据用了——报错通常是"读不到字段",一眼看去像数据问题,其实是时序问题。

💡 关键直觉:看到等待关键字就默念"此处线程去干别的了,后面代码晚点再跑"。把它理解成书签而不是停机——页面没冻住,只是这一段的后续排到了兑现之后。

本节要点回顾

  • 单线程生存法:耗时活交后台、回调排队回线程,冻页面的同步等待是禁区;
  • 两种结局:承诺必走其一的兑现与驳回,链式挂载、末段必经收尾;
  • 等待语法:读序即执行序的现代首选,造轮子时用承诺构造器包老回调;
  • 两段式请求:先等响应头再读正文,状态码不成功要主动抛错;
  • 请求护栏:超时用中止控制器掐断、竞态只认最新请求、入址输入先编码;
  • 并行策略:互不依赖一起等、可降级单独兜,全有全无与部分降级按业务定。

外线接通了,下一节领新工装:现代语法与模块化,让行为班的代码说上通行语。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U