简介:Vue filter 过滤当前时间并实现实时更新效果的 PDF 教程,面向需要掌握 Vue 自定义过滤器及生命周期钩子用法的初中级前端开发者。资源仅含 1 个 PDF 文件,体积 49KB,内含可直接复制的完整 HTML+JavaScript 示例,从模板绑定、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 lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Vue Filter 实时时间</title> <!-- 老项目里常见用法:CDN 引入 Vue 2.x --> <script src="https://unpkg.com/vue"></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() 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() | 四位年份 | 如 2024 |
getMonth() | 月份 | 0 ~ 11,注意从 0 开始 |
getDate() | 日期 | 1 ~ 31 |
getHours() | 小时 | 0 ~ 23 |
getMinutes() | 分钟 | 0 ~ 59 |
getSeconds() | 秒 | 0 ~ 59 |
这里藏着一个全教程最典型的坑:getMonth()返回的月份是从 0 开始计数的。1 月是 0,12 月是 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 lang="en"> <head> <meta charset="UTF-8"> <title>Vue 全局时间过滤器</title> <script src="https://unpkg.com/vue"></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 } 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> </html>Vue.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)返回"03",addZero(12)返回"12",返回结果全是字符串,行为更一致。两种写法的边界差异在于:val超过两位数时,if (val < 10)的写法返回原数字,padStart写法返回原字符串,效果一样。
3.3 四种 type 的参数分支与调用方式
全局过滤器formatTime接收两个参数:value和type。value是管道符左边传进来的值,在这个例子里是message,也就是时间戳1501068985877;type是模板管道写法里括号里传的参数,比如formatTime('YMD')里的'YMD'。
过滤器内部先用var data = new Date()创建日期对象,再调用data.setTime(value)把时间戳设置为该日期对象的时间。这里value是毫秒级时间戳,setTime接收的正是毫秒数,所以能正确解析。
四种type对应的输出格式如下:
| type | 返回格式 | 示例(基于 2024-05-06 14:03:09) |
|---|---|---|
YMD | 年-月-日 | 2024-05-06 |
YMDHMS | 年-月-日 时:分:秒 | 2024-05-06 14:03:09 |
HMS | 时:分:秒 | 14:03:09 |
YM | 年-月 | 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的 getter,Vue 会把这个渲染函数记为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,又改了userName,Vue 会把两次变更合并到一次渲染周期里,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 到 11,0 代表一月。它和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())创建响应式时间对象,修改时要用.value;onMounted里建定时器、onUnmounted里清定时器,对应 Vue 2 的created和beforeDestroy;formatTime函数保持纯函数风格,方便在多个组件间共享。
如果多个页面都用到实时时间,还可以把整个定时器逻辑抽成一个useNow组合式函数,返回now和格式化方法,每个组件各自调用、各自清理,互不干扰。我在实际项目里就是这么处理的——把“定时器 + 格式化 + 清理”封好后,业务页面只剩一行const now = useNow()。
从那以后,我每次写实时时间相关的代码,都强制自己先写销毁逻辑再写更新逻辑,定时器一建必清、监听一挂必解。这个习惯帮我挡掉了好多次页面卡死和内存泄漏的线上事故。希望帮到你。
本文还有配套的精品资源,点击获取