Dev KnowledgeV2.0
输入关键词开始搜索

    前端运行时与框架速记

    按组件状态、构建加载、网络与实时性能整理前端运行时的易混边界。

    类型
    速记知识
    适合读者
    希望快速复习前端工程行为与诊断方法的开发者
    最近核验

    这里仅记录框架、浏览器运行时和工程加载边界。纯 CSS 属性已经归入独立的 CSS 主题,本地命令、进程和构建脚本归入 工程工具与系统。

    组件状态与数据边界

    当前页面与业务完成状态要分开

    多步工作台中,“正在第几页”“临时选中了什么”和“业务实际完成了什么”是不同状态。导航、步骤条与帮助应共享有序阶段定义,完成标记从有效结果推导,不能因访问过页面就点亮。

    • 版本边界:审阅或批准关联具体内容与输入版本;内容改变后,保留历史并重新确认。整包交付还要关联对象集合,集合变化可能只需重新打包,不必撤销未变内容的有效审阅。
    • 异步边界:导出开始时固定内容、范围和版本,文件生成后再次核对;期间改变则拒绝标记成功。仅在打包前校验不足以排除等待期间的变化。
    • 验证:2026-10-27 用聚焦状态测试覆盖集合改变、切换上下文、内容重做和审阅记录改变,并在浏览器复核新建、确认、审阅、实际下载及恢复。该证据支持界面一致性策略,不代表客户端变化键具备安全签名或服务端并发控制能力。

    input 与 change 的触发时机不同

    需要边输入边更新预览、计算或筛选时,监听 input;需要在用户提交选择或结束编辑后处理时,再考虑 change。change 不保证每次值变化都触发,具体时机与控件类型和交互方式有关,不能统一理解为“失焦事件”。

    • 控件区别:文本类输入通常在内容变更并失焦后触发 change;复选框在勾选或取消时触发,单选框只在选中时触发,原生选择器在提交选择时触发。
    • 实时派生值:使用 input 更新计算结果时,保留尚未完成的输入状态,例如空字符串或正在输入的小数;不要每次重建正在编辑的控件,导致光标、焦点或小数中间态丢失。可只更新依赖结果,在提交时执行完整校验。
    • 程序赋值边界:直接用 JavaScript 修改元素的 value 不会自动触发 input;程序更新应明确调用状态逻辑。框架的同名事件封装还需核对框架语义,不能直接套用原生事件时机。
    • 验证:2026-10-24 在本地浏览器复核数值输入与派生摘要,改为响应 input 后,修改有效金额会立即重算;计算表单仅更新结果区时,可保留输入焦点。此观察不代表所有输入法和浏览器组合都已验证。
    • 来源:MDN input 事件、MDN change 事件。

    启动读取失败不能当作持久化空数据

    从持久化存储读取失败,只能说明尚未拿到可信快照,不能证明用户原来没有数据。若界面先用默认值继续运行,后续写入重试成功可能把这些默认值覆盖到尚未读出的旧数据上。

    • 处理方式:快照不可用期间保留内存操作、阻止普通持久化写入,明确提示刷新恢复;或者实现完整的重新读取与状态协调,再恢复保存。不能只给读取错误弹提示,同时放任默认值继续写回。
    • 恢复边界:用户明确要求全量替换数据,且替换已成功提交时,可以把新内容作为可信快照;局部写入成功不等于旧快照已经恢复。
    • 迁移边界:缺少新字段不能单独证明快照来自旧格式。用独立格式标记区分“尚未迁移”与“已迁移但丢失数据段”;仅前者可以初始化默认段,后者应拒绝、保留现场或恢复可信备份,避免把损坏数据当成首次初始化而重复创建资源。已存在的数据段也不能在每次启动时重置为默认值。
    • 验证补充:2026-10-25 用旧格式缺段、新格式缺段、完整新格式重载三组样例,核对一次性迁移、损坏拒绝与余额不变。验证针对持久化契约,不依赖具体业务内容。
    • 验证:2026-10-15 用 fake-indexeddb 保存自定义配置,再让首次打开失败、随后恢复;确认内存中的默认配置没有覆盖数据库旧值。该验证覆盖存储状态与调用顺序,不证明所有浏览器故障都具有相同行为。

    账号隔离也要覆盖未同步的持久化数据

    取消旧请求或检查会话代数,只能防止迟到响应回填。已登录时产生的离线改动还必须保存所属账号,否则下一次登录可能把它当成游客数据上传给另一个账号。

    • 处理方式:区分真正的游客数据与某个账号的待同步数据;后者按账号保存和恢复。重试时核对创建改动时的归属,不能只读取当前登录身份。
    • 验证:2026-10-18 以模拟账号 A/B 和本地存储复现:A 加载失败后新增的数据落入无归属的共享键,B 登录后被自动迁移到 B。原有会话守卫仍正常生效,说明请求隔离与持久化归属需要分别验证。

    分页列表不能代表跨页选择的有效范围

    当前页没有某个已选 ID,不能说明对应记录已经失效。刷新跨页多选表格时,应单独按已选 ID 查询仍然符合条件的记录,再清理失效选择;不能直接拿选择集合与当前页 ID 求交集。

    • 例子:已选 a、b、c,当前页只显示 a,而 b 在另一页、c 已被删除。独立查询应保留 a、b;按当前页清理会误删 b。
    • 失败边界:查询失败不能当作“全部失效”。依赖查询结果清理选择时,应等待查询成功再更新;提交前后的数据还可能变化,服务端执行校验仍需保留。
    • 页码边界:数据减少后还要检查 offset >= total。越界时回退并重查最后一页,零基偏移为 max(0, ceil(total / limit) - 1) * limit;否则可能出现空列表,而总数不足一页又让翻页按钮消失。补查响应也要核对当前筛选和请求序号,不能覆盖较新的查询。
    • 来源:2026-09-09 通过本地 Vue 状态测试验证跨页选择保留、失效记录移除、查询拒绝时保留原选择,并对照批量查询与提交校验源码核验。
    • 浏览器复核:2026-09-09 在本地 Chrome 验证 60 条数据减少到 45 条后,位于第二页的列表正确返回有效页;另用 Vue 状态测试延迟页码补查响应,验证它不会覆盖新的筛选结果。

    编辑器的 dirty 状态应与保存基线比较

    关闭保护要判断当前缓冲区是否仍与最近保存的内容不同,而不是在第一次输入后永久标记为已修改。

    • 用法:文本编辑器可保存 savedText,用 computed(() => text.value !== savedText.value) 派生 dirty;保存或重新加载成功后更新基线。
    • 边界:撤销或重新输入原文后应回到未修改;如果产品忽略格式、空白或字段顺序,需要另外定义语义比较规则。文本相等只适用于以原文为保存单位的编辑器。
    • 导入边界:导入文件只是替换编辑缓冲区,没有完成持久化,不能同时更新保存基线;否则 dirty 会变成 false,关闭时可能丢弃尚未保存的导入内容。只有成功保存或明确重新加载持久化内容时才移动基线。
    • 写入拒绝:新增容量限制时,要先核对实际保存单位;整张列表共用一个存储键时,单值上限就是整张列表的容量上限。异步写入被拒后,仅弹提示却仍关闭编辑器或移动保存基线,会留下“显示新内容、持久化仍是旧内容”的分裂状态。2026-10-18 以真实编辑状态逻辑和模拟存储桥验证了该场景,未操作用户数据。
    • 只读边界:读取草稿的 getter 不应为了方便而写入空对象。若 dirty 通过快照比较派生,这个空对象也可能被当成真实编辑,在合并时覆盖其他页面较新的草稿。真正编辑时再建立草稿;2026-10-24 用序列化前后对比和独立状态测试验证读取不改变存档。
    • 来源:2026-09-09 本地 Vue 3.5.8 响应式实验验证原文、修改、恢复原文三个阶段分别为 false → true → false;浏览器同时检查关闭确认的取消操作保留编辑缓冲区。
    • 追加验证:2026-10-13 用本地浏览器验证导入后更新基线会绕过未保存关闭保护,并以独立 Node.js 示例确认导入内容应继续与原保存内容比较。

    localStorage 多标签页写回不会自动合并

    同源页面可以共享 localStorage,但读取、计算、再 setItem 不构成事务。旧页面把整个快照写回时,可能抹掉另一页新增的独立记录;HTML 标准也要求作者不要假定存在锁。Web Storage 共享状态说明

    • 降低覆盖风险:保留本页保存基线,写入前读取最新存档,按“基线、本页实际变更、最新存档”合并。不同记录可按稳定身份合并;同一草稿冲突需明确定义策略,收藏新增和删除也应按实际变更处理。
    • 边界:三方合并是应用策略,不会把 localStorage 变成比较并交换或加锁事务。真正同时读写仍可能竞争;需要严格并发保证时,应另选事务或协调机制,不能把收到 storage 事件当成互斥锁。
    • 验证:2026-10-24 用独立状态测试及两个浏览器页面顺序交错操作验证:旧页面只记录浏览事件时,另一页新增的记录仍保留。该证据不涵盖所有真正同时写入的时序。

    IndexedDB 的版本核对与写入应放在同一事务

    本机编辑器可以在同一个 readwrite 事务中读取记录版本、比较编辑时的预期版本,再写入新内容并递增版本;发现不一致就中止事务,保留本页编辑缓冲区,让用户重新读取或另存副本。把核对和写入拆成两个事务,仍会留下竞争窗口。MDN IDBTransaction

    • 成功时机:单个请求的 success 不等于整个事务已经提交;界面应等事务的 complete 再报告保存成功,并处理 abort 和配额等错误。
    • 异步边界:没有待处理请求时事务会自动提交。图片解码、网络请求等外部异步准备应放在事务之前,避免等待结束后向已失活的事务追加操作。
    • 验证范围:2026-11-03 两个浏览器页面先后编辑同一记录,旧版本保存被拒,另存后两份内容均保留;该证据是本机冲突处理,不是跨设备同步或所有并发时序的压力测试。

    Object.keys 不能代替对象类型校验

    Object.keys 会先把参数转换为对象。Object.keys(7)、Object.keys(true) 都返回空数组,所以“所有键都合法”的循环可能误把标量当成空字典;字符串则返回字符索引,null 和 undefined 会抛出 TypeError。ECMAScript 的 Object.keys 算法

    • 处理方式:解析外部 JSON 或本地存档时,先检查 value !== null && typeof value === 'object' && !Array.isArray(value),再验证必需字段、允许的键和值的范围。
    • 边界:上述检查只排除标量、空值和数组,不保证对象属于某个具体业务结构,也不等于“普通对象”或完整 schema 校验。键列表为空不代表记录合法。
    • 验证:2026-10-24 用独立 Node.js 调用核验数字、布尔值、字符串、空值与未定义值的行为,并对照语言规范。

    对象展开不能隔离表单的嵌套草稿

    { ...template } 只复制第一层属性,嵌套对象和数组仍与模板共享引用。在表单中修改数组成员,即使最后选择放弃,也可能已经改动默认模板,影响下一次新建。

    const template = { rules: [{ required: true }] };
    const draft = { ...template };
    draft.rules[0].required = false;
    console.log(template.rules[0].required); // false
    • 用法:对于普通、可结构化克隆的数据,打开编辑器时用 structuredClone(template) 建立独立草稿;只有保存成功才把结果交回持久化层或父状态。不要把函数、DOM 或 Vue Proxy 直接当成可克隆的数据,应先明确普通数据边界。
    • 验证:2026-10-13 独立 Node.js 断言确认展开对象共享嵌套引用,而对同一普通数据使用 structuredClone 后,修改草稿不会改变模板;另用本地浏览器复核“修改、放弃、再次新建”的完整交互。

    Vue computed 不会因时间经过自动重算

    计算属性缓存依赖响应式数据;读取 Date.now() 或 new Date(),不会把系统时间变成响应式依赖。对已经缓存的结果,单纯推进时钟不会保证它重新计算。

    const now = ref(Date.now());
    const deadline = ref(Date.now() + 60_000);
    const expired = computed(() => now.value >= deadline.value);
    
    // 由现有调度器或明确的刷新事件更新真正的响应式依赖。
    now.value = Date.now();
    • 适合场景:倒计时、过期提示、按当前时间筛选记录,以及依赖时间边界的展示状态。
    • 边界:改成普通函数也不会让界面随时间自动刷新,仍需要触发调用或渲染。时间变化只说明判断边界变化,不代表缓存中的外部数据已经更新;数据也有时效要求时,应同时明确重取与失效规则。
    • 来源:Vue 计算属性缓存与方法。2026-10-11 用假时钟核验:依赖数组不变时,跨过截止时间后计算属性仍返回缓存筛选结果。

    上下文菜单的目标应绑定触发行

    上下文菜单目标与当前选中项可以不同:正在阅读 A 时,在 B 上右键,查看区可以继续显示 A,而菜单动作必须作用于 B。

    • 使用触发记录的稳定 ID 或对应行的事件参数执行操作;可见的省略号与右键入口复用同一套命令,避免各自维护一份逻辑。
    • 每次打开先关闭旧菜单,滚动时收起菜单;键盘打开后将焦点移到菜单项,不能只显示菜单却把焦点留在列表中。
    • 来源:2026-09-09 本地 Vue 界面验证:在未选中行上右键,当前查看对象保持不变,确认弹窗使用右键行的名称;滚轮滚动关闭菜单,列表记录未被修改。

    条件 class 的判断数据不必来自当前元素

    动态 class 加在哪个元素上,与它应由哪个数据决定,是两个不同问题。样式可能施加在说明文字上,但触发原因来自相邻标题、父组件状态或整体布局模式。

    • 记忆点:先说明 class 的视觉意图,再选择判断条件;不要仅因为 class 加在某个节点上,就机械地改为读取该节点展示的数据。若代码容易误读,应把 predicate 或 class 命名成原因,例如 should-align-with-heading,而不是只描述目标元素。
    • 来源:Vue Class 与 Style 绑定:对象值的真假决定 class 是否存在。

    HTML 字符引用不在 <style> 中展开

    &#x20; 在普通 HTML 文本或属性中解码为 U+0020 空格;<style> 是 raw text 元素,其中不能用 HTML 字符引用代替 CSS 语法空格。源码字符经过 HTML 解析、JSON 解析或框架模板转换后,CSS 只能处理最终进入 DOM 的真实字符。

    组件库与框架适配

    Element Plus 与 Ant Design 的组件不能跨框架直接替换

    组件库需要匹配应用的渲染与状态模型。Element Plus 面向 Vue 3;antd 是 React 组件库。安装包成功或能够复用 CSS,不代表 React 可以直接渲染 Vue 组件。

    • 选择方式:现有 React 项目优先评估 React 组件库;现有 Vue 3 项目可评估 Element Plus。若必须使用另一框架的库,需要迁移界面或增加明确的框架集成层,同时处理挂载、生命周期、属性和事件,而非只改导入名称。
    • 设计边界:组件库能统一控件与交互状态,信息层级、页面密度和业务任务仍需产品设计;换库本身不保证页面美观。
    • 来源:Element Plus 官方安装示例显式加载 Vue 3;Ant Design 官方介绍说明其 React 组件定位。核验于 2026-10-24。

    组件语义与可访问性

    Vue 的属性透传不保证标注到内部交互控件

    单根组件会把未声明的属性自动加到根元素。如果根元素只是布局容器,实际的 input、button 或 role="combobox" 在内部,把 aria-label 写在组件调用处可能只给外层容器命名,内部控件仍然没有正确的可访问名称。

    • 实现选择:用明确的 ariaLabel / ariaLabelledby prop 绑定到实际控件;或关闭自动继承后,有选择地把 $attrs 转发到控件。布局用的 class、style 与控件属性要分别确定目标,避免整包移动后改变布局。
    • 标签与值:选择器名称应表达“选择什么”,例如“周期”;当前值是“周线”。有可见标签时可使用 aria-labelledby 建立关联;aria-label 也应与可见标签保持一致。
    • 验证:检查浏览器可访问性树中控件的名称,用角色和名称定位控件,并验证方向键选择、Enter 确认、Escape 关闭和焦点恢复;只看到外层 DOM 上有属性不足以证明控件已标注。
    • 来源:Vue 属性透传、WAI-ARIA Combobox 模式。2026-10-11 通过组件测试与 Chrome 的 DOM / 可访问性观察核验内部控件命名和键盘交互。

    构建与加载

    Vue SFC 的编译与类型检查是两条链路

    Vite 转换 TypeScript 和 Vue 单文件组件,并不等于执行类型检查;SFC 的脚本、模板、props 和事件类型应通过 vue-tsc 校验,并明确接入开发或 CI 流程。

    • 关键命令:已有正确配置时运行 pnpm exec vue-tsc --noEmit。能手工运行,不等于项目构建或 CI 已经自动调用。
    • 通配声明边界:declare module '*.vue' 中的宽泛 DefineComponent 声明,不能单独证明 vue-tsc 会丢失 SFC 的 props/emits 类型;它能够从实际 SFC 生成类型信息。应以具体调用的诊断结果确认边界,不机械地为每个组件手写另一份声明。
    • 验证:2026-10-13 用两个通用 Vue 组件及同样的通配声明核验:子组件声明数字 prop 和数字事件参数,父组件传字符串 prop、使用接收字符串的事件处理器,vue-tsc 分别报告两处 TS2322。
    • 来源:Vue 官方 TypeScript 概览说明 Vite 转换与类型检查的职责,以及 vue-tsc 对 SFC 的支持。

    astro dev 与 astro preview 的内容来源不同

    astro dev 运行开发服务器,监听 src/ 并通过 HMR 更新页面;astro preview 只提供最近一次 astro build 生成的静态目录(默认 dist/),修改 Markdown 或源码后不会自动重新构建。

    • 排查旧页面:用 lsof -nP -iTCP:<port> -sTCP:LISTEN 找 PID,再用 ps -p <pid> -o pid=,ppid=,lstart=,etime=,command= 和 lsof -a -p <pid> -d cwd 确认命令、启动时间和工作目录;同时比较 dist/ 与源文件时间,并直接请求新增路由验证。
    • 记忆点:本地 URL 能访问只证明端口仍有进程监听,不证明它读取的是最新源码。需要边编辑边看时运行 astro dev;需要验证生产构建时先 astro build,再运行 astro preview。
    • 后台进程:Astro 7 的 background 模式会把 PID、端口和日志写入 .astro/dev.json 或 .astro/preview.json;检测到 AI 编码代理时可能自动启用。原终端关闭后进程仍可存活,父 PID 可能变为 1,应结合监听端口和 JSON 中的 PID 判断,不能只看是否还有终端窗口。
    • 来源:Astro CLI:dev、build、preview 与 background;结论也由本地 Astro 7.2.9 进程命令、锁文件、构建时间和 HTTP 404 响应复核。

    defineAsyncComponent 与路由体积预算

    异步组件被拆成独立 chunk,不等于它不属于页面首次进入成本;如果异步组件在父页面首帧就被渲染,Vue 会立即调用 loader 并下载该 chunk。

    • 适合场景:根据 Vite manifest.json 编写首屏或路由体积预算。
    • 记忆点:Vite manifest 分别记录静态 imports 和 dynamicImports。只遍历 imports 会漏掉首帧立即渲染的 defineAsyncComponent;递归计入所有 dynamicImports 又会把真正由点击等操作延迟的功能算进去。
    • 验证方法:预算配置显式标记“首帧必触发”的动态入口并纳入依赖闭包,再用冷缓存浏览器网络记录核对实际请求。是否属于首次成本取决于组件何时渲染,而不是只看它是否使用 import()。
    • 来源:Vue 异步组件、Vite manifest 结构。

    TypeScript 类型与运行时校验

    as unknown as T 是双重类型断言,不是数据转换

    value as unknown as T 先把表达式视为 unknown,再要求编译器把它视为 T。当原类型与目标类型缺少足够的兼容关系、直接断言被拒绝时,这种写法可以绕过检查,但不会验证字段、补齐属性或转换实际值。

    const value = '42' as unknown as number;
    console.log(typeof value); // 运行时仍然是 string
    • 删除边界:只删中间的 as unknown,可能重新触发类型不兼容错误;全部删除,则恢复原表达式的静态类型。应先确认声明与真实数据为什么不同。
    • 替代方式:对于 IPC、接口或测试替身的独立数据协议,可以用运行时 Schema 校验并推导类型。测试如果需要核对请求是否原样传递,应保留原始字段做完整比较,避免通过补默认值或转换掩盖传输问题。
    • 来源:TypeScript 官方类型断言说明,2026-10-13 核验;类型断言在编译时移除,不包含运行时检查。

    JavaScript 表达式

    比较运算符先于相等运算符分组

    <、>、<=、>= 的优先级高于 ===、!==。例如 flag !== count > 0 解析为 flag !== (count > 0),先比较数值,再比较两个布尔值;不是 (flag !== count) > 0。

    • 可读性:显式写成 flag !== (count > 0) 能表达分组意图,但在这个表达式中加括号不改变行为。优先级决定语法分组,不等于所有操作数都按优先级顺序求值。
    • 验证:2026-10-14 用 TypeScript 解析器确认外层节点为 !==、右侧节点为 >;在 Node.js v26.0.0 中遍历布尔 flag 与 count 为 0、1、2 的六种组合,确认原式与显式括号形式结果一致。

    JavaScript 字符串

    String.replace 的替换字符串也有模板语法

    text.replace(search, replacement) 中,即使 search 是普通字符串,字符串形式的 replacement 也会解释 $&(匹配内容)、$$(一个美元符号)、美元符号加反引号(匹配前内容)和 $'(匹配后内容);正则捕获还支持对应的组引用。把压缩代码或任意文本直接作为替换字符串,可能意外插入原文。

    const payload = '$&';
    'left TOKEN right'.replace('TOKEN', payload);       // 'left TOKEN right'
    'left TOKEN right'.replace('TOKEN', () => payload); // 'left $& right'
    • 原样插入:使用替换函数 () => payload,其返回值作为文本拼接,不再经过替换模板展开。它不提供 HTML 转义或其他注入防护,输出场景仍需单独处理。
    • 来源与验证:ECMAScript replace 与 GetSubstitution 区分函数替换与字符串模板;2026-10-24 用 Node.js 验证上述行为,并通过生成脚本的语法检查发现和修正实际拼接故障。

    JavaScript 数据与序列化

    随机抽取不保证不重复,轮换需要单独维护状态

    Math.random() 只提供取值,不提供已抽历史。每次用 floor(random() * length) 独立抽索引,完全可能连续得到同一项;「随机」「下一项不同」「一轮内不重复」是三种不同要求。MDN Math.random

    • 不连续重复:先排除当前稳定标识,再从剩余候选抽取。候选不足两项时应返回明确结果,不靠反复重试随机碰运气。
    • 一轮内不重复:维护已抽集合或洗牌队列;优先从未抽集合取,穷尽后重开一轮,并继续排除当前项。筛选条件变化时明确历史是否隔离或重置,刷新持久化策略也要单独约定。
    • 固定部分选择:先用硬条件过滤候选,再判断剩余数量和抽取;给匹配项加分或提高权重不保证固定项始终不变。过滤后无解应保留原状态并解释原因,不能悄悄放松条件。2026-10-26 用独立 a/b 候选实验覆盖随机端点与空集合。
    • 验证:注入随机函数,稳定检查索引端点、完整一轮、重开边界、唯一候选和无候选;不能只点两次恰好不同就断言已保证。2026-10-24 用独立 a/b/c 状态实验核验首轮覆盖与跨轮不连续重复。

    序列化时的条件判断:空数组也是真值

    序列化是把数据转换成适合保存或传输的表示,JavaScript 中常见的是用 JSON.stringify 生成 JSON 文本。应用也可能先把运行时模型转换成普通对象,再生成 JSON;在构造对象时写的条件决定哪些字段会被输出,并不等于执行数据校验。

    const items = [];
    const output = {
      ...(items ? { items } : {}),
    };
    JSON.stringify(output); // '{"items":[]}'
    • 条件展开:三元表达式选择一个对象,... 再把该对象的字段加入结果;选中 {} 就不添加字段。
    • 空不等于假:Boolean([]) 为 true,空数组会进入真分支;undefined 为 false,上述条件在字段未定义时选择空对象。
    • 判断意图:if (items) 检查真值,不检查数组是否有成员。若协议只输出非空数组,需要明确判断数组类型和 length > 0;若空数组本身表示有意义的空结果,就应保留。
    • 验证:2026-10-09 用独立 Node.js 示例检查 undefined、空数组、非空数组三种输入,条件展开后分别得到字段省略、字段值为 []、字段值为非空数组。

    JavaScript 调用与作用域

    逻辑或与三元表达式的优先级和短路

    a || b ? x : y 等价于 (a || b) ? x : y:|| 的优先级高于条件运算符 ?:,整个或表达式是真值时返回 x,否则返回 y。换行和缩进不会改变结合方式,显式括号能帮助阅读。

    const status = (isReady || checkReady()) ? 'ready' : 'pending';
    • 短路:isReady 为真值时不会调用 checkReady();只有前者为假值才计算后者。三元表达式也只执行选中的结果分支。
    • 边界:|| 本身返回操作数,不保证返回 boolean;三元条件再按真值判断。例如 [] 是真值,不能借此判断数组是否为空。
    • 验证:2026-10-29 用独立 Node.js 示例检查两个布尔条件的 (true, false)、(false, true)、(false, false) 组合,结果分别进入真、真、假分支,右侧检查的调用次数分别为 0、1、1。

    把函数作为参数:回调的实参由接收方提供

    run(callback) 传入的是函数本身;run 的实现决定何时调用它、传入哪些实参,以及是否返回它的结果。函数有两个形参,不代表外层 run 也接收两个参数。

    async function withResource(execute) {
        const resource = { value: 'ready' };
        const readState = () => resource.value;
        return execute(resource, readState);
    }
    
    const suffix = '!';
    const result = await withResource((resource, readState) => {
        return Promise.resolve(readState() + suffix);
    });
    // result === 'ready!'
    • 参数来源:resource 和 readState 由 withResource 调用回调时提供;suffix 来自回调定义处的外层作用域,这就是闭包。
    • 返回链:回调返回 Promise,外层 async 函数返回它,调用方 await 得到最终值。箭头函数的表达式体会隐式返回表达式;改成花括号函数体时,需要明确写 return 才会传出该结果。
    • 执行时机:定义或传递函数不等于执行函数体;这里真正触发执行的是 execute(resource, readState)。回调也不天然意味着异步或另开线程。
    • 验证:2026-10-13 独立 Node.js 断言确认定义回调时无执行记录,包装器调用后按准备、回调顺序执行,最终 await 收到 ready!。

    对象方法与外层函数同名,不代表裸调用就是递归

    对象方法简写中的方法名是属性名;方法体里的裸标识符按词法作用域查找,不会因为属性同名就自动指向该方法。

    function load(value) {
        return `outer:${value}`;
    }
    
    const worker = {
        load(value) {
            return load(value);
        },
    };
    
    worker.load('demo'); // 'outer:demo'

    这里调用的是外层函数。this.load(value) 则通过接收者读取方法;同名复用容易让读者误判,包装方法可以保留公开名称,并给内部函数使用更明确的名字。上述方法简写与外层函数的解析关系已通过本地 Node.js 聚焦运行验证。

    异步任务与错误处理

    单线程、事件循环、await 与并发上限的完整机制见单线程 JavaScript 如何实现异步并发。下列速记聚焦返回值和错误传播。

    自定义 Error 类负责携带错误语义

    自定义错误类可以很简单:它统一构造稳定的 code、错误名称和必要的上下文字段,让捕获方判断发生了什么。重试、终止或界面提示等处理策略由调用方决定,不需要都写进错误类。

    class LookupError extends Error {
      code = 'LOOKUP_MISSING';
    
      constructor(key) {
        super(`Missing key: ${key}`);
        this.name = 'LookupError';
        this.key = key;
      }
    }
    • 识别依据:code 是应用自行约定的字段,普通 new Error() 不会自动带业务错误码。使用稳定的 code 和上下文字段比解析可能改写的 message 更可靠;同一种“对象不存在”还可能需要结合已有记录才能判断原因。
    • 继承的价值:仍可正常 throw/catch,使用 Error 的 message、运行时提供的 stack,以及同一 JavaScript realm 内的 instanceof 判断。类主要保证构造方式和字段约定,普通 Error 加字段或工厂函数也能实现同类协议。
    • 封装边界:错误定义可以是内部实现,独立文件只是组织选择。其他内部文件要导入定义时需要模块导出,但不代表必须额外公开 package.json 的包子路径;是否提供公开入口应由包外调用需求决定。调用方只消费 code 与普通字段时,无需依赖具体错误类。
    • 传输边界:显式发送 { code, message, key } 等普通数据;JSON 序列化不会保留自定义类原型,接收方不能假设对象仍能通过 instanceof LookupError。
    • 外部错误码的类型:不同库的 code 可能是数字、字符串或缺失值。error.code?.match(...) 只在 code 为 null/undefined 时短路,数字 code 仍会触发 TypeError;写成 as { code?: string } 也不会转换实际值。分类前先用 typeof code === 'string' 缩窄,再调用字符串方法,避免异常处理器自身再次抛错。2026-10-15 在 Node.js 独立验证数字、字符串、缺失字段和 null 输入。
    • 来源:2026-10-09 在 Node.js v24.19.0 独立验证继承、错误码/元数据、stack,以及 JSON 往返后原型身份丢失而 code 保留;另以临时 ESM 包验证内部模块导出可供包主入口使用,未列入 exports 的子路径则报 ERR_PACKAGE_PATH_NOT_EXPORTED。

    加载状态与加载提示的显示时机应分开

    短暂的异步切换不一定需要立即显示加载文字;提示每次出现再消失,反而会产生闪烁。可以只在等待明显变长时展示反馈,并在完成、切换目标或组件卸载时清理提示计时器。

    • 静默读取:不显示提示,不等于删除请求状态。仍需阻止未就绪的提交、处理并发与失败;用 hasLoaded 区分首次读取与真正空结果,刷新期间保留已有内容,成功后再替换。
    • 启动边界:区分首次必需资源、进入场景所需资源和可降级的装饰资源。全局待加载计数包含后两类时,不能用它重新触发启动页;场景入口等待所需资源,可选资源保留当前画面并局部更新。用户离开等待中的入口时取消该意图,避免迟到回调把页面带回旧场景。2026-10-28 用可控回调验证延迟、失败重试、取消及多个运行阶段均不退回启动页。
    • 布局边界:提示若插入正常文档流,会改变正文位置。预留空间或使用不挤压正文的浮层;可变长度标题也需要稳定占位,否则去掉加载行后仍可能上下跳动。
    • 组件边界:同一容器切换资源时,避免仅因资源 URL 改变就强制重建整个容器;真正需要重置状态的主动重试可单独处理。
    • 来源:2026-09-09 在本地 Vue 界面连续切换三个长短标题的异步内容,DOM 观测确认没有即时加载文字,内容区域的顶部坐标与高度三次一致。等待阈值属于具体产品策略,不是通用固定值。

    轮询与取消请求应保护任务身份和终态

    异步请求的完成顺序不等于状态的新旧顺序。取消和轮询并行时,迟到的“运行中”或“取消中”响应可能覆盖已经完成的任务。

    • 提交边界:发送前复制本次选择和参数,之后的进度占位读取提交快照,不能重新读取仍可编辑的表单。
    • 回写边界:发请求时记录任务 ID;响应回来后核对当前任务仍是同一个,再拒绝把终态回退为进行中。取消按钮还需有请求中的标记,防止连续点击发起重复取消。
    • 资源详情边界:选中资源与它的标题、正文和加载状态需要保持同一身份。切换资源后,旧请求的成功、错误和 finally 都不能改写新资源;新资源失败时显示它的失败状态,不能把旧正文挂在新 ID 下。取消传输与回写时校验请求版本可以同时使用,即使传输忽略取消,版本检查仍能阻止旧数据覆盖。
    • 补充验证:2026-10-10 用可手动完成的 Promise 验证先选 A、后选 B 而 A 后完成,旧请求完成时 B 仍在加载,以及 B 失败时清除旧详情三个场景;这些测试控制完成顺序,不依赖真实网络延迟。
    • 验证方法:用可手动完成的 Promise 控制响应顺序,覆盖“轮询先确认完成、取消响应后到”“取消先完成、旧轮询后到”,以及提交等待期间修改表单。2026-09-09 的本地状态测试验证了这些顺序。

    async function* 返回迭代器,消费时才执行函数体

    调用异步生成器函数只得到异步迭代器;函数体从第一次 .next() 开始执行。for await...of 会不断请求下一项,因此通常也是事件流开始实际工作的地方。

    async function* produce(input) {
        console.log('started', input);
        yield 'first';
        yield 'last';
    }
    
    const events = produce('demo'); // 此时还没有打印 started
    for await (const event of events) {
        console.log(event);
    }
    • 边界:这条惰性执行规则针对 async function*,不能推广到所有叫 stream 的函数,也不能套用于普通 async 函数;普通 async 函数调用后会立即执行到第一个等待点。
    • 验证:2026-10-13 独立 Node.js 断言确认创建迭代器后函数体执行记录仍为空,消费后才出现 started,按序收到 first 和 last。

    await 只能等待函数实际返回的任务

    函数内部启动了请求,不代表调用方 await fn() 就会等待该请求。必须返回请求 Promise,或在 async 函数内 await 它;只添加 async、内部仍然不返回也不等待请求,同样会提前完成。

    // 调用方能等待请求完成,并捕获失败。
    function loadData() {
      return fetch('/api/example');
    }
    
    try {
      const response = await loadData();
      // 继续处理响应。
    } catch (error) {
      console.error('请求失败', error);
    }
    • 记忆点:普通函数未写 return 时返回 undefined;await undefined 只经过一次异步续接,不会自动关联函数内部启动的请求。
    • 来源:2026-09-08 在 Node.js v24.19.0 用手动 resolve 的挂起 Promise 验证:函数不返回该 Promise 时,调用方 await 后任务尚未完成。

    固定大小分批请求与循环内 await

    接口限制每次提交的条目数时,可以用 slice(offset, offset + batchSize) 分块,再合并各批返回的数组。slice 不包含结束位置,也不修改原数组;在普通 for 循环体内等待本批请求完成后,才会发起下一批请求。

    const items: Item[] = [];
    const batchSize = 2; // 示例值;实际应遵循接口限制。
    for (let offset = 0; offset < ids.length; offset += batchSize) {
      const batchResult = await loadBatch(ids.slice(offset, offset + batchSize));
      items.push(...batchResult);
    }
    • 记忆点:5 个条目按每批 2 个拆成 2 + 2 + 1,批次串行执行。push(...batchResult) 把本批数组中的元素逐个追加,得到一个合并数组;push(batchResult) 则会追加数组本身。
    • 边界:空输入不会发起请求;若某批 Promise 拒绝且没有捕获,函数会停止后续批次并以拒绝结束,不会成功返回已经累积的部分结果。
    • 来源:2026-09-09 在 Node.js v26.0.0 用异步模拟请求验证:批次大小为 [2, 2, 1]、最多同时执行 1 批、结果合并且原输入不变;空输入调用次数为 0,第二批拒绝后未发起第三批。

    共享在途 Promise 需要失败后释放

    请求去重可以按资源身份保存正在执行的 Promise,让重叠调用复用同一个任务;成功和失败都必须释放在途记录,否则一次失败可能让后续重试一直复用已经拒绝的 Promise。

    • 边界:在途去重与结果缓存是两件事。前者只覆盖任务未完成的时间,后者需要另行定义有效期和失效规则;共享任务的一个消费者离开时,也不能直接取消其他消费者仍在等待的传输。
    • 验证:2026-10-11 在 Node.js 24.19.0 用同键并发调用验证两次调用得到同一个 Promise;第一轮拒绝并清理后,下一轮可以重新执行并成功。

    Promise.allSettled 允许明确处理部分成功

    Promise.all 遇到任意拒绝就以拒绝结束,调用方不会得到一个完整的成功结果数组;Promise.allSettled 等待全部任务结束,逐项保留 fulfilled 或 rejected,便于按产品约定保留有效结果并标记失败项。

    • 边界:两个组合器都不自动取消已经开始的任务;allSettled 也不会自动重试或把失败变成空数据。全部失败、部分失败和成功但为空,应由调用方明确区分。
    • 分批查询:每批自行捕获错误后继续调度,汇总已成功的批次;否则循环内一次未捕获的拒绝会提前停止后续批次。
    • 共享资源收尾:Promise.all 提前拒绝可以触发外层 finally,但其他任务仍可能使用共享资源。若首次异常应停止整批,需要分别停止领取新任务、等待已启动任务结束,再释放资源并抛出原始错误;仅换成 allSettled 不会阻止 worker 继续领取队列。
    • 收尾顺序验证:2026-10-14 用可手动完成的 Promise 验证,一个任务拒绝后,Promise.all(...).finally(cleanup) 的清理早于另一个任务完成。
    • 验证:2026-10-11 用独立 Node.js 实验验证一个拒绝任务和一个成功任务的结果均被保留;聚焦测试另验证一个批次失败时,后续成功批次仍能返回有效记录。

    外层 try/catch 不会捕获未等待 Promise 的异步拒绝

    try { startTask(); } catch { ... } 只能捕获调用时的同步抛错,不能捕获返回 Promise 后发生的拒绝。需要写 await startTask(),或显式接上 .catch(...)。即使函数标记为 async,函数体里的抛错也会表现为返回 Promise 的拒绝。

    • 错误传播:内部 .catch() 如果只记录日志而不重新 throw,产生的新 Promise 会转为成功状态,外层即使 await 也不会进入失败分支。
    • 来源:2026-09-08 在 Node.js v24.19.0 隔离验证:未等待的拒绝不进入外层 catch;等待的拒绝进入 catch;只记录、不重新抛错的 catch 使返回 Promise 成功解析为 undefined。

    绘制与命中坐标

    画面缩放后,命中检测要使用同一变换的逆变换

    将逻辑场景映射到显示区域时,绘制、拖拽落点、点击命中与裁剪边界必须采用同一坐标契约;只放大画面会让落点与单位位置错开。

    • 换算:若显示位置 p = offset + (world - origin) * scale,输入换回逻辑位置就是 world = origin + (p - offset) / scale,两轴分别计算,比例必须非零。存在外层画布缩放或留白时,先撤销外层变换,再撤销场景变换。
    • 范围:横纵比例不同时,逻辑空间中的圆会显示为椭圆。范围预览应投影实际逻辑半径,命中仍在逻辑空间计算;不能用屏幕像素距离直接代替。 若方格和圆形需要保形,场景应等比缩放,格位横纵间距也应相等;只把格子边框画成正方形,仍可能留下不一致的行列间隙。
    • 验证:对已知点做正反变换并用浮点容差比较;再检查绘制后的实际点击、拖拽落点和范围边缘。纯数学往返正确,仍不代表事件接入与可点击区域正确。
    • 依据:通用仿射公式推导与本地数值往返断言;独立交互与范围边缘检查验证了输入和绘制契约。该结论不依赖具体框架或渲染 API。

    画布主题也要覆盖内容区外的底色

    固定比例的绘制区域可能小于整个画布;页内填色与渲染器清屏色是两个来源。只改页面底色,内容区外仍可能保留另一种颜色,切换页面后尤其明显。

    • 处理:让页面根背景和渲染器使用同一主题值,再核对各子页的填充层;正文、弹层与容器可以有不同明暗,但根底色不应因遗留分支意外改变。
    • 验证:2026-10-25 对照本地WebGL绘制顺序、跨页面背景检查和实际截图,确认同步主题来源后,页内与外侧留白的根底色一致。该结论针对绘制主题,不意味着可以拉伸固定比例内容。

    贴底控件要锚定安全底边,不能锚定居中的固定画幅

    固定比例画幅居中时,画幅下缘与窗口下缘可能相距很远;再写死一段底部预留,会叠加出看似安全区的大块空白。应先按实际窗口和安全区契约计算可操作底边,再让导航、工具条与命中区共同锚定它。

    • 背景与操作区:底板可延伸到屏幕边缘,图标和可点击区域仍在安全底边之上。窗口已排除的区域不能重复扣除,缺失安全区信息时才使用有依据的兜底。
    • 保持比例:多出的高度可用于布局与背景,不应直接把方格、角色或圆形范围纵向拉伸;同步更新输入逆变换、拖拽槽位和裁剪边界。
    • 文字大小:最终显示字号随画幅缩放,例如16号设计字乘0.75后仅约12像素。加粗不能替代字号检查,窄框也不应无下限缩字;需要同时调整宽度、文案或排布。
    • 验证:2026-10-26 以五组窗口和安全区数据验证底边落点、手势区不可点、长屏拖拽与方格保形;另观察实际浏览器字形。尺寸模拟不等同于具体手机实测。

    精灵的透明留白也会影响状态条锚点

    血条、名字与装备标志应跟随精灵实际可见轮廓,而非整张图片的上边缘。图片尺寸相同,并不意味着人物头顶处于同一位置。

    • 处理:根据 alpha 包围盒确定可见顶部,将锚点按图片的实际显示比例换算到屏幕;动画帧可预计算轮廓数据,避免每帧读取像素。
    • 底部接触点:需要贴合地面或其他表面时,记录素材内的归一化接触点 (u, v),而不是默认使用整张画布的底边。显示尺寸为 (w, h)、目标点为 (x, y) 时,图像左上角为 (x - u × w, y - v × h);缩放后仍用同一接触点计算。透明留白、阴影与不对称轮廓都会让接触点偏离 (0.5, 1)。若使用 object-fit: contain 产生额外留白,还需计入实际图像的偏移与显示尺寸。
    • DOM 实现与验证:CSS translate() 的横、纵百分比分别按变换参考盒的宽、高换算,可用于把素材内接触点对准外部目标;不能把它误解为父容器百分比。依据 MDN translate() 及多种视口下的实际渲染坐标回算;数值落点之外,仍要查看可见轮廓与表面的接触关系。
    • 边界:透明装备图标仍可能因为绘制代码额外添加底框而显示成方块,先区分素材 alpha 与 UI 背景,再决定是否需要修改素材。透明视觉不要求缩小拖拽命中区域。
    • 依据:本地读取透明图集的 alpha 包围盒,结合实际绘制顶点与拖拽验收,验证了贴近可见轮廓的状态条和无底框图标。

    位图字体的文案正确,不代表字形已能显示

    使用 bitmap font 或字形图集时,新增文案还要覆盖它的每个字符;布局日志记录了正确字符串,最终画面仍可能因为缺少 glyph 而显示问号。

    • 处理:改文案后重新生成字形纹理及其坐标元数据,两者必须同批更新。用实际显示文本检查字符覆盖,再查看渲染结果;只检查文字内容与包体存在不足以证明显示正确。
    • 边界:字符有元数据也不保证字形清晰或形状正确,仍需检查纹理、采样与最终尺寸。该问题属于显式字形图集,不等同于浏览器系统字体自动回退。
    • 字重:位图字体需要把粗体轮廓写入纹理,只改一个 weight 元数据字段不会改变已生成的像素。可用重新解码后的alpha覆盖与实际画面核对粗细;在构建阶段生成粗体,也可避免每帧重复叠画所有字符。
    • 布局:渲染层若有最小字号限制,应按最终字号和真实字形高度排布,不能按调用时传入的字号估算。例如请求10号字实际绘制为16号字后,固定高度按钮里的两行内容可能越过边框。动作文字与成功率、费用等辅助信息可以分行分组,再检查文本包围盒与点击区域。
    • 验证:2026-10-26 本地实际画面出现新增中文字符的问号,而布局文本正确;重建图集并增加逐字符覆盖断言后,自动检查与实际渲染均恢复正确。
    • 布局核验:2026-10-28 对照最终字号、字形高度、绘制坐标和实际截图,确认将辅助信息移至按钮外后不再重叠;没有用缩小字号掩盖空间不足。

    媒体播放与手势

    音频能力要独立检测,不能由视频接口推断

    适配浏览器与独立运行时时,应检查目标音频接口是否存在,而不是用视频接口或某个全局命名空间作为整个媒体模块的能力开关。宿主可以提供音频而不提供视频;错误回退到 new Audio() 后,独立运行时可能因为没有浏览器构造器而直接失败。

    • 排查:分别记录音频、视频和浏览器构造器的存在性,并保留调用阶段的错误。存在音频接口只证明可调用的入口存在,不能证明文件可解码、播放请求已完成或扬声器已经发声。
    • 验证:2026-10-27,用隔离的 JavaScript 运行时构造“有音频工厂、无视频接口、无浏览器 Audio 构造器”的条件,确认视频门控会误入失败分支;这是能力组合测试,不代表所有设备都缺少视频能力。

    play() 尚未开始时暂停,会拒绝播放 Promise

    HTMLMediaElement.play() 是异步请求。播放真正开始前调用 pause()、load() 或重置 src,可能使该请求以中断错误拒绝;这不等于音视频文件损坏。Chrome 官方说明

    • 处理方式:检查并处理 play() 返回的 Promise,成功后再把界面标成“正在播放”。主动取消产生的预期拒绝可以单独处理,加载失败与权限拒绝仍要保留可诊断信息。
    • 记忆点:只在同步调用外写 try/catch 接不住稍后发生的拒绝,见异步拒绝的捕获边界。不能通过全局吞掉所有 Promise 错误解决问题。
    • 验证:2026-10-24 在本地浏览器观察到快速播放后暂停导致的 AbortError,并对照官方说明核验。复用短音效池时,静音与销毁是不同操作;前者不会释放实例,资源生命周期仍须单独管理。

    网络与实时性能

    Chrome 的本地网络权限与 CORS 是不同边界

    公网网页访问本机回环地址或局域网服务时,新版 Chrome 可以要求 Local Network Access 许可。服务端返回正常 CORS 响应、命令行能访问 /info,并不能证明浏览器已获准连接;页面请求可能仍在等待权限决定。Chrome 官方说明

    • 排查方式:分别检查本地服务健康、Network 请求与 CORS,再检查浏览器的网站权限提示。后台标签页的请求可能已发起许可询问,但提示需要切到该标签页才能直接看见;prompt 状态不能当成已经授权。
    • 处理边界:由用户决定是否允许相应站点访问本地应用或网络。不要用关闭浏览器安全检查、随意开启公网隧道或继续放宽 CORS 来代替这一步授权。
    • 验证:2026-10-19,在 Chrome 154 中观察到公网调试页面到本机服务的请求等待,服务端健康与 CORS 正常,权限查询为 prompt;将对应标签页切到前台后,浏览器显示“访问此设备上的其他应用和服务”的许可提示。此处记录的是等待与提示的观察,不推断所有浏览器或所有请求都具有相同表现。

    HTTP 响应头与 meta 的 CSP 会同时生效

    多条执行型 Content Security Policy(CSP)需要同时满足;HTML 的 <meta http-equiv="Content-Security-Policy"> 不能放宽服务器响应头已经施加的限制。

    • 典型现象:HTTP 响应头是 style-src 'self',页面 meta 写了 style-src 'unsafe-inline',内嵌 <style> 仍会被拦截。'self' 允许同源样式资源,不等于允许内联 CSS;正文可以正常显示,但配色、卡片和间距退回默认样式。
    • 排查方法:同时检查 Network 中实际文档响应头、HTML 中的 meta 和 Console 的 CSP 报错。同一份 HTML 直接打开正常,通过本地服务打开却没有样式时,尤其要检查服务统一添加的响应头。
    • 来源:CSP 规范:多条策略的效果;2026-09-09 本地 Chrome 验证,相同 HTML 在上述响应头下没有生效样式表,移除该响应头后恢复内嵌样式。

    请求超时应按工作流设置

    只有某个关键阶段需要避免无限等待时,应让请求层提供可选超时能力,并由该阶段显式传入;不要因此给所有请求增加同一个全局时限。

    • 适合场景:启动鉴权、健康检查等必须在有限时间内结束的流程,同时系统还包含同步、保存或其他可能合理耗时较长的请求。
    • 记忆点:超时时间属于工作流策略。全局默认值会扩大行为变化范围,让无关请求在慢网络下被提前取消;请求封装只负责组合调用方取消信号、可选计时器和稳定错误码。
    • 验证方法:分别覆盖显式超时会中止、调用方主动取消保持原语义、未传超时时不创建计时器三个测试。
    • 来源:通过 AbortController、假定时器和挂起请求的聚焦测试验证。

    实时循环中的 AI rollout 必须有独立预算

    在动画帧回调中同步执行前向推演、MCTS 或其他候选搜索时,最危险的不是平均 CPU 占用,而是某个决策帧一次性超过帧预算并阻塞主线程。

    • 适合场景:实时游戏、可视化模拟或交互式工具需要用多个候选动作 × 多个未来步长做决策。
    • 记忆点:计算量通常近似 候选数 × 推演步数 × 单步状态成本;难度若同时增加候选数和推演深度,会形成乘法增长。Web Worker 可以移走主线程卡顿,但不会减少总计算量;仍应使用简化预测模型、固定候选预算、缓存/复用缓冲区或跨帧分片。
    • 验证方法:用固定场景分别测量单步模拟、状态克隆和完整决策的耗时,并与目标刷新率的单帧预算比较;60 FPS 的理论预算约为 16.7 ms。一次聚焦微基准中,完整状态 rollout 随候选数和深度增长到数百至数千毫秒,证实渲染优化无法消除这种决策帧停顿。
    • 来源:通过固定输入的 TypeScript 聚焦微基准和实时循环源码路径验证。