多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用 Custom Elements 实现实时计时器 `<live-timer>`:生命周期清理与自定义事件实战

用 Custom Elements 实现实时计时器 `<live-timer>`:生命周期清理与自定义事件实战 文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载live-timer是《现代 JavaScript 教程》自定义元素章节中的一个经典实战任务在已有的time-formatted元素之上封装一个每秒自动更新、并向外派发tick自定义事件的实时时钟。本文以本仓库中的 任务描述 与 官方解答 为主体结合 源码实现完整讲解其实现思路并重点剖析两个容易被忽视的关键点元素被移除时如何正确清理setInterval定时器以及为什么类的属性和方法天然就是元素实例的属性和方法。读完本文你将掌握如何编写一个自带生命周期管理、可组合复用、可向外通信的自主自定义元素Autonomous Custom Element。任务背景在已有元素之上做组合而非重复造轮子在开始写live-timer之前本仓库的 custom elements 主章节 已经实现了一个名为time-formatted的自定义元素它基于Intl.DateTimeFormat将datetime属性中的时间按浏览器本地语言格式化展示。任务要求见 task.md非常明确live-timer元素内部必须使用time-formatted不要重复实现时间格式化逻辑元素内容每秒钟更新一次每秒钟派发一个名为tick的自定义事件事件的event.detail属性携带当前日期用法约定为live-timer idelem/live-timer script elem.addEventListener(tick, event console.log(event.detail)); /script也就是说这是一个典型的组合式自定义元素外层元素负责“定时刷新”这一行为内层元素负责“格式化展示”这一职责二者各司其职。这也体现了自定义元素与普通 HTML 标签一样可以嵌套、可以组合的本质特性。完整解答代码逐段拆解仓库中的官方解答位于 solution.view/live-timer.js完整代码如下class LiveTimer extends HTMLElement { render() { this.innerHTML time-formatted hournumeric minutenumeric secondnumeric /time-formatted ; this.timerElem this.firstElementChild; } connectedCallback() { // (2) if (!this.rendered) { this.render(); this.rendered true; } this.timer setInterval(() this.update(), 1000); } update() { this.date new Date(); this.timerElem.setAttribute(datetime, this.date); this.dispatchEvent(new CustomEvent(tick, { detail: this.date })); } disconnectedCallback() { clearInterval(this.timer); // important to let the element be garbage-collected } } customElements.define(live-timer, LiveTimer);配套的 HTML 使用示例见 solution.view/index.html它先引入time-formatted.js与live-timer.js然后放置live-timer元素并监听tick事件。下面把这段代码拆成四个相互协作的部分逐一讲解。1.render()组装内部结构并缓存子元素引用render() { this.innerHTML time-formatted hournumeric minutenumeric secondnumeric /time-formatted ; this.timerElem this.firstElementChild; }通过this.innerHTML直接注入内层标签time-formatted复用其Intl.DateTimeFormat格式化能力实现任务要求中“不要重复实现这个元素功能”的组合目标hour、minute、second三个属性告诉内层元素只展示时分秒this.timerElem this.firstElementChild把刚渲染出来的第一个子元素即time-formatted缓存为实例属性后续每秒更新时可以直接操作它避免重复查询 DOM。注意这里的渲染时机render()是在connectedCallback()中首次调用的而不是在constructor()中。原因在主章节中讲得很清楚见 8-web-components/2-custom-elements/article.mdconstructor阶段元素尚未接入文档浏览器还没有处理元素属性此时getAttribute会得到null而且推迟渲染到真正需要时对性能更友好。2.connectedCallback()进入文档时启动定时器connectedCallback() { // (2) if (!this.rendered) { this.render(); this.rendered true; } this.timer setInterval(() this.update(), 1000); }用this.rendered布尔标志保证只渲染一次。因为connectedCallback在元素被反复添加/移除文档时会多次触发如果没有这个保护重复渲染会覆盖 DOM 和缓存引用setInterval(() this.update(), 1000)每 1000 毫秒即每秒调用一次update()实现“每秒更新”的核心需求定时器 ID 被保存在this.timer上供后续清理使用。3.update()刷新时间、同步属性并派发事件update() { this.date new Date(); this.timerElem.setAttribute(datetime, this.date); this.dispatchEvent(new CustomEvent(tick, { detail: this.date })); }update()每秒做三件事记录当前时间this.date new Date()把时间同步给内层time-formatted的datetime属性。回忆一下主章节的实现见 solution.view/time-formatted.jsTimeFormatted的observedAttributes中监视了datetime属性一变就会触发attributeChangedCallback并重新render()因此内层的显示内容随之自动刷新派发自定义事件new CustomEvent(tick, { detail: this.date })把当前日期放进event.detail完全符合任务对事件载荷的约定。这一设计印证了自定义元素之间通过“属性 生命周期回调”进行数据流协作的模式外层不需要直接操作内层的文本内容只需更新其监视的属性即可。4.disconnectedCallback()离开文档时清理定时器disconnectedCallback() { clearInterval(this.timer); // important to let the element be garbage-collected }当live-timer从文档中移除时浏览器会调用disconnectedCallback这里用之前保存的定时器 ID 调用clearInterval停止计时。这是整个解答中最容易出错、也最重要的一步官方解答的注释直截了当地写道important to let the element be garbage-collected对元素能否被垃圾回收至关重要。我们会在下一节专门展开。注册customElements.definecustomElements.define(live-timer, LiveTimer);LiveTimer继承自HTMLElement属于自主自定义元素Autonomous Custom Element因此只需要两个参数即可注册。按照规范见 8-web-components/2-custom-elements/article.md自定义元素名称必须包含短横线-live-timer满足这一要求同时也不会与任何内建标签冲突。关键点一为什么移除元素时必须清除setInterval官方解答solution.md开宗明义地强调了第一个要点在元素被从文档移除的时候我们会清除setInterval的 timer。这非常重要否则即使我们不再需要它了它仍然会继续计时。这样浏览器就不能清除这个元素占用和被这个元素引用的内存了。这句话背后是 JavaScript 定时器与垃圾回收GC之间的经典关系值得拆开理解setInterval返回的定时器 ID 背后浏览器会长期持有对该回调闭包的引用只要定时器未被清除回调就会一直存活本例中定时器回调是() this.update()这个箭头函数捕获了this即live-timer元素实例。因此只要定时器还在运行它就间接持有对元素实例的强引用即便页面上的live-timer已经被remove()移除、不再可见只要disconnectedCallback没有调用clearInterval定时器依旧每秒触发元素实例及其内部 DOM、缓存引用就永远无法被垃圾回收形成内存泄漏长此以往每次创建-移除live-timer都会泄漏一个定时器和一个元素实例在动态页面如列表频繁增删、组件反复挂载卸载中内存会持续膨胀。因此清理定时器的正确姿势是在disconnectedCallback中clearInterval与connectedCallback中的setInterval成对出现。这两个生命周期回调的语义也在此处体现得淋漓尽致connectedCallback—— 元素被插入文档时调用元素被反复插入/移除时会多次触发disconnectedCallback—— 元素从文档移除时调用同样可能多次触发。主章节的 生命周期方法概览 还列出了attributeChangedCallback、adoptedCallback等其他钩子它们的调用时机共同构成了自定义元素的生命周期模型。像“进入时启动资源、离开时释放资源”这样的对称写法是自定义元素管理外部资源定时器、监听器、网络连接等的通用范式应成为编写自定义元素的肌肉记忆。关键点二this.date是元素自身的属性而非额外状态官方解答的第二个要点是我们可以通过elem.date属性得到当前时间。类所有的方法和属性天生就是元素的方法和属性。结合代码理解这句话的含义update() { this.date new Date(); // this 就是 live-timer 元素实例 ... }LiveTimer是HTMLElement的子类而元素实例就是类实例。在类内部this指向的就是那个 DOM 元素本身类上定义的任何实例属性如this.date、this.timer、this.timerElem都会直接挂到元素对象上。因此在类外部可以通过elem.date直接读取当前时间const elem document.querySelector(live-timer); console.log(elem.date); // 当前 Date 对象同样的道理适用于类中定义的方法——elem.render()、elem.update()都可以像调用普通对象方法一样被外部调用。这提示我们自定义元素的“公开 API”就是类的方法与属性天然暴露在元素对象上无需额外封装外部代码既可以通过属性 APIelem.date读取内部状态也可以通过事件 APItick事件订阅状态变化两种通信方式都是自定义元素的标准接口形态。tick自定义事件元素向外部通信的标准姿势任务要求“每一秒钟生成一个tick自定义事件event.detail携带当前日期”解答中对应的一行是this.dispatchEvent(new CustomEvent(tick, { detail: this.date }));CustomEvent是浏览器提供的标准事件构造器第二个参数{ detail: ... }可以把任意数据放进事件的detail字段dispatchEvent会在元素上触发该事件并沿 DOM 树向上冒泡CustomEvent默认bubbles: true因此父级容器甚至文档级别都可以监听到实现事件委托任务示例中的elem.addEventListener(tick, event console.log(event.detail))正是消费端用法event.detail拿到的就是最新日期。这与主章节中“内层元素可以分派initialized之类的事件、外层元素监听并响应”的嵌套元素通信建议见 8-web-components/2-custom-elements/article.md一脉相承自定义事件是自定义元素向外广播内部状态变化的标准手段配合detail载荷可以让宿主页面无需轮询就能感知元素状态。运行与验证仓库为本题提供了两个可直接打开的示例目录source.view任务初始状态live-timer.js中只有空的类骨架/* your code here */供读者自行实现solution.view官方完整解答包含index.html、live-timer.js、time-formatted.js三个文件。用浏览器直接打开solution.view/index.html即可看到元素每秒刷新时间同时在控制台每秒打印一次tick事件的detail。若要验证内存清理行为可以在 DevTools 中观察反复document.body.append(elem)/elem.remove()后若disconnectedCallback正确执行不会出现定时器与元素实例持续累积的情况反之若删除清理逻辑控制台会一直打印、元素对象无法被回收。实现时还可以参考同目录下 time-formatted.js 的源码它通过observedAttributesattributeChangedCallback实现了属性驱动重渲染这正是live-timer每秒更新datetime属性后内层能即时刷新的底层机制。总结通过live-timer这个实战案例可以提炼出自定义元素工程化的四个要点组合复用外层元素通过innerHTML组装内层元素属性驱动内层刷新不重复实现已有功能生命周期对称管理connectedCallback中setIntervaldisconnectedCallback中clearInterval成对出现避免定时器持有元素引用导致的内存泄漏实例即元素类的属性与方法天然成为元素的属性与方法this.date在外部就是elem.date这是自定义元素最容易被忽视却最实用的特性事件向外通信用CustomEventdetail载荷派发tick等自定义事件让宿主页面以订阅方式感知元素内部状态。这四个要点覆盖了自定义元素从“定义”到“使用”再到“销毁”的完整生命周期是构建健壮、可复用 Web 组件的基础功。更多背景知识如observedAttributes、自定义内建元素is语法等可继续阅读本仓库的 Custom elements 主章节。赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐用 Custom Elements 实现 live-timer 实时时钟组合复用、生命周期清理与自定义 tick 事件用 Custom Elements 实现 live timer 实时时钟组合复用、生命周期清理与自定义 tick 事件 live timer 是 8 we文档/教程前端zh.javascript.info 实战用 Custom Elements 组合实现 live-timer 秒级计时器并派发 tick 自定义事件zh.javascript.info 实战用 Custom Elements 组合实现 live timer 秒级计时器并派发 tick 自定义事件 导读文档教程前端如何用jQuery轻松掌握Custom Elements自定义元素生命周期管理终极指南如何用jQuery轻松掌握Custom Elements自定义元素生命周期管理终极指南 jQuery作为经典的JavaScript库不仅简化了DOM操作还前端UI组件上一篇Vue-WorkerVue.js多线程优化完整指南下一篇哈希表散列Hash 实战全解七种冲突解决方案与 Java 源码级验证创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表