TypeScript中Knockout实现的moment.utc()连续调用非顺序问题
解决Knockout+TypeScript中时间Observable返回非顺序时间的问题
我来帮你分析下代码里的问题,以及给出修复方案:
问题根源
你的时间工具类出现时间非顺序的情况,主要有这几个原因:
PureComputed的休眠机制坑
你用了KnockoutpureComputed的awake/asleep事件来启停定时器,但如果只是单次调用utcNowWithSecondPrecision()(而不是订阅它的变化),Knockout会在短时间后把这个计算属性标记为“休眠”,直接停掉定时器。下次再调用时,会重新唤醒并把时间重置为当前系统时间——如果这段时间系统因为NTP同步等原因把时间回退了,那新返回的时间就会比之前的早,自然就乱序了。定时器管理有漏洞
每次触发awake时你直接启动新定时器,但如果短时间内多次调用(比如连续两次读取时间),asleep事件还没来得及触发,旧的定时器还在跑,这时候又启动新的,就会有多个定时器同时更新时间。虽然不会直接导致时间回退,但会造成资源浪费和更新混乱。Moment对象的可变性隐患
虽然你每次都生成新的moment.utc()实例,但调用now.add(...)会直接修改这个实例本身——如果在定时器更新前你再次读取时间,拿到的就是被修改过的错误时间。
修复后的代码
我调整了代码,解决了这些问题,同时保证时间始终按顺序递增:
import * as ko from "knockout"; import * as moment from "moment"; class TimeHelpers { private static createTimeBasedObservable(updatePeriodInMillis: number) { const nowTime: KnockoutObservable<moment.Moment> = ko.observable(moment.utc()); let interval: number | undefined; let lastTime: moment.Moment = moment.utc(); // 记录上一次时间,保证不回退 // 确保时间始终递增,即使系统时间回退 const getMonotonicTime = () => { const current = moment.utc(); if (current.isBefore(lastTime)) { // 系统时间回退时,返回上一次时间+1毫秒,维持顺序 return lastTime.clone().add(1, "millisecond"); } lastTime = current; return current; }; // 安全启动定时器:避免重复创建 const startInterval = () => { if (interval !== undefined) return; nowTime(getMonotonicTime()); interval = window.setInterval(() => { nowTime(getMonotonicTime()); }, updatePeriodInMillis); }; // 安全停止定时器 const stopInterval = () => { if (interval !== undefined) { window.clearInterval(interval); interval = undefined; } }; // 暴露时间的计算属性,返回克隆防止外部修改 const computed = ko.pureComputed(() => nowTime().clone()); // 用awake/asleep管理定时器启停,节省资源 computed.subscribe(startInterval, this, "awake"); computed.subscribe(stopInterval, this, "asleep"); return computed; } static readonly utcNowWithMinutePrecision = TimeHelpers.createTimeBasedObservable(60 * 1000); static readonly utcNowWithSecondPrecision = TimeHelpers.createTimeBasedObservable(1000); } export = TimeHelpers;
关键修复点说明
- 时间单调性保证:新增
getMonotonicTime函数,每次获取时间都会和上一次对比,如果系统时间回退,就返回上一次时间+1毫秒,绝对不会出现时间倒序的情况。 - 定时器安全管理:
startInterval会先检查是否已有定时器在运行,避免重复创建;stopInterval确保清理时不会出错。 - 防止意外修改:计算属性返回的是时间实例的克隆,外部调用
add等方法只会修改克隆后的对象,不会影响内部存储的原始时间值。
简化版本(如果不需要休眠机制)
如果这个时间工具是全局常用的,不需要为了节省资源启停定时器,可以用更简单的版本:
import * as ko from "knockout"; import * as moment from "moment"; class TimeHelpers { private static createTimeBasedObservable(updatePeriodInMillis: number) { const nowTime: KnockoutObservable<moment.Moment> = ko.observable(moment.utc()); let lastTime: moment.Moment = moment.utc(); const getMonotonicTime = () => { const current = moment.utc(); if (current.isBefore(lastTime)) { return lastTime.clone().add(1, "millisecond"); } lastTime = current; return current; }; // 初始化就启动定时器,一直运行 window.setInterval(() => { nowTime(getMonotonicTime()); }, updatePeriodInMillis); // 返回克隆的时间实例 return ko.pureComputed(() => nowTime().clone()); } static readonly utcNowWithMinutePrecision = TimeHelpers.createTimeBasedObservable(60 * 1000); static readonly utcNowWithSecondPrecision = TimeHelpers.createTimeBasedObservable(1000); } export = TimeHelpers;
内容的提问来源于stack exchange,提问作者Danielle
相关产品推荐
相关产品推荐

