HTML状态机应通过data-fsm-state属性驱动,而非JS变量;details、:target、radio+:checked等原生机制可替代手写状态逻辑;自定义元素需用attributeChangedCallback响应属性变更,render时避免innerHTML重绘。

用 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)。
- 推荐统一写在根容器上,例如:
<main data-fsm-state="validating"> - CSS 直接响应:
[data-fsm-state="validating"] .submit-btn { opacity: 0.6; pointer-events: none; } - 更新只需一行:
el.dataset.fsmState = 'success',无需手动操作 class 或 style
details、:target、radio + :checked 是现成的状态机
很多场景根本不需要自己实现状态机逻辑:
-
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 中的初始属性,必须手动触发一次或监听后续变更:
- 在类中声明静态 getter:
static get observedAttributes() { return ['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和class,而非整块重绘
真正容易被忽略的是:状态外显化之后,CSS 和测试就变得可预测;而一旦回到“JS 变量驱动 + innerHTML 覆盖”的老路,交互就变成黑盒,改一处、崩一片。



















