HTML可通过attribute+observedAttributes和CustomEvent实现单向数据流:父→子用属性注入并禁止反向修改,子→父用冒泡事件上报意图且克隆数据,严禁子组件读取/修改父状态或用存储通信。

HTML 本身没有内置的单向数据流机制,但可以通过属性(attribute)、自定义事件(CustomEvent)和 DOM 状态管理组合出**可预测、可审计、不可逆**的单向流向。关键不是“让 HTML 支持 React 风格”,而是用原生能力封住反向篡改路径。
用 attribute + observedAttributes 实现父 → 子单向注入
子组件通过 static get observedAttributes() 声明监听哪些 HTML 属性,attributeChangedCallback 中只做响应,不修改自身 props 或父节点。这是 Web Components 的强制约定。
常见错误现象:在 attributeChangedCallback 里调用 this.setAttribute('value', newValue) —— 这会触发新一轮回调,造成死循环或状态污染。
使用场景:
立即学习“前端免费学习笔记(深入)”;
- 权限按钮组件接收
permission="user:edit",只据此渲染/禁用,不反向写回 - 步骤条组件接收
current-step="2",只更新当前项高亮,不监听点击去改这个属性
参数差异:attribute 是字符串,需手动解析(如 JSON.parse(this.getAttribute('config'))),而 property 可传任意类型——但 property 不会触发 observedAttributes,所以必须用 attribute 才能保证“注入即生效”的单向性。
用 dispatchEvent + bubbles: true 实现子 → 父意图上报
子组件只能发事件,不能直接调用父方法或修改父数据。事件名必须是动词+名词(如 add-item),且必须带 bubbles: true 和 composed: true,否则穿不出 Shadow DOM。
容易踩的坑:
- 漏设
composed: true→ 在 Lit 或 Vue 封装的 Web Component 里事件丢失 - 在事件处理器里再
dispatchEvent另一个业务事件 → 形成隐式链式调用,流向失控 - 把原始对象塞进
detail→ 父组件修改后影响子组件内部状态,破坏单向隔离
正确做法:发送前克隆数据 detail: { ...item } 或用 structuredClone(item)(注意兼容性)。
禁止在组件内操作外部状态的三个硬约束
单向数据流失效,往往不是技术做不到,而是约束没守住。这三个点必须人工卡死:
- 禁止子组件读取父组件的
data、state或全局 store 实例(哪怕只是 console.log) - 禁止在自定义事件回调中调用
document.querySelector拿父容器并直接改它的innerText - 禁止用
localStorage/sessionStorage作为跨组件通信通道——它绕过了所有流向控制
性能影响:这些约束看似“多此一举”,但换来的是调试时能精准定位到某次 dispatchEvent 触发了哪次状态变更,而不是在一堆异步回调里翻找谁悄悄改了 window.appState。
为什么不用 v-model 或 ngModel?
因为它们本质是语法糖,底层仍是双向绑定。在纯 HTML 组件场景下,v-model 会自动监听 input 并同步更新绑定变量,等于开了后门——子组件一次 input 事件就能静默改掉父级数据,完全违背单向原则。
替代方案:用 change 事件 + 显式 dispatchEvent,由父组件决定是否接受、转换、合并该值。比如输入框组件只发 value-change,父组件收到后做防抖、校验、再决定是否更新自己的 formData。
最易被忽略的一点:单向不是指“一次赋值”,而是指“每次数据流动都有唯一源头、唯一路径、唯一决策点”。哪怕你用了 attribute 和 CustomEvent,只要在某个事件回调里偷偷调了 parentEl.dataset.flag = 'done',整条流就断了。



















