目录
2.1.4 attributeChangedCallback
通过这篇文章,你可以学习到:
针对上面的场景,预言家 Google 早在2011年就推出了解决方案 —— Web Component

Web Components 也被叫做 Custom Elements,它已经成为 浏览器标准 API
下面是各大浏览器针对 Web Components(Custom Elements) 的支持情况:
开发者可以在 Custom Elements 中,封装结构(HTML)、样式(CSS)和行为(JavaScript),最终生成自定义元素标签(使用方式类似于原生 html 标签),不会受框架限制
举个例子:
- <button name="button">按钮button>
-
- <el-button type="success">按钮el-button>
-
- <my-button text="按钮">my-button>
温馨提示:也可以食用完下面的实践出真知,再回头阅读这儿的概念介绍,这样更容易理解
![]()
一种将 DOM、样式、行为 封装在一个可重用的、封装的组件中的技术
Shadow DOM 可以帮助开发者避免样式和 DOM 冲突,提高代码的可维护性和可重用性(大家可以回忆一下修改 Ionic 组件样式时的那种感觉)
它允许开发者创建自定义元素,这些元素可以在页面上使用,就像普通的 HTML 元素一样
我们看下 Ionic 官网按钮示例,可以看出 ionic 组件就是用 Shadow DOM 写的自定义元素

该方法返回 ShadowRoot 对象,用于将一个 Shadow DOM 附加到 指定元素 上
- // 通过 Element.attachShadow() 获取 ShadowRoot 对象
- // open shadow root:元素可以从 js 外部访问根节点
- // closed 拒绝从 js 外部访问关闭的 shadow root 节点
- const shadowRoot = this.attachShadow({ mode: "open" });
- // 把 Shadow DOM 附加到 template 上
- shadowRoot.appendChild(templateDOM.content.cloneNode(true));
关于操作 shadow root 节点的必要性:这个当然很重要了
比如修改 Shadow Dom 中某个节点的 innerHTML
- constructor() {
- // ...
- // 获取按钮文字 DOM
- this.btnTextDOM = shadowRoot.querySelector(".button-inner");
- }
- render() {
- this.btnTextDOM.innerHTML = this.text;
- }
总体思路:创建组件模板,给组件加 Shadow DOM
具体步骤:
当自定义元素的 属性 变化(增加、移除、更改)时调用
- attributeChangedCallback(name, oldVal, newVal) {
- this[name] = newVal;
- this.render();
- }
注意:此方法通常与 get observedAttributes() 结合使用
如果需要在属性变化后,在 attributeChangedCallback 回调函数中执行某些操作,则必须监听这个属性
通过定义 get observedAttributes() 来监听属性变化
- static get observedAttributes() {
- return ["text"];
- }
注意:
dispatchEvent() 方法是用来触发指定事件的
它接受一个 Event 对象作为参数,该对象描述了要触发的事件的类型、是否冒泡、是否可以取消等信息
调用 dispatchEvent() 方法后,会在当前元素上触发指定的事件,并且事件会沿着 DOM 树向上传播,直到到达根节点或者被取消
- // 获取要触发事件的元素
- const myElement = document.querySelector('#my-element');
-
- // 创建一个自定义事件
- const myEvent = new CustomEvent('my-event', {
- detail: {
- message: 'Hello world!'
- }
- });
-
- // 触发自定义事件
- myElement.dispatchEvent(myEvent);
CustomEvent 是用来创建自定义事件的构造函数,它可以创建一个自定义事件对象,该对象可以包含任意的数据,用于在 DOM 中传递信息(PS:所有传递的数据都要放在 details 对象里)
与原生事件不同,自定义事件可以自定义事件类型、是否冒泡、是否可以取消等信息
这里不做详细说明
我们先来搭建一个最基本的按钮,基本步骤如下:
-
- // 模板内容
- const LC_BUTTON_CONTENT = `
-
- .button-container {
- /* background: yellow; */
- }
- .button-inner {
- display: inline-block;
- padding: 12px 20px;
- background-color: red;
- border-radius: 4px;
- border: none;
- font-size: 14px;
- color: #fff;
- cursor: pointer;
- }
-
-
-
-
- `;
-
- class LcButton extends HTMLElement {
- constructor() {
- super();
- // 创建 template
- const templateDOM = document.createElement("template");
- // 填充模板内容
- templateDOM.innerHTML = LC_BUTTON_CONTENT;
-
- // 通过 Element.attachShadow() 获取 ShadowRoot 对象
- // open shadow root:元素可以从 js 外部访问根节点
- // closed 拒绝从 js 外部访问关闭的 shadow root 节点
- const shadowRoot = this.attachShadow({ mode: "open" });
- // 把 Shadow DOM 附加到 template 上
- shadowRoot.appendChild(templateDOM.content.cloneNode(true));
- }
- }
-
- // 创建 Custom Elements(自定义元素)
- window.customElements.define("lc-button", LcButton);
-
效果展示:

