我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Vue过滤器实战:从实时时钟到时间格式化,避开五个常见坑

Vue过滤器实战:从实时时钟到时间格式化,避开五个常见坑 简介这份PDF围绕Vue.js中的过滤器应用面向前端开发与Vue入门者专门解决在页面中实时展示、自动更新当前时间并完成格式转换的问题。文档直接给出可运行代码在模板中使用{{ currentTime | filterTime }}调用自定义过滤器在filters选项中定义filterTime方法分别提取年、月、日、时、分、秒并拼接为中文时间字符串同时在created生命周期内配合setInterval定时器每隔1秒更新currentTime实现视图刷新。文档还补充了另一个通用时间过滤器formatTime支持按YMD、YMDHMS、HMS、YM等参数输出对应格式并对个位数进行补零处理便于适应不同展示场景。资源文件为单个PDF大小约49KB轻量便于保存与查阅目前已有263人学习浏览适合作为Vue过滤器、时间格式化及生命周期配合定时器开发需求的即查即用参考代码可直接迁移到项目中。1. 过滤器不是服务端那道网Vue 模板管道背后的实时时钟玩法很多刚从后端转过来的开发者一听到过滤器三个字脑子里先浮现的是 Servlet 里那一长串 Filter 链请求进来先过一遍不合规的直接拦在门外。但 Vue 里说的 filter 完全是另一回事——它是模板里那个管道符|右侧的格式化函数负责把数据在渲染前做一次美容比如把 13 位时间戳转换成2025-05-18 14:30:00。这篇笔记要拆的就是一种很常见的需求在页面上显示当前时间并且让它每秒自己跳动。核心不是过滤器本身而是过滤器 响应式数据 生命周期三者的配合方式。2. 局部过滤器加定时器把当前时间变成每秒跳动的数字2.1 先看最小可运行版本原始案例给的是一个单文件 HTML直接在 CDN 引入 Vue 2.4.4就能跑起来。我把代码整理过一遍修正了月份取值的问题后面会专门讲这个坑完整逻辑如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleVue filter 实时时钟/title script srchttps://cdn.bootcss.com/vue/2.4.4/vue.min.js/script /head body div idssl{{ currentTime | filterTime }}/div script var em new Vue({ el: #ssl, data: { currentTime: new Date() }, filters: { filterTime(val) { var Y val.getFullYear() var M val.getMonth() 1 // 注意getMonth() 返回 0-11必须 1 var D val.getDate() var H val.getHours() var MI val.getMinutes() var S val.getSeconds() return Y 年 M 月 D 日 H 时 MI 分 S 秒 } }, created() { var _this this setInterval(function () { _this.currentTime new Date() }, 1000) } }) /script /body /html这里面的执行顺序值得捋一遍页面加载时Vue 实例创建data.currentTime被初始化为当前时间接着created钩子触发定时器启动每 1 秒currentTime被替换成一个新的Date对象这个赋值动作触发了响应式依赖更新模板里的{{ currentTime | filterTime }}重新求值页面上的时间自然跟着走。过滤器filterTime只负责一件事把一个Date对象转换成中文格式的字符串。它不负责计时也不负责触发更新。2.2 定时器为什么放在 created 而不是 mounted原始代码在created里启动了定时器注释里写的是created 是最早使用 data 数据的生命周期。这个说法在 Vue 2 里是准确的created阶段实例已经完成了数据观测、属性计算、方法绑定data和methods都能直接访问但 DOM 还没挂载this.$el还是 undefined。mounted虽然也能放定时器但对这个需求来说晚了一步——没有任何理由等 DOM 渲染完再开始计时。我一般会把这类启动型逻辑放在created把操作 DOM 的逻辑留在mounted。比如后面要做滚动定位、图表初始化才需要mounted。这里只是个数据更新任务created的执行时机更早用户看到第一帧画面时时间就已经是准的。2.3 为什么非要 var _this this这一行是初学者最容易跳过的代码但少了它时钟直接停摆。看下面这段created() { setInterval(function () { this.currentTime new Date() }, 1000) }问题出在function关键字上。普通函数体内的this由调用方式决定setInterval调用回调时会把this指向window在浏览器环境里。于是this.currentTime变成给window对象挂属性Vue 实例里的currentTime纹丝不动页面时间自然不跳。var _this this的原理很简单created钩子本身是 Vue 实例的方法调用时this指向实例用闭包把这一层this存进_this定时器回调里再拿_this来用。同样的处理方式在setTimeout、事件监听回调、axios.then这些场景里都很常见。当然换成箭头函数也可以解决但 Vue 2.4.4 的时代箭头函数在部分浏览器上还有兼容顾虑存_this是更稳妥的老派写法。2.4 这个方案的边界在哪当你把这段代码跑通会发现一个体验问题秒数是1、2、3这样单数字显示没有补零。如果页面上的时钟要放在后台系统顶栏旁边再挂个日期这种宽度跳动的数字会显得很业余。原始代码里第二个示例就补上了这个短板用addZero函数把少于 10 的数字补成01、09。这是格式化逻辑的典型演进方向先能跑再规范化输出。另外一个隐患是定时器没有保存引用也没有在beforeDestroy里清理。组件实例如果被销毁定时器还会继续每秒给一个已销毁的实例赋值。Vue 2 里对已销毁实例的响应式更新通常会静默失败也可能报错但定时器本身不回收属于内存泄漏。这个坑放到第 5 章详细排查。3. 全局过滤器 formatTime一个过滤器吃透四种时间格式3.1 局部注册和全局注册的差别原始代码第二个示例换了一种注册方式Vue.filter(formatTime, function(value, type){ ... })直接在构造器外面全局注册。局部过滤器挂在组件选项的filters对象里作用域仅限当前组件全局过滤器注册一次任何组件模板里都能直接用| formatTime。对于时间格式化这种通用需求全局注册明显更合适——项目里有十几个页面都要显示时间总不能在每个组件里复制一份filterTime。全局注册的时机也讲究必须在new Vue()创建实例之前调用。顺序反了模板编译时找不到过滤器渲染出来就是原始值控制台还会抛Failed to resolve filter警告。这个和注册全局组件是同一个道理。3.2 补零函数唯一不能省的工具函数addZero是 formatTime 方案的基石。没有它2025-5-6 7:8:9这种输出会让时钟显得很不专业。补个规范版本function addZero(val) { if (val 10) { return 0 val } return val }逻辑很简单小于 10 的数字前面拼一个 0。有个细节——函数的返回值类型不是固定的。输入9返回字符串09输入10返回数字10。好在拼字符串的时候 JavaScript 会自动做类型转换2025- 09和2025- 10都能得到正确结果所以这里不强制转类型也不会出错。但如果后续逻辑要用这个返回值做数学运算就得留个心眼。原版还有一个常见写法差异有的开发者喜欢写return val 10 ? 0 val : val效果一样纯粹是风格问题。3.3 四种格式分支和传参细节完整跑通全局过滤器版本!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleVue 全局时间过滤器/title script srchttps://cdn.bootcss.com/vue/2.4.4/vue.min.js/script /head body div idapp div{{ message | formatTime(YMD) }}/div div{{ message | formatTime(YMDHMS) }}/div div{{ message | formatTime(HMS) }}/div div{{ message | formatTime(YM) }}/div /div script function addZero(val) { if (val 10) { return 0 val } return val } Vue.filter(formatTime, function (value, type) { var dataTime var data new Date() data.setTime(value) var year data.getFullYear() var month addZero(data.getMonth() 1) var day addZero(data.getDate()) var hour addZero(data.getHours()) var minute addZero(data.getMinutes()) var second addZero(data.getSeconds()) if (type YMD) { dataTime year - month - day } else if (type YMDHMS) { dataTime year - month - day hour : minute : second } else if (type HMS) { dataTime hour : minute : second } else if (type YM) { dataTime year - month } return dataTime }) var app new Vue({ el: #app, data: { message: 1501068985877 } }) /script /html模板里{{ message | formatTime(YMDHMS) }}的传参顺序要记牢管道符左侧的message会作为过滤器的第一个参数传入括号里的YMDHMS是第二个参数。过滤器本质就是个普通函数value是待格式化的时间数据type决定输出格式。四个分支输出分别是type 参数输出示例典型使用场景YMD2025-05-18列表页日期列、表格筛选区YMDHMS2025-05-18 14:30:09日志时间、操作记录详情HMS14:30:09倒计时、当天用时统计YM2025-05月份选择器、归档分组标题格式化出来的时间不满足业务要求时最常改的就是这个分支结构。比如需要带星期几就加一个week数组[日,一,二,三,四,五,六]再往返回值里拼data.getDay()。这类扩展都在过滤器函数内部完成不需要动模板。3.4 时间数据和 formatTime 的配合边界formatTime在处理标准 13 位时间戳比如消息里的1501068985877时工作得很好。但如果业务接口返回的是2025-05-18 14:30:09这种字符串直接data.setTime(value)会得到Invalid Date。面对这种情况我通常会在过滤器开头加个类型判断把字符串转成时间戳再做格式化。第 5 章的排查清单里会专门写这一条。另外这三种数据格式要分清时间戳数字、Date对象对象、日期字符串字符串它们之间互转的方式各不相同过滤器设计时最好只认其中一种或者做统一兼容。4. 过滤器为什么能实时刷新依赖追踪与渲染管线的配合细节4.1 过滤器不是数据源只是显示层转换很多初学者把currentTime | filterTime理解成过滤器每秒自动执行一次这是方向性错误。Vue 的过滤器没有任何定时能力它只是一个同步纯函数你给我一个值我还你一个处理后的值。真正驱动页面刷新的是currentTime这个响应式数据被重新赋值。改数据触发依赖更新模板重新渲染渲染时碰到的过滤器顺带重新执行——这个链条才是实时更新的本质。4.2 模板依赖收集改的虽然是一个字段动的却是整条渲染链路Vue 2 的响应式系统在初始化时遍历data的每个属性用Object.defineProperty把它们转成 getter/setter。模板编译阶段渲染函数执行时会访问currentTime的 getter此刻正在运行的渲染 watcher 就被注册成了该属性的依赖。之后定时器里执行_this.currentTime new Date()触发 setter通知渲染 watcher 进入排队状态下一个 tick 重新执行渲染函数。渲染函数重新求值{{ currentTime | filterTime }}管道右侧的filterTime也随之重新执行。这个机制解释了三个现象为什么只改Date对象内部的秒数页面不会动——因为Date对象不是响应式的改它的内部状态根本不会触发 setter。为什么重新赋值new Date()就行——因为替换了整个对象属于给响应式属性赋新值。为什么过滤器里做任何逻辑处理都不影响更新频率——过滤器只是渲染过程中的一个环节执行次数由渲染触发次数决定由定时器的频率间接控制。4.3 定时器间隔和渲染刷新是两套节奏代码里setInterval的间隔是 1000 毫秒但currentTime赋值不会精确到每一秒整因为setInterval本身受事件循环阻塞、浏览器后台标签页节流的影响实际触发间隔可能偏长。页面切到后台Chrome 会把定时器最小间隔拉长到 1 秒甚至更久标签页完全隐藏时部分浏览器会挂起定时器。所以实时更新的上限由浏览器调度决定这是前端定时任务的物理边界。如果业务上要求精准到秒级整点光靠setInterval不够需要在回调里用系统时间对时校准——取Date.now()而不是在回调里再new Date()一次本质上没差别因为new Date()拿到的就是系统当前时间。4.4 这套机制的性能边界在哪每秒重新渲染一次渲染函数里还跑一个字符串拼接这个开销对单个组件来说完全可以忽略。但如果页面上同时挂了几十个带时间过滤器的组件每个组件都开一个setInterval就会产生几十个定时器并发运行。定时器回调堆在一起执行可能引起掉帧或 CPU 占用波动。下一章要讲的定时器清理就是为了防止这类问题而第 6 章的封装方案则是从根源上规避多个定时器并发。5. 常见问题排查五个让时间显示翻车的真实坑5.1 月份永远少 1现象明明是 5 月 18 日页面上显示的却是 5 月 18 日因为显示的是5月和18日问题出在月份数值上——更准确地说是 5 月显示成 4 月。原始代码里写的是val.getMonth()没有加 1。原因getMonth()返回的月份范围是 0-110 代表 1 月。这是Date对象从 Java 时代沿袭下来的老规矩不是 Vue 的问题。解决取到月份后统一加 1。全局过滤器formatTime的源码里正确写了data.getMonth() 1但第一个示例漏了。这也是我在第 2 章整理代码时特意修正的原因。从那以后我每次看到getMonth()后面不跟 1都会条件反射地多看两眼。5.2 定时器回调里 this 指向 window时钟纹丝不动现象页面加载后时间显示出来但永远停在同一秒。控制台没有报错或者说报了Cannot read property currentTime of undefined之类的错但不好定位。原因setInterval(function(){ this.currentTime ... })的回调里this指向全局对象不是 Vue 实例。赋值操作变成了给window挂属性实例数据未被更新。解决提前var _this this存实例引用或者回调改箭头函数。这是 JavaScript 的this绑定规则问题跟 Vue 无关但出错的场景常常在 Vue 的生命周期回调里。遇到数据没更新但也不报错的情况优先检查回调函数里有没有用普通function。5.3 秒数从 9 跳到 10数字宽度突然变一下现象用第一个方案时时间区域每秒变化但分钟个位从 9 变 10 的瞬间数字宽度明显跳动页面像被什么推了一下。原因没有做补零处理个位数和十位数的字符宽度不同。在等宽字体下影响小但页面字体是默认的微软雅黑或苹方时数字宽度差异明显每秒都在微抖整秒跨位时长宽跳动更突出。解决统一走addZero补零保证所有时间位输出恒定宽度。这里也有个不起眼的细节补零后输出的是字符串不补时有些编译器可能推断成数字字符串拼接场景下结果一样但如果要拿这个值做计算就显式String(val)包裹一下更为稳妥。5.4 组件销毁了定时器还在后台跑现象从列表页进入详情页再返回列表页控制台不断打印报错或者页面性能逐渐变差。原因created里启动了setInterval但没有在任何销毁钩子里清理。组件实例销毁后定时器回调仍然每秒执行一次_this.currentTime new Date()给一个已经卸载的实例做响应式赋值。轻则内存泄漏重则在严格模式下抛警告。解决把定时器句柄存到实例上在beforeDestroy里清理created() { var _this this this._timer setInterval(function () { _this.currentTime new Date() }, 1000) }, beforeDestroy() { clearInterval(this._timer) }多组件场景里每个组件各自持有定时器句柄销毁时各自清理互不干扰。判断有没有正确清理可以在beforeDestroy里打个console.log(timer cleared)配合 Vue 开发者工具看实例销毁过程。5.5 接口返回的时间字符串塞进过滤器输出 Invalid Date现象后端返回2025-05-18 14:30:09这种字符串直接data.setTime(value)后用getFullYear()等内容取出的全是NaN页面显示NaN-NaN-NaN或Invalid Date。原因setTime()接收的是毫秒时间戳数字不是日期字符串。字符串会被转成NaN整个Date对象进入无效状态。解决在过滤器入口做兼容。常见做法是检测传入值类型字符串优先用new Date(value)解析时间戳数字才走setTimeVue.filter(formatTime, function (value, type) { var data if (typeof value number) { data new Date() data.setTime(value) } else { var parseVal typeof value string ? value.replace(/-/g, /) : value data new Date(parseVal) } // 后续统一取年月日时分秒 })这里把连字符-替换成/是为了兼容 iOS 上new Date(2025-05-18 14:30:09)返回 Invalid Date 的老问题。Android 和桌面端通常能直接解析这种格式iOS 的 JavaScriptCore 引擎则要求斜杠分隔。这个兼容处理加到过滤器里是一种低成本方案后端时间格式统一了前台展示就不会因为引擎差异而翻车。6. 进阶把实时时钟封装成自定义指令顺手解决销毁问题每次写一个需要实时更新的时间展示都得手动created里开定时器、beforeDestroy里清理代码重复度高。我更习惯的做法是把这套逻辑抽成一个自定义指令模板里只留一个属性定时器和销毁全部由指令内部接管Vue.directive(clock, { bind: function (el, binding) { function formatTime(date, fmt) { var map { Y: date.getFullYear(), M: date.getMonth() 1, D: date.getDate(), H: date.getHours(), MI: date.getMinutes(), S: date.getSeconds() } var result fmt for (var key in map) { var val map[key] result result.split(key).join(val 10 ? 0 val : val) } return result } function update() { el.textContent formatTime(new Date(), binding.value || Y-M-D H:MI:S) } update() el.__clockTimer setInterval(update, 1000) }, unbind: function (el) { clearInterval(el.__clockTimer) } })模板里这样调用div v-clockY-M-D H:MI:S2025-05-18 14:30:09/div指令把定时器句柄挂在 DOM 元素上unbind时直接清理不需要在组件里写任何生命周期代码。类似地处理数据格式兼容也顺理成章指令内部封装了格式化函数业务组件完全不关心实现细节。这样一个小组件就实现了定时器生命周期与视图解耦。从那以后我每次做后台系统顶栏的时钟或倒计时需求都会先评估能不能走v-clock这种指令方案让定时器数量可控、销毁逻辑集中。这套模式对 Vue 2.4 到 2.6 都适用遇到类似场景可以直接复用希望帮到你。本文还有配套的精品资源点击获取
返回列表