1. Promise.then 写多了之后的真实痛点先说一个我自己的感受前几年在维护一个老项目的时候代码里铺满了Promise.then一个业务动作动辄五六层链式调用。你盯着屏幕想改一个逻辑得先顺着.then往下捋半天捋到一半发现中间某个分支有个.catch再回头看外层还有个.catch整个流程像一团打了结的毛线。这不是说.then本身有问题而是链式写法在表达复杂业务流程的时候天然有几种让人难受的地方。第一个痛点是变量作用域被切碎。举个最常见的例子先取用户信息再根据用户信息去取订单最后把两个数据拼在一起返回。用.then写出来大概是这样的function getUserOrderInfo(userId) { let userInfo null; return getUser(userId) .then(user { userInfo user; return getOrderByUserId(user.id); }) .then(order { return { user: userInfo, order: order }; }); }问题就很明显userInfo不得不提升到外层变量因为第二个.then里要用到第一个.then的结果。业务在增长这种临时提升变量会越来越多每个变量都要小心管理它的赋值时机和生命周期稍不留神就会出现这个变量在这个分支里有没有被赋值的疑神疑鬼。第二个痛点是条件分支读起来非常别扭。如果某个请求是否发起 depend on 前一个请求的返回值.then里就得嵌套条件判断然后每个分支再返回 Promise缩进层级瞬间就起来了。你当然可以努努力保持扁平但代码会变成判断判断再判断的表达式拼盘可读性并没有真正改善。第三个痛点是错误处理的两难。.then链上挂一个.catch确实能接住整条链的错误但问题在于整条链。如果你只想知道中间某一步失败的具体原因.catch里接到的 error 是笼统的你还得自己去判断错误类型、错误阶段甚至得在每一步.then里手动打日志定位。错误上下文被链式结构稀释掉了。所以当async/await逐渐成为主流之后很多人第一反应是这玩意儿就是 Promise 的语法糖。表面上确实是但用久了你会发现它带来的不只是少写几个.then括号而是整个代码的思考方式变了——你终于可以用写同步代码的脑子去组织异步逻辑了。2. async/await 底层到底做了什么从 generator 到自动执行器很多人对async/await的理解停留在让异步代码看起来像同步代码这一层。这个说法没错但它忽略了一个关键问题JavaScript 是单线程语言它不可能真的把异步操作变成同步阻塞。所以async/await的本质不是消灭异步而是用同步的写法来组织和表达异步流程。要理解这一点得从它的底层实现说起。async/await的核心是建立在 Generator 函数和 Promise 之上的。Generator 函数有一个特性它可以在执行过程中暂停也可以从暂停的地方恢复。你可以把 Generator 理解成一个可中断的函数每次调用next()就往下执行一段遇到yield就停下来等待外部指令。function* stepByStep() { const a yield 第一步; const b yield 第二步; return a b; } const gen stepByStep(); console.log(gen.next()); // { value: 第一步, done: false } console.log(gen.next(1)); // { value: 第二步, done: false } console.log(gen.next(2)); // { value: 3, done: true }注意next()是可以传参数的这个参数会成为上一个yield表达式的返回值。这意味着外部可以通过喂值的方式把异步结果传给 Generator 内部继续执行。那和async/await有什么关系如果你把yield后面的值换成 Promise然后在外部写一个驱动函数当 Promise resolve 之后把结果通过next(result)传回 Generator让代码继续往下走这不就是一个异步流程的自动执行器吗function run(generator) { const iterator generator(); function handle(iteratorResult) { if (iteratorResult.done) return; const promise iteratorResult.value; promise.then(res { handle(iterator.next(res)); }); } handle(iterator.next()); }上面这个run函数就是一个极简的自动执行器。async/await在底层做的事情本质上就是这个把async函数体编译成一个 Generator 类似的执行流程每当遇到await就暂停函数的执行等 Promise resolve 后再自动恢复并把结果赋给await表达式。这里有个关键认知await后面的值不一定是 Promise。如果你写await 3JavaScript 会自动用Promise.resolve(3)包一层。如果await后面是一个 Thenable 对象带有then方法的对象它也会被当成 Promise 处理。这就是为什么你的任何函数返回值都可以直接await不会报错。但反过来说如果你的异步函数里漏掉了returnawait拿到的是undefined这往往是很多诡异 bug 的来源。async函数本身也有一个重要的行为特征它一定会返回一个 Promise。不管你在函数里return什么值外部拿到的都是包了一层 Promise 的结果。如果函数内部抛异常这个 Promise 会被 reject异常不会直接冒泡到外层调用栈。这一点很多人会忽略但它是后面讨论错误处理的基础。理解了这层机制你就能明白为什么说async/await不是简单的把.then换成await——它是在编译层面改变了代码的执行流程让异步结果可以被塞回到原本暂停的位置从而让你用顺序思维写异步逻辑。这个转变才是它真正优雅的地方。3. 错误处理范式对比.catch() 与 try/catch 的差异上面说了async函数一定会返回 Promise那么问题来了如果函数内部抛异常外部怎么感知答案是通过返回的这个 Promise 的 reject。这意味着你在任何一个async函数内部都可以用throw来主动制造一个失败信号外部用try/catch或者.catch()都能接到。3.1 错误捕获范围的差异这是两者最大的区别用.then链的时候错误处理通常长这样getUser(userId) .then(user getOrderByUserId(user.id)) .then(order render(order)) .catch(error { // 所有步骤的错误都汇聚到这里 handleError(error); });好处是简洁一个.catch管所有。坏处是你不知道错误发生在哪一步。用户拿不到、订单查不到、渲染出错全都在同一个error对象里。虽然你可以通过 error 的类型或者 message 去区分但实际项目里错误类型往往不够规范区分起来很痛苦。用try/catch的写法就自由多了try { const user await getUser(userId); const order await getOrderByUserId(user.id); render(order); } catch (error) { handleError(error); }如果只是想粗粒度处理它和.catch差不多。但如果你想做精细处理可以按步骤拆分let user; try { user await getUser(userId); } catch (error) { // 用户信息获取失败走降级逻辑 user getDefaultUser(); } let order; try { order await getOrderByUserId(user.id); } catch (error) { // 订单获取失败提示稍后重试 notifyRetry(); }这种每步都有自己的错误处理写法用.then也能做但写起来会嵌套得很深而且每一步的错误上下文要手动传给后续流程代码会被错误处理逻辑占掉大半篇幅。try/catch的语法结构天然支持一段代码一个错误处理块和同步代码的思维完全一致。3.2 await 运算符只能用于异步方法这类报错的成因与解决网上搜 async/await 相关热词await 运算符只能用于异步方法中这个错误出现的频率非常高。这个报错本身很简单就是你在一个没有async修饰的普通函数里用了await。但很多人困惑的点在于我的函数明明没有做异步操作为什么还要加async这就要回到上一节讲的机制await需要暂停当前函数的执行并且把后续代码放到一个恢复执行的状态里。如果当前函数不是async函数JavaScript 引擎根本不知道如何去暂停和恢复它。哪怕你await的是一个普通的值引擎也没有合法的挂起点可用。所以强制规则是await只能在async函数内使用。举一个常见的实际场景事件回调函数里用await// 错误示例 button.addEventListener(click, () { const data await fetchData(); // 报错await 只能在 async 函数中使用 updateUI(data); });改法很简单在回调函数前面加上asyncbutton.addEventListener(click, async () { const data await fetchData(); updateUI(data); });还有一个隐蔽的场景——forEach回调里用awaitasync function processList(items) { // 这样写不会报错但行为不符合预期 items.forEach(async (item) { await processOne(item); }); }这里forEach里的回调是一个独立的async函数它内部的await是被允许的。但由于forEach不会等待回调函数返回的 Promise所以外面的processList会在所有processOne执行完之前就返回了。这不是语法错误而是逻辑错误。这种语法合法但行为诡异的坑比直接报错更危险。4. 实战改造三个典型的 .then 迁移案例说了一堆原理和对比接下来用三个真实项目里最常见的场景演示一下从.then迁移到async/await的实际改造过程。这三个案例覆盖了链式请求、条件分支和并行协作三类典型问题基本能把日常开发中 80% 的异步场景说透。4.1 链式请求从多层嵌套到顺序直写第一个场景是三步连续接口调用先登录拿 token再用 token 拿用户信息最后用 uid 拿用户配置。.then链式写法function initProfile() { let token ; let uid ; return login() .then(res { token res.token; return fetchUserInfo(res.token); }) .then(user { uid user.uid; return fetchUserConfig(user.uid, token); }) .then(config { renderProfile(uid, config); }) .catch(err { showError(err.message); }); }注意这里为了把token传给第三个请求只能在链外用变量顶着。如果后面还要用uid也得再顶一个。链条越长顶在外面的中间变量越多。async/await改造后async function initProfile() { try { const loginRes await login(); const user await fetchUserInfo(loginRes.token); const config await fetchUserConfig(user.uid, loginRes.token); renderProfile(user.uid, config); } catch (err) { showError(err.message); } }改造前后的关键差异不是代码量少了多少而是**token和user.uid的作用域清晰了**。它们在async函数体内部后面的代码想看随时能看不用为了延续生命周期而把变量定义在函数体外围。这直接消除了这一类临时变量提升的隐患。4.2 条件分支复杂业务逻辑的救星第二个场景更复杂先请求一个活动配置然后根据配置里的type字段决定走哪条分支不同分支调不同接口最后汇总结果。.then链式写法很容易陷入嵌套困境function fetchActivity(activityId) { return getActivityConfig(activityId) .then(config { if (config.type game) { return getGameReward(config.rewardId) .then(reward { return { config: config, reward: reward, extra: null }; }); } else if (config.type lottery) { return getLotteryDraw(config.lotteryId) .then(draw { return { config: config, reward: null, extra: draw }; }); } return { config: config, reward: null, extra: null }; }); }这段代码的缩进已经很能说明问题了。如果getGameReward之后还要根据reward再做一次判断层级还会继续加深。async/await改造后async function fetchActivity(activityId) { const config await getActivityConfig(activityId); if (config.type game) { const reward await getGameReward(config.rewardId); return { config, reward, extra: null }; } if (config.type lottery) { const draw await getLotteryDraw(config.lotteryId); return { config, reward: null, extra: draw }; } return { config, reward: null, extra: null }; }注意这里我用了对象属性简写只是为了让代码更紧凑。重点在于每个分支内部可以继续用await拉平嵌套不再需要为了结构工整而把返回逻辑绕来绕去。代码的可读性直接和你写同步逻辑时的脑回路对齐了。4.3 并行请求别把 Promise.all 忘了在强调async/await好处的同时必须反手拍一个醒钟async/await是把异步变顺序但并行场景下你不能真的顺序 await。这是一个常见的迁移反模式。错误示范async function loadPage() { // 这两个请求没有依赖关系但被写成了串行 const newsList await fetchNews(); const userInfo await fetchUser(); render(newsList, userInfo); }fetchNews和fetchUser之间没有依赖关系但用上面的写法fetchUser必须等fetchNews完成后才发出去。如果两个接口都是 300ms串行就是 600ms并行只需要 300ms。页面性能直接翻倍变慢。正确的写法是用Promise.all保持并行再用await统一收结果async function loadPage() { const [newsList, userInfo] await Promise.all([ fetchNews(), fetchUser() ]); render(newsList, userInfo); }这里有个细节值得展开Promise.all是两败俱伤的策略只要有一个 Promise reject整个await就会抛异常另外几个 Promise 的结果即使已经返回也会被丢弃。如果你的场景是部分失败也要有兜底展示可以考虑Promise.allSettledasync function loadPage() { const [newsResult, userResult] await Promise.allSettled([ fetchNews(), fetchUser() ]); // 各自处理成功或失败 render(newsResult.status fulfilled ? newsResult.value : [], userResult.status fulfilled ? userResult.value : null); }Promise.allSettled会等所有 Promise 都 settle无论成功还是失败再返回结果每个结果对象上带有status字段用着很直观。在选择并行策略的时候先问自己一个问题这两个请求我是一个都不想少还是有则用之答案决定了用all还是allSettled。5. 迁移过程中最容易踩的坑从.then迁到async/await并不是简单地把箭头函数改成async函数就完事了。我在实际项目里踩过几个坑也帮同事排查过几个隐蔽 bug总结下来最值得警惕的是以下四类问题。5.1 漏掉 awaitPromise 对象被当成正常值传递这是新手最容易犯的错而且它不报错只会让你的日志和渲染结果变得无比诡异。async function loadData() { const data fetchData(); // 忘写 awaitdata 是 Promise 对象 console.log(data); // 打印出来是一个 Promise不是数据 render(data); // 渲染出来是 [object Promise] }为什么会这样因为fetchData()作为一个函数调用本身是同步执行的它返回的是一个 Promise 对象。这个 Promise 会进入微任务队列等着执行。而函数里的下一行代码console.log和render会立刻执行——这时候异步数据根本还没回来。如果你直接render(data)页面渲染的就是一个 Promise 对象。更麻烦的情况是后续代码里对这个假的 data取属性比如data.list得到的是undefined然后又是一路排查最终发现是漏了await。这个坑之所以隐蔽是因为很多代码在开发环境下看起来有时候正常有时候不正常。比如数据源有缓存第一次请求慢、第二次走缓存的时候Promise 的状态可能已经 settled 了某些操作立刻取到值误打误撞也能跑通。但性能不受控一旦网络慢一点问题就暴露了。我的建议是在代码审查里把await 是否齐全当成一个必查项。团队里如果用了 ESLint强烈建议开启require-await规则来辅助排查——虽然它只能检查async 函数里有没有用 await不能检查该 await 的地方漏没漏但至少能堵住一部分无意义 async 函数。5.2 await 在循环里的性能陷阱上面提到过forEach里用async回调不会等结果但还有一种反模式是把await直接写在for循环里造成不必要的串行。async function fetchBatch(urls) { const results []; for (const url of urls) { const data await fetch(url); results.push(data); } return results; }如果urls有 10 个每个请求 300ms这个函数要跑 3 秒。而这些请求相互之间没有任何依赖完全可以并发发起。一种改进方案是用Promise.all配合mapasync function fetchBatch(urls) { const results await Promise.all(urls.map(url fetch(url))); return results; }这个写法在数据量不大的时候是首选。但如果urls数量特别大比如几百上千个一次性并发发这么多请求可能把服务器打挂也可能触发浏览器的并发连接限制。这时候要的是限制并发数的批处理async function fetchBatch(urls, limit 5) { const results []; const queue [...urls]; const workers []; for (let i 0; i limit; i) { workers.push((async () { while (queue.length 0) { const url queue.shift(); const data await fetch(url); results.push(data); } })()); } await Promise.all(workers); return results; }这里的思路是开limit个工人每个工人从队列里取任务取到就await执行执行完再取下一个直到队列清空。这样把并发数稳定在可控范围内既不会串行拖慢速度也不会并发过高打爆服务。这种串并行结合的写法在.then时代写起来非常痛苦但用async/await加闭包实现就很自然。5.3 try/catch 范围太大错误被无差别吞掉try/catch的便利性也会带来一个副作用开发者容易把一大段逻辑全部包进一个try/catch里导致错误定位困难。看这个例子async function initAll() { try { const user await fetchUser(); const config await fetchConfig(user.id); const notifications await fetchNotifications(); renderAll(user, config, notifications); } catch (error) { console.error(初始化失败, error); showErrorPage(); } }表面看没问题但有一个隐患如果fetchConfig失败用户仍然是有登录信息的也许可以展示部分页面而不是整页报错。但上面这个写法把三个请求的结果绑定在一起其中一个失败就全盘报废。更隐蔽的问题是**renderAll本身出错也会被同一个catch捕获**。如果你的catch块看到错误就判断接口失败那是在误导排障方向——也许接口一切正常只是渲染逻辑写错了。一个实用的原则是**try/catch只包裹你关心错误的代码段。** 对于独立的异步请求步骤按需分开捕获对于不应被上层捕获的渲染错误让它保留在catch之外或者单独处理。5.4 错误吞噬async 回调里的异常不会自动冒泡这个坑主要出现在事件回调和定时器场景。当async函数作为回调被传入某些 API 时函数内部的异常不会再沿着调用栈向外传播而是被包进返回的 Promise 里。如果这个 Promise 没有被任何人await或.catch异常就成了一个被拒绝但无人处理的 Promise在浏览器控制台里表现为Unhandled Promise Rejection在 Node.js 里甚至可能直接让进程退出。setInterval(async () { const data await fetchData(); // 如果这里 reject // 后续代码不执行异常无人捕获 }, 5000);正确的姿势是需要及时处理回调函数里的异常避免让它进入无主状态setInterval(async () { try { const data await fetchData(); // 正常逻辑 } catch (error) { console.error(定时任务失败, error); } }, 5000);这是从.then迁移到async/await时特别容易忽略的一点。在.then链里如果你不写.catch浏览器顶多打个警告但在async回调的场景里异常处理的责任边界完全由你自己管理一旦管理不到位错误就静默消失了。排查线上问题的时候你最怕的不是报错了而是什么都没报但功能坏了。6. 我的取舍经验什么时候继续用 .then 反而更好写了这么多async/await的好处但我要诚实地告诉你它并不是万能的也不应该把项目里的.then全部清空。有些场景下.then的写法反而更合适强行迁移反而是过度设计。6.1 保持 .then 的几个场景第一个场景是单步异步操作、不需要任何后续逻辑的时候。比如一次打点上报reportEvent(click, { id: 123 }).catch(() {});这句用.then链或者纯.catch就够了没有必要为了它包一个async函数。反过来如果你一定要写async function reportClick(id) { await reportEvent(click, { id }); }多写了一个函数壳子没有任何收益。第二个场景是中间处理函数需要作为参数传递的时候。.then(handler)里的handler本身就是一个普通函数天然适配传参语义。如果你想保持handler内部有复杂的异步逻辑当然也可以把它定义成async函数传进去这没问题。但如果只是做一个简单的值转换.then一行就能表达清楚getUser(userId) .then(user user.name) .then(displayName renderName(displayName));改成await版本需要三个语句信息量没有增加反而多了函数栈的切换。第三个场景是利用then的自动展开特性的时候。Promise.resolve().then(() promiseA).then(() promiseB)这种写法.then回调的返回值如果是 Promise它会被扁平化处理等待其完成后继续。这在做链式管道操作pipe时非常顺滑每个环节都是纯函数职责单一、易于测试loadData() .then(validate) .then(transform) .then(save);用async/await写需要额外的临时变量来承接每一步的结果反而把简单的管道拆散了。这类数据管道风格的代码.then的表达力更强。6.2 团队协作与代码审查中的注意事项如果团队决定逐步向async/await迁移我有几个实操层面的建议。第一个建议是别搞一刀切式的批量重写。一次提交几百个文件把.then全改成await评审人根本看不过来而且大量改动会让 git blame 失去意义。更稳妥的做法是改到哪算哪——在你实际编辑的文件里把周围相关的异步链顺手改掉新建代码一律用async/await。这种方式迁移周期长但风险和噪音都小。第二个建议是在错误处理策略上达成统一。.then时代的.catch往往集中在链尾async/await时代的try/catch粒度则更灵活。团队里如果没有约定的错误处理范式很容易出现一个人写整段一个大 try/catch另一个人写每步一个小 try/catch代码风格割裂严重。我建议在小范围内先定一个规则默认用细分 try/catch 处理用户可感知的错误用统一的全局错误上报兜底。具体粒度可以根据业务复杂度来调。第三个建议是理解并善用工具的辅助能力。ESLint 的require-await、no-async-promise-executor这些规则还有 TypeScript 编译层的useUnknownInCatchVariables等选项都能在代码审查之前自动拦掉一批低级问题。工具不是万能的但它能把你从人肉 review 漏没漏 await这件事里解放出来让你把注意力放在真正有价值的设计审查上。6.3 从使用语法到理解模型最后分享一点个人的感悟。我在刚学会async/await的时候觉得它是把异步代码变好看的魔法。用了两年之后我意识到真正重要的不是语法本身而是你对异步执行模型的理解。代码里最危险的不是复杂语法带来的报错而是看起来像同步的异步逻辑带来的误判。await把异步流程伪装成了顺序流程如果你脑子里忘了这里其实是异步的很容易写出以为拿到数据了其实没有的代码。反过来如果你已经理解了事件循环、微任务队列、Promise 状态机这些底层概念那么.then还是await只是表达方式的选择你随时可以在两种写法之间切换而不出 bug。这也是我建议所有前端开发者在掌握async/await之后回头再去读一遍 Generator 和 Promise 原理的原因。你不需要手写一个自动执行器但了解它如何工作可以帮助你在遇到那些稀奇古怪的异步 bug 时快速定位问题的本质。说到底技术选型没有银弹。async/await让代码更优雅但优雅的前提是你真正理解了它背后的运行机制。工具永远是辅助对问题的理解才是根基。
企业数字化 ERP 产品动态
相关推荐
MATLAB SVM参数寻优与交叉验证实战:固定分层4折与网格搜索流水线 简介:这份资源面向机器学习入门者与需要做模型调参的工程人员,聚焦支持向量机(SVM)的参数寻优与交叉验证实践。内容围绕核函数选择、惩罚参数C与RBF核参数γ的调优展开,通过K折交叉验证遍历不同参数组合,帮… · 2026/9/24 21:23:28
WD5081电动车,仪器仪表应用单片异步降压芯片 WD5081 是 WDSemi 微电半导体的6.5‑90V 单片异步 Buck 降压芯片,SOT23‑6 小封装,持续输出 1A、峰值 3.5A,输出 3‑30V 可调,具备 100V 瞬时浪涌耐压、超低待机功耗、全套自恢复保护,专门解决高压直流中小功率辅助供电… · 2026/9/24 21:23:21
Python人口普查数据可视化:七次普查省级面板与动态图表实战 简介:这份Python人口普查数据可视化项目源码,面向高校学生、数据分析初学者及需要完成期末大作业或课程设计的开发者,围绕1953至2021年七次全国人口普查数据,解决各省人口数量变化趋势的统计分析与图表呈现问题。压缩包共10个文件… · 2026/9/24 21:23:21
PaddleSpeech FastSpeech2 多说话人声学模型微调实战:基于预训练权重定制你自己的语音合成模型 人工智能语音音频NLP媒体生成 【免费下载链接】PaddleSpeech Easy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation … · 2026/9/25 3:29:59
TensorFlow中dtensor导入失败的根因分析与分版本修复方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 3:29:59
Mopidy-File 扩展完全解析:浏览本地音乐档案的机制与配置 音视频后端 【免费下载链接】mopidy Mopidy is an extensible music server written in Python 项目地址: https://gitcode.com/gh_mirrors/mo/mopidy 点击查看 免费下载 Mopidy-File 是 Mopidy 内置并默认启用的文件后端扩展,它让你可以直接通过 file:… · 2026/9/25 3:29:59
为什么地址是0x13?深入解析ps2-controller背后PS2手柄I2C通信原理 为什么地址是0x13?深入解析ps2-controller背后PS2手柄I2C通信原理 【免费下载链接】ps2-controller 源师兄扩展项目: PS2 | 由源师兄组织创建 项目地址: https://gitcode.com/yuanshixiong/ps2-controller
在 ps2-controller 这款源师兄出品的 PS2 手柄 I2C … · 2026/9/25 3:29:40
华为云与腾讯云怎么选?从云原生到信创的全场景决策指南 前阵子有个朋友找我做选型咨询,他们要做一个面向连锁餐饮企业的数据分析中台,既要卖软件又要做交付,甲方那边点名要“信创”。朋友打开两个网页问我:华为云和腾讯云到底差在哪?参数表我看得头晕,你直接告诉… · 2026/9/25 3:29:40
PCI简易通讯控制器黄标修复全指南 1. 黄色感叹号不是故障,而是Windows在向你发求救信号“PCI简易通讯控制器”这个名称听起来很陌生,但只要你打开设备管理器,展开“系统设备”或“其他设备”,大概率会看到它——一个带着黄色感叹号的灰色图标,名字里带着… · 2026/9/25 3:29:34
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37