简介:这份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 lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Vue filter 实时时钟</title> <script src="https://cdn.bootcss.com/vue/2.4.4/vue.min.js"></script> </head> <body> <div id="ssl">{{ 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 lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Vue 全局时间过滤器</title> <script src="https://cdn.bootcss.com/vue/2.4.4/vue.min.js"></script> </head> <body> <div id="app"> <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 参数 | 输出示例 | 典型使用场景 |
|---|---|---|
'YMD' | 2025-05-18 | 列表页日期列、表格筛选区 |
'YMDHMS' | 2025-05-18 14:30:09 | 日志时间、操作记录详情 |
'HMS' | 14:30:09 | 倒计时、当天用时统计 |
'YM' | 2025-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-11,0 代表 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)解析,时间戳数字才走setTime:
Vue.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-clock="'Y-M-D H:MI:S'">2025-05-18 14:30:09</div>指令把定时器句柄挂在 DOM 元素上,unbind时直接清理,不需要在组件里写任何生命周期代码。类似地处理"数据格式兼容"也顺理成章:指令内部封装了格式化函数,业务组件完全不关心实现细节。这样一个小组件就实现了定时器生命周期与视图解耦。从那以后我每次做后台系统顶栏的时钟或倒计时需求,都会先评估能不能走v-clock这种指令方案,让定时器数量可控、销毁逻辑集中。这套模式对 Vue 2.4 到 2.6 都适用,遇到类似场景可以直接复用,希望帮到你。
本文还有配套的精品资源,点击获取