该如何理解 属性 呢?
监听属性变化,基本步骤如下:
- class LcButton extends HTMLElement {
- constructor() {
- super();
- // 创建 template
- const templateDOM = document.createElement("template");
- // 填充模板内容
- templateDOM.innerHTML = LC_BUTTON_CONTENT;
-
- // 通过 Element.attachShadow() 获取 ShadowRoot 对象
- // open shadow root:元素可以从 js 外部访问根节点
- // closed 拒绝从 js 外部访问关闭的 shadow root 节点
- const shadowRoot = this.attachShadow({ mode: "open" });
- // 把 Shadow DOM 附加到 template 上
- shadowRoot.appendChild(templateDOM.content.cloneNode(true));
- // 获取按钮文字 DOM
- this.btnTextDOM = shadowRoot.querySelector(".button-inner");
- }
-
- render() {
- // 修改文字内容
- this.btnTextDOM.innerHTML = this.text;
- }
-
- // 如果需要在元素属性变化后,触发 attributeChangedCallback 回调函数,我们必须监听这个属性
- // 通过定义 observedAttributes() 来实现监听属性变化
- static get observedAttributes() {
- return ["text"];
- }
-
- // 当自定义元素的 属性 变化(增加、移除、更改)时调用
- attributeChangedCallback(name, oldVal, newVal) {
- this[name] = newVal;
- this.render();
- }
- }
效果展示:

传递属性,使用的方式是
"给按钮加属性">
映射属性,使用的方式是
- const element = document.querySelector("lc-button");
- element.text = "给按钮加属性-映射";
所谓传递属性,就是通过 attributeChangedCallback 来监听用户传入的 text,监听到变化后,开发者需要手动给 this.text 赋值
- // 当自定义元素的 属性 变化(增加、移除、更改)时调用
- attributeChangedCallback(name, oldVal, newVal) {
- this[name] = newVal;
- this.render();
- }
所谓映射属性,就是用户每次修改 text,都会自动通过 get()/set() 函数来 获取/设置 this.text,开发者不需要手动给 this.text 赋值了
- get text() {
- return this.getAttribute("text");
- }
-
- set text(value) {
- this.setAttribute("text", value);
- }
-
- // 当自定义元素的 属性 变化(增加、移除、更改)时调用
- attributeChangedCallback(name, oldVal, newVal) {
- // this[name] = newVal;
- this.render();
- }
如果强行赋值,会导致栈溢出 因为:


在自定义元素 内部 加事件监听,并把自定义元素 内部值 抛出去
- class LcButton extends HTMLElement {
- constructor() {
- // 获取按钮文字 DOM
- this.btnTextDOM = shadowRoot.querySelector(".button-inner");
- // 在自定义组件 内部 添加事件
- this.btnTextDOM.addEventListener("click", () => {
- console.log("在自定义组件 内部 添加事件");
- // 把内部值传出去
- this.onClick("把内部值传出去", 666);
- });
- }
- }
在自定义元素 外部 加事件监听,并接收自定义组件 内部传出来的值
- const element = document.querySelector("lc-button");
-
- // 在自定义组件 外部 添加事件
- element.addEventListener("click", () => {
- console.log("在自定义组件 外部 添加事件");
- });
-
- // 接收自定义组件 内部 传出来的值
- element.onClick = (value1, value2) => {
- console.log('接收内部传出来的值', value1, value2);
- };

所谓自定义事件,就是自定义组件内部开发者定义的、格式为 onXXX 格式的事件,在自定义组件外部可以通过 addEventListener 监听到
举个例子,下面的 onCustomClick 就是自定义事件
- // 接收 自定义组件 内部的 自定义事件 传出来的值
- element.addEventListener("onCustomClick", (val) => {
- console.log("接收 自定义组件 内部的 自定义事件 传出来的值\n", val);
- });
在自定义组件内部,可以通过 dispatchEvent() 和 new CustomEvent() 来添加自定义事件
这里为了方便触发,我规定了在点击时,发出这个 onCustomClick 事件
- // 在自定义组件 内部 添加事件
- this.btnTextDOM.addEventListener("click", () => {
- // dispatchEvent() 方法会向一个指定的 事件目标 派发一个 Event
- this.dispatchEvent(
- // new CustomEvent() 方法创建一个新的 CustomEvent 对象
- new CustomEvent("onCustomClick", {
- // 需要把想要传递的参数包裹在一个包含detail属性的对象,否则传递的参数不会被挂载
- detail: {
- keysone: "onCustomClick 自定义事件抛出的值",
- keystwo: Number(new Date()),
- },
- })
- );
- });
效果如下:

通过上面一系列精彩的操作,可以看出手写一个 Web Component 组件,还是比较费劲的

但没关系,后面会介绍更简单的方式,开发 Wep Components 组件,敬请期待!