
简介面向 Vue 初、中级开发者的 filter 过滤器专题资料围绕“当前时间实时更新”场景先说明客户端与服务器之间的过滤器概念再聚焦 Vue 自定义过滤器写法。内容从模板语法 {{ currentTime | filterTime }} 出发演示通过 created 生命周期中的 setInterval 定时器每秒更新 currentTime配合 filterTime 输出“年月日时分秒”格式并补充补零处理同时给出 formatTime 日期过滤器覆盖 YMD、YMDHMS、HMS、YM 等格式方便按需选择。示例涵盖 HTML 模板、Vue 实例、filters 配置与 created 钩子可对照学习完整的代码组织方式。资源为 1 份 PDF压缩包大小 49KB文件不多但要点集中适合想在项目中格式化时间、做实时时钟或倒计时以及希望理解过滤器与生命周期配合使用的读者。已有 263 人学习下载阅读后可掌握从过滤器定义、定时触发、格式转换到页面刷新的完整思路直接借鉴到实际开发中。1. Vue filter 过滤当前时间格式化靠 filter实时更新靠什么接手后台系统时页脚要显示当前时间还要每秒走动。很多人第一反应是写一个 Vue filter 把时间戳格式化成人能读的字符串结果时间显示出来了却一动不动。原因很简单filter 是纯函数它负责把传进去的值格式化但不会主动刷新视图。这个需求实际上要拆成两件事格式化交给 filter实时更新则需要用定时器周期性地修改响应式数据让模板重新求值。这篇文章就沿着这两条线讲先给你能直接跑的代码再把响应式刷新、定时器清理、时区解析这些坑一个个说清楚。适合刚接触 Vue 2 过滤器的同学也适合正在把老项目往 Vue 3 迁的熟手。2. 先定时间格式Vue.filter 注册、串行调用与格式化函数的参数边界Vue 2 的过滤器设计得像 Unix 管道值在前规则在后。它最大的价值是让模板里的时间显示意图非常清晰数据从后端回来是什么格式不用管展示层用一个管道符搞定。但它的边界也很明确过滤函数拿不到组件实例也就是访问不了this所以凡是依赖组件状态、路由信息、用户时区偏好的格式化都不适合放进 filter。先把这个边界立住后面调参数才不会反复改结构。2.1 为什么当前时间格式化最适合用 filter纯函数管道的取舍写{{ timestamp | formatTime(YYYY-MM-DD HH:mm:ss) }}比写{{ formatTime(timestamp, YYYY-MM-DD HH:mm:ss) }}看起来更清爽尤其在模板里同时出现好几个时间字段时管道写法能少一层嵌套阅读顺序也更接近自然语言先看到数据再看到怎么处理。过滤器还支持串联比如{{ value | toDate | formatTime(HH:mm) }}前一个函数的输出会变成后一个函数的输入这在做「先解析再格式化」的两步处理时非常方便。但取舍也很关键。过滤器注册一次是全局的意味着所有组件都能用也意味着如果某个格式只有某一个页面用全局注册反而污染了命名空间。另外过滤器函数必须是纯的不能访问this不能做异步操作。我之前在这个上面翻过车想在过滤器里根据当前用户时区动态显示时间写了一半发现拿不到用户信息最后只能改成方法调用。所以选型时可以记一条标准格式规则是静态的、与业务状态无关的用 filter格式规则依赖组件数据的用 methods 或 computed。2.2 注册一个全局 formatTime 过滤器最小可用代码与占位符下面这段是 Vue 2 项目里最常见的全局注册写法直接放在入口文件里。格式化函数支持传入时间戳、日期字符串或者Date对象并且用pad函数补零避免输出2024-5-3 8:5:9这种不整齐的格式。// 全局注册 formatTime 过滤器适用于 Vue 2 function pad (n) { return n 10 ? 0 n : n } function formatTime (value, pattern) { // 不传 pattern 时给一个默认格式避免 replace 报错 pattern pattern || YYYY-MM-DD HH:mm:ss if (!value) return // 兼容时间戳、日期字符串和 Date 对象 let date value instanceof Date ? value : new Date(value) if (isNaN(date.getTime())) return const map { YYYY: date.getFullYear(), MM: pad(date.getMonth() 1), // getMonth() 从 0 开始必须 1 DD: pad(date.getDate()), HH: pad(date.getHours()), mm: pad(date.getMinutes()), ss: pad(date.getSeconds()) } return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, function (key) { return map[key] }) } Vue.filter(formatTime, formatTime)模板里这样用template span{{ timestamp | formatTime(YYYY-MM-DD HH:mm:ss) }}/span /template这段代码的关键点有三个。第一pattern.replace里用的正则占位符都是两位长度所以MM和mm不会互相干扰但如果你扩展出M、D、H这样的单字符占位符正则必须按长度降序排列否则MM会被拆成两个M。第二date.getMonth()返回的是 0 到 11直接输出会差一个月这是新手最容易踩的。第三isNaN(date.getTime())这个校验能拦截掉非法输入不然格式化一个Invalid Date会输出一串NaN-NaN-NaN。2.3 传参与串联filter 不是「当前时间」的开关过滤器传参的语法是{{ value | filterName(参数1) }}多个参数用逗号分隔。串联调用的执行顺序是从左到右前一个函数的返回值作为后一个函数的第一个入参。比如先解析再格式化template span{{ rawTime | parseDate | formatTime(HH:mm) }}/span /template设计合理的串联可以让管道语义非常清楚但有一个使用误区必须提醒不要在 filter 内部写new Date()。虽然这样模板里写{{ new Date() | formatTime(HH:mm:ss) }}也能显示一个时间但filter只在渲染时执行一次之后没有任何机制让它重新执行。这个写法给人的错觉是「filter 会自动获取当前时间」实际上它获取的只是「渲染那一刻的时间」。真正的实时更新需要数据层面配合这也是下一章要解决的问题。3. 让时间真正走起来响应式刷新策略与定时器最小实现实时更新这件事本质上和 filter 无关。filter 只是「显示层格式化」真正驱动它重新执行的是 Vue 的响应式系统当模板依赖的响应式数据发生变化组件重新渲染filter 才会再次被调用。所以核心思路是让「当前时间」成为一个持续变化的响应式数据然后用定时器去改它。3.1 核心思路刷新的是数据不是过滤器Vue 2 的响应式系统通过Object.defineProperty拦截属性的读写。当你在模板里写了{{ now | formatTime(HH:mm:ss) }}模板渲染时会读取this.now这个读取动作会把「当前组件」记录下来成为now这个属性的依赖。之后只要now被重新赋值Vue 就会通知组件重新渲染filter 随之重新执行。这里有一个关键细节now建议存「时间戳数字」而不是Date对象。原因不是Date对象不能用而是很多人会写出这样的代码this.date.setSeconds(this.date.getSeconds() 1)。这行代码修改了Date对象的内部状态但没有给date属性重新赋值Vue 检测不到变化视图自然不动。而this.now Date.now()是一次真正的属性赋值setter 会被触发渲染才会更新。这个坑放到下一章单独讲这里先记住结论用整体替换不要修改对象内部。3.2 最小实现data 里放时间戳mounted 里开定时器直接给一套能跑的完整组件代码。这是 Vue 2 的写法也是旧项目里最常见的落地方式template div{{ now | formatTime(YYYY-MM-DD HH:mm:ss) }}/div /template script export default { name: CurrentTimeDemo, data () { return { now: Date.now() // 用时间戳数字避免 Date 对象内部修改不触发更新 } }, mounted () { // 定时器要么不写写就必须同时写清理逻辑 this.timer setInterval(this.tick, 1000) }, beforeDestroy () { clearInterval(this.timer) this.timer null }, methods: { tick () { // 每次生成新的时间戳值的引用变化后 filter 才会重新执行 this.now Date.now() } } } /script定时器为什么放在mounted而不是created两种都可以因为这里只修改数据不操作 DOM。我习惯放mounted是想着万一以后定时器回调里要直接访问 DOM 节点这时节点一定已经渲染完成不会出现拿不到的情况。setInterval的间隔和格式化精度要配套显示秒就 1000ms 一次只显示到分把间隔改成 60000ms。但这里马上会碰到一个真实问题如果间隔是 60000ms页面在 12:00:59 挂载下一次触发是 12:01:59而在这 60 秒内模板显示的分钟一直是 12:00。换句话说分钟级更新不是对齐整分钟的而是对齐「挂载时刻」的。要真正在整分钟边界更新需要用 setTimeout 计算到下一个整分钟的时间差这个技巧放在最后一章讲。3.3 备选方案$forceUpdate 强制刷新与它的代价还有一种常见做法不存任何时间字段模板直接写{{ new Date() | formatTime(HH:mm:ss) }}定时器里只调this.$forceUpdate()。每次强制重新渲染模板里的new Date()都会重新执行filter 拿到的就是新时间。mounted () { this.timer setInterval(() { this.$forceUpdate() }, 1000) }这个方案代码更少但代价也明显$forceUpdate会让整个组件重新渲染而不是精准刷新某一个插值。组件里如果还有列表、图表之类的重内容每秒强制渲染一次性能压力会很大。而且它不会递归触发子组件更新如果你以为「父组件强制刷新子组件也跟着刷新」那就错了。我的经验是这个写法只适合页面本身就极轻、且只有一两个时间展示的场景。组件一旦复杂老老实实按 3.2 的方案走。另外再提一个和 3.2 等价的「笨办法」在 data 里放一个没实际用途的计数器定时器里this.counter。模板里的now本身不变化时counter的变化也会带动整个组件渲染。这个办法能用但语义不清晰后来我做代码评审时看到这种写法一般会建议直接改成时间戳刷新因为计数器名字和真实业务没有关联过三个月自己都看不懂。3.4 一个容易翻车的时间误差写法不要写「基础时间 每秒累加」的逻辑。比如先把初始时间存下来每次 tick 里给初始时间setSeconds(初始.getSeconds() 1)再把结果赋给now。这个方案在浏览器正常运行没问题但一旦标签页切到后台浏览器会大幅节流setInterval回调可能几秒甚至几十秒才执行一次累加出来的时间就和真实时间严重偏离。等用户切回页面看到的时间已经慢了好几分钟。正确做法是每次 tick 都直接取Date.now()让系统时钟来做校准。定时器只是一个「触发信号」时间值永远来自系统这是实时时间组件保持准确的最简单也最可靠的原则。4. 避坑排查实时时间过滤里的 4 个高频问题这块我直接按「现象 → 原因 → 解决」的顺序写都是实际项目里反复遇到的问题前三个和定时器、响应式、时区相关第四个是后台节流引发的显示异常。4.1 路由跳走再回来控制台还在每秒打日志内存只涨不降现象组件里开了定时器打印日志切换路由离开页面后日志还在输出页面来回多切几次明显感觉浏览器变卡。原因组件销毁时没有清理定时器。Vue 只负责销毁组件的 DOM 和响应式实例不会替你clearInterval。如果定时器回调里用了箭头函数且没有把 timer 句柄存到this上那连清理的机会都没有这个定时器就成了永远停不下来的野定时器。解决养成「写定时器就同时写清理」的习惯。mounted里开beforeDestroy里清。如果组件被keep-alive缓存beforeDestroy在路由切换时不会触发这时要改用activated里重启、deactivated里停止。判断方法很简单DevTools 里切路由后看 Network 或者 Console 是否还有输出有就是没清干净。4.2 显示的时间比手机本地时间差了好几个小时现象同一个时间戳在电脑浏览器和手机浏览器上显示的小时数不一样有的差 8 小时有的差 5 小时。原因日期字符串解析的时区规则不一致。服务器返回2024-01-01 00:00:00这种不带时区标记的字符串浏览器会把它当作本地时间解析但服务器本意可能是 UTC 时间反过来返回2024-01-01T00:00:00Z这种带 Z 的 ISO 字符串浏览器会正确解析为 UTC 时间可你在formatTime里用getHours()取的又是本地时区的小时数自然就偏移了。解决在格式化函数里加一个useUTC参数。true时用getUTCFullYear、getUTCHours这一组方法false时用本地的。后端接口的时间格式也要统一要么全部带时区偏移要么明确在接口文档里写明是 UTC。这个问题靠前端单方面修是修不干净的前后端约定必须一起改。4.3 时间在跳页面纹丝不动或者跳了一次就停住现象控制台打印this.now每次都变但页面上显示的时间永远是同一个值刷新页面后才正常一次。原因这是 3.1 里提到的 Date 对象内部修改问题。this.date.setSeconds(this.date.getSeconds() 1)改的是date对象内部的数据date这个属性的引用没有变Vue 的响应式系统根本感知不到变化。还有一种情况是 data 里压根没初始化now只在mounted里赋值模板虽然显示了但依赖没有被收集后续再怎么赋值都触发不了更新。解决this.now new Date()或this.now Date.now()整体替换引用。data 里先初始化now: null保证模板在首次渲染时就读到这个属性依赖收集才能生效。如果你用了 3.3 的$forceUpdate方案就不要在 data 里再放now两套机制混用会导致重复刷新时间倒是准的但白耗性能。4.4 切后台再回来秒针跳了一大截或者看起来卡住几秒现象页面挂在后台标签页几分钟切回来发现秒数比真实时间慢了几十秒然后又突然跳回正确值。原因浏览器为了省电会对后台标签页的setInterval做节流间隔可能从 1000ms 被拉长到 1000ms 以上严重时达到分钟级。所以定时器回调实际执行的周期是不稳定的页面切回前台后要等下一个节流周期结束显示才会追上真实时间。解决监听visibilitychange页面从隐藏变为可见时立刻补一次刷新再重置定时器。下面这段加在mounted里beforeDestroy里记得移除监听mounted () { this.timer setInterval(this.tick, 1000) document.addEventListener(visibilitychange, this.onVisible) }, beforeDestroy () { clearInterval(this.timer) document.removeEventListener(visibilitychange, this.onVisible) }, methods: { onVisible () { if (!document.hidden) { this.tick() } } }参数说明document.hidden为true表示页面当前不可见切回前台时隐藏状态变为false此时手动执行一次tick页面就能立刻显示正确时间不用干等下一个定时器周期。5. Vue 3 已经移除了 filter当前时间格式化的两个替代做法如果你在用 Vue 3 开发新项目前面所有Vue.filter注册代码都不能用模板里写| formatTime会直接编译报错。Vue 3 对过滤器的处理是彻底移除官方把选择权交给你要么用函数调用要么用 computed。迁移并不复杂但思路要转过来。5.1 从管道到函数filter 被移除的原因与迁移对照filter 在 Vue 2 里有一些别扭的地方它拿不到组件实例所有依赖外部状态的格式化都得通过闭包或者全局变量搞定它需要单独注册团队成员如果不知道这个全局函数存在很容易重复造轮子。Vue 3 把模板语法收紧了原本的 filter 用法直接改成函数调用语义更接近原生 JavaScript。迁移对照就是下面这个映射关系。Vue 2 写法template span{{ time | formatTime(HH:mm:ss) }}/span /templateVue 3 等价写法template span{{ formatTime(time, HH:mm:ss) }}/span /template全局注册的Vue.filter可以改成普通工具函数导入组件后在setup里返回或者挂在app.config.globalProperties上。工具函数本身不需要绑定 Vue它就是一段纯 JavaScript测试起来反而更方便。5.2 Composition API 方案ref 存时间戳computed 管格式化Vue 3 里做实时时间展示最顺手的方案是ref存时间戳computed做格式化。computed会自动追踪now的变化now每次更新timeText都会重新计算模板里只需要绑定timeText不需要关心格式化逻辑放在哪里。import { ref, computed, onMounted, onUnmounted } from vue setup () { const now ref(Date.now()) let timer null function tick () { now.value Date.now() } onMounted(() { timer setInterval(tick, 1000) }) onUnmounted(() { clearInterval(timer) }) const timeText computed(() formatTime(now.value, YYYY-MM-DD HH:mm:ss)) return { timeText } }参数说明now是唯一的响应式数据源computed的缓存特性在这里能避免模板里每次渲染都重新执行格式化函数虽然这次渲染本来就只依赖now一个值缓存收益不明显但语义上比直接调用函数更清晰。formatTime函数可以直接复用 Vue 2 时期写的那个它不依赖任何框架复制到utils/time.js就能用。5.3 要不要引入 dayjs一个 90% 场景都用不上的纠结很多人在这一步会纠结要不要引 dayjs。我的判断标准只有一个你需不需要复杂的相对时间计算和时区转换。如果只是YYYY-MM-DD HH:mm:ss这种固定格式展示手写 20 行函数已经覆盖了所有需求引入库反而增加打包体积和学习成本。如果业务里要处理「两个时间差多少天」「某时区用户的本地时间」「跨月跨年的日期运算」这类问题才值得引入成熟的日期库。dayjs 的优势是插件机制明确按需加载utc、timezone、relativeTime插件体积控制得比老牌时间库好很多。但有一个边界要说清楚不要因为引了 dayjs 就把前面的formatTime函数全部丢掉组件里如果只展示一个当前时间直接用原生Date方法就够了。我见过最典型的过度设计是只为了显示一个页脚时间在项目里引入了完整的时间库和时区数据包构建体积涨了一截运行时的效果和原生写法一模一样。6. 进阶做法把实时时间封装成可复用组件顺手解决跨天和整分钟对齐到了这一章前面的知识基本都能串起来了。我现在如果新开一个需要实时时间的页面不会在每个组件里重复写定时器和清理逻辑而是封装成一个TimeText组件格式、刷新间隔、是否用 UTC 都通过 props 控制用的时候一行标签搞定。6.1 封装 TimeText 组件props 控制格式、间隔和时区下面是一个 Vue 3 的完整组件写法用script setup语法。核心逻辑和第五张一样但把格式、间隔、时区偏好都做成了可配置项template span :titlefullTime{{ currentText }}/span /template script setup import { ref, computed, onMounted, onUnmounted } from vue import { formatTime } from /utils/time const props defineProps({ format: { type: String, default: YYYY-MM-DD HH:mm:ss }, interval: { type: Number, default: 1000 }, useUTC: { type: Boolean, default: false } }) const now ref(Date.now()) let timer null function tick () { now.value Date.now() } function onVisible () { if (!document.hidden) { tick() } } onMounted(() { tick() timer setInterval(tick, props.interval) document.addEventListener(visibilitychange, onVisible) }) onUnmounted(() { clearInterval(timer) document.removeEventListener(visibilitychange, onVisible) }) const currentText computed(() formatTime(now.value, props.format, props.useUTC)) const fullTime computed(() formatTime(now.value, YYYY-MM-DD HH:mm:ss)) /script使用方式TimeText formatHH:mm:ss / TimeText formatYYYY-MM-DD :interval60000 /参数说明interval控制刷新频率秒级展示传 1000分钟级传 60000useUTC传入true时格式化函数内部会走getUTC*那一组方法:titlefullTime让鼠标悬停时能看到完整的年月日是一个低成本的好细节。6.2 整分钟对齐用 setTimeout 代替 setInterval 的一个技巧前面提到过分钟级展示如果直接把interval设为 60000更新时刻取决于组件挂载时间而不是整分钟边界。解决方法是计算到下一个整分钟的时间差用递归 setTimeout 来安排下一次执行这样每次触发都精准落在分钟边界上function scheduleMinuteTick () { const now new Date() const delay (60 - now.getSeconds()) * 1000 - now.getMilliseconds() setTimeout(() { tick() scheduleMinuteTick() }, delay) }delay的计算逻辑是60 - 当前秒数得到距离下一个整分钟还有多少秒再乘以 1000 转成毫秒最后减去当前毫秒数让触发点准确对齐到整分钟的00毫秒。这个方案的代价是每次触发后都要重新计算下一次延迟比setInterval写起来麻烦一点但分钟级展示的场景下它能避免「分钟纹丝不动几十秒然后突然跳变」的观感。6.3 手工验证清单交付前花两分钟测这三项第一项把页面切到后台等 30 秒再切回来看秒数是否立刻恢复正常。如果停住了说明visibilitychange监听没生效检查一下document.hidden的判断条件。第二项在路由里加一个离开当前页面的按钮切走后再切回来用 DevTools 的 Performance 面板看有没有定时器继续触发这能验证清理逻辑是否真的生效。第三项把电脑时区改成一个和自己当前所在不同的时区刷新页面看时间变化是否符合预期这能确认useUTC分支没有把所有页面都变成 UTC 显示。我之前接手过一个维护了多年的后台实时时间组件分散在四个页面里每个页面都是自己复制粘贴一套定时器代码有的清了定时器有的没清。后来统一收敛成这个TimeText组件问题才彻底消失。现在我的习惯是任何时间展示组件先写清理函数再考虑格式化和刷新策略格式化反而是最不着急的部分。希望帮到你。本文还有配套的精品资源点击获取