Vue过滤器实时时间:formatTime、setInterval与避坑
简介Vue filter 过滤当前时间并实现实时更新效果的 PDF 教程面向需要掌握 Vue 自定义过滤器及生命周期钩子用法的初中级前端开发者。资源仅含 1 个 PDF 文件体积 49KB内含可直接复制的完整 HTMLJavaScript 示例从模板绑定、filters 定义到 created 中定时更新数据的实现链路均有注释说明。文中先展示 filterTime 过滤器将 Date 对象转换为年月日时分秒中文格式再通过 setInterval 每秒修改 currentTime 触发视图刷新并解释如何用变量锁定 this 保证作用域一致随后介绍更通用的 formatTime 过滤器支持 YMD、YMDHMS、HMS、YM 四种格式同时附带数字补零的 addZero 方法可直接用于订单时间、日志时间戳等场景。这份 49KB 的压缩包适合 Vue 入门者理解过滤器机制与实时渲染思路也可以作为项目中日期组件的代码参考。目前已有 263 人浏览学习内容精炼便于快速查阅。1. Vue filter 实时时间为什么一个过滤器能撑起秒级刷新一个div{{ currentTime | filterTime }}/div配一个setInterval每秒把currentTime换成新的new Date()页面上的时间就开始一秒一秒地走。这是 Vue 2 时代最典型的实时时间写法常被用在工单页的“最后更新于”、数据大屏右下角、日志列表时间戳这类场景里。它的核心其实不是过滤器而是过滤器背后的数据驱动时间要“变”靠的是 data 里的变量被替换而不是手动去改 DOM 文本。这套写法适合两类人——刚开始搭后台页面、想少写冗余代码的前端以及接到旧项目、需要快速读懂类似结构的维护者。下面把两段代码拆开讲清楚怎么跑、为什么这么写、以及哪几处最容易翻车。2. 落地第一版filterTime 的模板、数据与生命周期2.1 完整页面一个 div、一个 filters、一个 created先看原教程里最基础的那段可运行页面我按原样整理过来代码结构比较完整适合直接存成.html文件打开验证!DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 meta http-equivX-UA-Compatible contentieedge titleVue Filter 实时时间/title !-- 老项目里常见用法CDN 引入 Vue 2.x -- script srchttps://unpkg.com/vue/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() 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整个页面的数据流其实只有一条线created钩子启动定时器定时器每 1 秒给currentTime赋一个全新的Date实例模板里的{{ currentTime | filterTime }}检测到数据变化后重新求值把新的时间文本渲染到页面上。这里有个容易被忽略的细节filters是在组件实例里通过filters: { ... }声明的这是局部过滤器作用于当前 Vue 实例控制的模板范围。同一页面里如果有第二个 Vue 实例它是拿不到filterTime的。后面第 3 章要讲的Vue.filter全局注册写法才是跨实例共享的方案。新手经常在这两者之间混用结果要么是“过滤器未定义”要么是全局过滤器把局部同名过滤器覆盖掉。2.2 created 里挂定时器生命周期选型与 this 作用域为什么定时器要放在created而不是mounted或者methods里这得从 Vue 2 的实例初始化顺序看。created触发时data里的数据已经完成响应式绑定实例的methods、filters也都可用了但模板对应的真实 DOM 还没挂载。在这个阶段启动定时器可以确保定时器回调里访问this.currentTime时数据是就绪的而且不依赖 DOM 存在。mounted当然也能挂定时器差别在于mounted要等 DOM 渲染完才执行对“显示当前时间”这种纯数据需求来说晚了一步没有额外收益。放在methods里更不合适——methods只是定义了方法没人调用它就不会执行除非你在created或模板里手动触发绕一圈回来还是得回到生命周期钩子。再看created内部这段经典写法created() { var _this this setInterval(function () { _this.currentTime new Date() }, 1000) }变量名_this的作用是保存 Vue 实例的引用。setInterval的回调是普通function在浏览器里执行时回调函数内部的this指向的并不是 Vue 实例而是window。如果直接写this.currentTime new Date()在非严格模式下它会给window对象挂一个currentTime属性Vue 实例里的数据纹丝不动页面时间自然也不会更新。用箭头函数可以省掉_this这行因为箭头函数没有自己的this会沿作用域链向上找created() { setInterval(() { this.currentTime new Date() }, 1000) }两种写法效果一致。老代码里var _this this很常见新手读到时别觉得多余它是为了解决 var 函数内部this指向问题而存在的。顺带一提setInterval的第二个参数是毫秒数1000 就是 1 秒想要更高频率就把数字调小比如 500 就是半秒刷新一次但真实项目里大多数场景 1 秒足够再高的频率只会徒增不必要的渲染开销。2.3 格式化方法逐字段取出与月份加一的坑filterTime(val)是这个方案里最直观的部分传入的val就是管道符前面的currentTime类型是Date对象然后通过Date原型上的取值方法逐个拆出年月日时分秒。各方法返回值说明方法返回值范围getFullYear()四位年份如 2024getMonth()月份0 ~ 11注意从 0 开始getDate()日期1 ~ 31getHours()小时0 ~ 23getMinutes()分钟0 ~ 59getSeconds()秒0 ~ 59这里藏着一个全教程最典型的坑getMonth()返回的月份是从 0 开始计数的。1 月是 012 月是 11。上面原样代码里直接val.getMonth()拼接会导致 5 月显示成 4 月、12 月显示成 11 月。想正确显示必须加 1。filterTime(val) { var Y val.getFullYear() var M val.getMonth() 1 // 注意补上加 1否则月份少一月 var D val.getDate() var H val.getHours() var MI val.getMinutes() var S val.getSeconds() return Y 年 M 月 D 日 H : MI : S }修正后再看返回的字符串2024年5月6日 14:3:9。语义上没问题但美感上还差一步——分钟和秒是个位数时没有补零显示成14:3:9而不是14:03:09。这在日志或工单场景里读起来很别扭。补零的事第 3 章的addZero会专门处理这一章先记住getMonth()必须加 1否则时间显示就是错的而且这种错很隐蔽因为月份从 0 到 11 都有哪怕错了数字看着也“像真的”。3. 通用化formatTime 全局过滤器的四种格式与补零设计3.1 全局注册 Vue.filter组件内外都可用的写法第一段代码的过滤器是写在组件里的局部过滤器换一个 Vue 实例就得重新声明一遍。原教程第二段给了一个更通用的方案用Vue.filter全局注册格式化为可配置类型。!DOCTYPE html html langen head meta charsetUTF-8 titleVue 全局时间过滤器/title script srchttps://unpkg.com/vue/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 } else { 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 /body /htmlVue.filter(formatTime, handler)是在创建任何 Vue 实例之前注册的注册之后页面里所有 Vue 实例的模板都能直接用formatTime过滤器。对老项目来说这种全局注册适合放公共时间格式、公共货币格式之类的通用转换避免每个组件各写一遍。缺点是全局命名空间会被占住如果两个开发者各注册了一个同名过滤器后注册的会覆盖先注册的排查起来比较绕。3.2 addZero 补零为什么不能直接 val 10补零函数是这段代码里最值得单独拎出来讲的小工具function addZero(val) { if (val 10) { return 0 val } else { return val } }逻辑很简单值小于 10 就在前面拼一个0否则原样返回。它解决的问题是让3变成03、让9变成09保证时间显示固定占两位。这里有一个类型上的细节val 10为真时0 val的结果是字符串比如0 3得到03为假时返回的是数字比如12。后续拼接进dataTime时会被隐式转成字符串最终结果不受影响但如果你把addZero单独拿来比较比如addZero(12) 12返回的是false因为左边是数字12。老代码图省事这样写没问题想要返回类型统一可以改成function addZero(val) { return String(val).padStart(2, 0) }padStart(2, 0)的含义是如果字符串长度不足两位就在开头用0补齐。addZero(3)返回03addZero(12)返回12返回结果全是字符串行为更一致。两种写法的边界差异在于val超过两位数时if (val 10)的写法返回原数字padStart写法返回原字符串效果一样。3.3 四种 type 的参数分支与调用方式全局过滤器formatTime接收两个参数value和type。value是管道符左边传进来的值在这个例子里是message也就是时间戳1501068985877type是模板管道写法里括号里传的参数比如formatTime(YMD)里的YMD。过滤器内部先用var data new Date()创建日期对象再调用data.setTime(value)把时间戳设置为该日期对象的时间。这里value是毫秒级时间戳setTime接收的正是毫秒数所以能正确解析。四种type对应的输出格式如下type返回格式示例基于 2024-05-06 14:03:09YMD年-月-日2024-05-06YMDHMS年-月-日 时:分:秒2024-05-06 14:03:09HMS时:分:秒14:03:09YM年-月2024-05模板里的调用方式对应关系是{{ message | formatTime(YMD) }}等价于调用formatTime(message, YMD)管道符前面的值自动成为第一个参数括号里显式写的值成为第二个参数。这种把格式类型作为参数的设计比写死在过滤器里灵活得多——同一个时间戳在列表页需要日期在详情页需要完整时间只需要换参数而不需要换过滤器。分支判断用了一串else if每个分支只做一件事按type拼出对应字段。如果传入的type不在四种类型里函数返回空字符串dataTime。这是个值得注意的边界调用方拼错参数名时页面不会报错但该位置会渲染成空白。实际项目里可以在else分支返回原始value或返回一个默认格式让错误暴露得更明显。4. 实时更新机制数据驱动与渲染时机4.1 从 currentTime 到 DOM响应式链路拆解很多新手会把“实时更新”归功于过滤器其实过滤器只是最后一道格式化工序真正的驱动引擎是 Vue 的响应式系统。整个链路可以分为四步第一步模板编译。{{ currentTime | filterTime }}在渲染时会被解析为一个表达式Vue 内部把它编译进渲染函数渲染函数执行时读取currentTime的值这一步同时触发了currentTime的 getterVue 会把这个渲染函数记为currentTime的依赖。第二步定时器改数据。setInterval回调里执行_this.currentTime new Date()给currentTime赋了一个全新对象触发 setter。Vue 的响应式系统发现这个数据有依赖者就把对应的渲染任务放进异步更新队列。第三步异步批量渲染。Vue 不会立刻同步更新 DOM而是等当前同步代码执行完在下一个 tick 里统一执行队列里的渲染任务。这保证了即使一秒钟内同一数据被修改多次最终只会渲染一次。第四步过滤器重新执行。渲染函数再度执行时filterTime(val)被调用传入新的Date对象返回新的字符串Vue 用这个新字符串去更新真实 DOM 的文本节点。关键结论是过滤器本身不会主动触发更新它只是渲染时被反复调用的普通函数。让页面“动起来”的是currentTime这个数据被持续替换。理解了这一点遇到“过滤器写了但时间不动”的问题时排查方向就清楚了——先看 data 有没有变再看过滤器有没有被调用。4.2 过滤器执行时机每次渲染都会重新跑要明确一个边界Vue 2 里组件内任何一个响应式数据发生变化都会让该组件的渲染函数重新执行渲染函数里的过滤器自然也跟着重新执行。也就是说filterTime的执行频率并不只取决于currentTime的变化频率。举个例子同一个 Vue 实例里还有另一个数据userName它被某个输入框绑定。用户每敲一个字母userName变化组件重新渲染{{ currentTime | filterTime }}的过滤器也被重新执行一遍——哪怕时间本身没变。这种特性带来的实用建议是过滤器里只做轻量格式化不要放复杂计算。像getFullYear()、getHours()这种操作耗时极短放在过滤器里完全没问题但如果要格式化的是长列表里几百条时间每条都走一遍formatTime累计消耗就不能忽视了。密集型场景更适合用computed预计算或者把格式化逻辑下沉到数据层在赋值前就处理好。4.3 setInterval 与 Vue 更新队列的配合setInterval的回调里只改数据、不动 DOM这是和 Vue 更新机制配合的正确姿势。如果你在回调里直接写document.getElementById(time).innerText ...等于绕过了 Vue 的响应式系统既享受不到框架的批量更新优化还会让数据和视图出现双份状态时间一长必然不一致。Vue 的异步更新队列有个好处同一 tick 内多次修改数据会合并渲染。比如某个业务逻辑在 1 秒内先改了currentTime又改了userNameVue 会把两次变更合并到一次渲染周期里DOM 只更新一次。这在实时时间场景里意义不大因为每秒就一次赋值但在复杂页面里能显著减少重绘次数。反过来也要注意setInterval本身是不受 Vue 控制的浏览器 API它不理会组件是否销毁。如果组件被销毁了而定时器没清掉回调会在组件实例之外继续执行Vue 实例虽然已经不在了但定时器还在引用着它形成内存泄漏。这就是第 5 章要重点展开的第一个坑。4.4 过滤器是纯函数多实例复用的底气把filterTime和formatTime抽出来看它们都符合纯函数的特征同样的输入永远得到同样的输出函数内部不修改外部状态。这个特性意味着同一个过滤器可以被页面里任意多个节点安全复用不会出现“A 节点用了之后 B 节点格式被改掉”的副作用。实践中常见这样的大屏页面顶部一个当前时间中间日志列表每条带时间戳底部还有统计周期。如果每个位置都单独写格式化逻辑代码冗余不说改格式时得同时改好几处。用同一个过滤器改一处全局生效。这也是为什么原教程第二段选择Vue.filter全局注册的原因——多实例、多节点的场景下纯函数式过滤器比各自为政的局部逻辑更省心。5. 避坑复盘定时器泄漏、月份偏移与时间格式边界5.1 页面销毁后时间还在走定时器泄漏现象在单页应用里切走路由页面组件已经销毁控制台却还在每秒输出时间更新日志切回来时发现组件重新创建了旧定时器和新定时器同时在跑页面出现两个时间源。原因setInterval是浏览器全局 API不会因为 Vue 组件销毁而自动停止。回调里引用了_this等于让定时器一直持有对组件实例的引用组件实例无法被垃圾回收。路由反复切换几次定时器越积越多。解决把定时器 ID 存到实例上在beforeDestroy钩子里清理。created() { this.timer setInterval(() { this.currentTime new Date() }, 1000) }, beforeDestroy() { clearInterval(this.timer) this.timer null }clearInterval(this.timer)告诉浏览器停止对应定时器。养成“创建定时器的地方一定要写清理逻辑”的习惯后这类泄漏基本不会再犯。5.2 月份永远少一月getMonth 从 0 开始现象5 月 6 日在页面上显示成 “4月6日”12 月显示成 “11月”起初以为是时区问题查来查去发现只有月份对不上。原因getMonth()的返回值范围是 0 到 110 代表一月。它和getDate()、getHours()的取值规则不同这两个方法返回的才是自然值。原教程第一段代码直接拼接val.getMonth()没有加 1。解决val.getMonth() 1。这个坑在第二段代码里已经被修正过了——addZero(data.getMonth() 1)——所以两段代码对比着看时正是一个活教材。5.3 个位数不补零14:3:9 的观感问题现象时间显示成14:3:9秒数跳动时数字宽度来回变页面整体在看板类的场景下显得很业余。原因格式化时直接拼接数字getSeconds()返回 9 就拼 9没有做两位数处理。第一段代码完全没有补零逻辑。解决复用addZero或padStart。function addZero(val) { return val 10 ? 0 val : val }addZero之后拼接出来的时间统一是14:03:09的固定宽度。注意函数最终返回的应该是字符串用 val把数字也转一下避免数字和字符串混用带来的隐式转换困惑。5.4 时间戳单位混淆毫秒被当成秒用现象过滤器传入的value是1501068985877时显示正常换成后端接口返回的1501068985后时间变成1970-01-18左右的奇怪日期。原因Date.setTime()接收的是毫秒。如果后端给的是秒级时间戳直接丢给setTime数值小了 1000 倍自然算出来是几十年前。毫秒级时间戳现在是 13 位秒级是 10 位看位数能快速判断。解决在过滤器入口做一次兼容处理。Vue.filter(formatTime, function (value, type) { if (typeof value number value 100000000000) { value value * 1000 // 秒级时间戳转毫秒 } var data new Date() data.setTime(value) // ... 后续格式化逻辑不变 })value 100000000000用来粗略识别秒级时间戳10 位就是秒级。当然最稳的办法还是和后端约定统一单位前端只在接口层做一次转换不把兼容逻辑散落到各个过滤器里。5.5 浏览器后台标签页定时器变慢现象用户把页面切到后台一会儿再回来页面上显示的时间比真实时间慢了一两分钟但又没有完全停住。原因浏览器会对后台标签页的setInterval做节流降低执行频率以节省资源。这是浏览器行为不是 Vue 或代码的问题。时间精度要求高的场景1 秒的定时器在后台可能被拉长到几秒甚至更久。解决监听visibilitychange事件页面重新可见时立刻刷新一次时间。created() { this.timer setInterval(() { this.currentTime new Date() }, 1000) document.addEventListener(visibilitychange, this.handleVisible) }, beforeDestroy() { clearInterval(this.timer) document.removeEventListener(visibilitychange, this.handleVisible) }, methods: { handleVisible() { if (document.visibilityState visible) { this.currentTime new Date() } } }addEventListener的第三个场景还提示了一件事任何在created里挂到全局或document上的监听器都要在beforeDestroy里同步移除否则组件销毁后监听器还活着回调里访问已销毁实例同样会出问题。6. 进阶技巧filter 移除后的组件化迁移Vue 3 移除了过滤器语法管道符写法的{{ currentTime | filterTime }}在 Vue 3 里会直接报错。接手新项目如果用了 Vue 3需要把时间格式化逻辑迁移成computed或普通函数。迁移方式不复杂但正好能把前面学到的生命周期和数据驱动知识串起来。一个 Vue 3 组合式 API 的等价写法script setup import { ref, computed, onMounted, onUnmounted } from vue const currentTime ref(new Date()) function formatTime(date) { const pad (n) String(n).padStart(2, 0) const Y date.getFullYear() const M pad(date.getMonth() 1) const D pad(date.getDate()) const H pad(date.getHours()) const Mi pad(date.getMinutes()) const S pad(date.getSeconds()) return ${Y}-${M}-${D} ${H}:${Mi}:${S} } const displayTime computed(() formatTime(currentTime.value)) let timer null onMounted(() { timer setInterval(() { currentTime.value new Date() }, 1000) }) onUnmounted(() { clearInterval(timer) timer null }) /script template div{{ displayTime }}/div /template这里的computed承担了过滤器原先的功能它声明了displayTime依赖currentTime组件每次渲染时currentTime变了displayTime会自动重新求值。模板里不再有管道符直接绑定计算属性逻辑反而更直观。迁移时注意三点ref(new Date())创建响应式时间对象修改时要用.valueonMounted里建定时器、onUnmounted里清定时器对应 Vue 2 的created和beforeDestroyformatTime函数保持纯函数风格方便在多个组件间共享。如果多个页面都用到实时时间还可以把整个定时器逻辑抽成一个useNow组合式函数返回now和格式化方法每个组件各自调用、各自清理互不干扰。我在实际项目里就是这么处理的——把“定时器 格式化 清理”封好后业务页面只剩一行const now useNow()。从那以后我每次写实时时间相关的代码都强制自己先写销毁逻辑再写更新逻辑定时器一建必清、监听一挂必解。这个习惯帮我挡掉了好多次页面卡死和内存泄漏的线上事故。希望帮到你。本文还有配套的精品资源点击获取