应使用 data-fsm-state 驱动状态而非 JS 变量,将状态写入 DOM 属性可使浏览器、CSS、测试脚本和无障碍工具直接响应;details、:target、radio+ :checked 等原生机制可替代手写状态机;自定义元素需用 attributeChangedCallback 结合 connectedCallback 确保初始与后续状态同步;render 时避免 innerHTML 覆盖,应精准更新节点、class 和 dataset。

用 data-fsm-state 驱动状态,别存 JS 变量里
HTML 状态机的核心不是写一堆 if/else 切换变量,而是把状态“晒出来”——写进 DOM 属性里。浏览器能天然响应它,CSS 能直接选中,测试脚本能直接断言,无障碍工具也能读取。常见错误是只在 JS 里维护 fsm.state = 'loading',却忘了同步到 HTML:document.documentElement.dataset.fsmState = fsm.state(注意:kebab-case 自动转 camelCase,data-fsm-state 对应 dataset.fsmState)。
details 和 :target 是现成的状态机,别手写
很多场景根本不需要自己实现状态机逻辑:
-
details元素自带开合状态,点击即切换,DOM 自己记着open属性,无需监听、无需setState -
:target伪类自动匹配 URL hash,a[href="#faq"]点击后,section[id="faq"]立刻命中section:target,高亮或动画全靠 CSS - radio 组 +
:checked ~ .panel可纯 CSS 实现单选导航,name就是状态键,value就是状态值,浏览器保证互斥
这些原生机制不触发重绘、不依赖框架、无障碍友好,但常被跳过,只因开发者默认“得写 JS 才算交互”。
自定义元素里用 attributeChangedCallback 响应状态变更
如果你封装了 <status-indicator> 这类组件,想让它随 data-status="error" 自动变样式,不能只靠初始渲染——attributeChangedCallback 不响应 HTML 中的初始属性,必须手动触发一次或监听后续变更:
立即学习“前端免费学习笔记(深入)”;
- 确保静态 HTML 写了
<status-indicator data-status="loading"></status-indicator> - 在
observedAttributes里声明['data-status'] - 在
attributeChangedCallback中调用this.render(),但首次初始化需在connectedCallback补一手:this.render() - 数值或布尔值要小心:
Number(this.dataset.step)、this.hasAttribute('data-active'),别信this.dataset.active === 'true'
render() 里别用 innerHTML =,会丢事件监听器
状态变化后,最危险的更新方式是直接覆盖整个区域的 innerHTML。比如在 render() 里写 el.innerHTML = '<button>Retry</button>',之前绑在按钮上的 addEventListener 全没了。正确做法是:
- 只更新必要节点:
el.querySelector('.status-text').textContent = 'Failed' - 用
classList.toggle('hidden', state !== 'success')控制显隐 - 用
dataset同步状态,让 CSS 和外部逻辑统一读取源
状态机的复杂点不在逻辑分支,而在 DOM 更新的副作用——一次赋值,可能悄悄抹掉用户已输入的内容、中断正在播放的音频、清空未提交的表单。把状态外显化,就是把副作用暴露出来,让人看得见、测得到、控得住。



















