如何使用Javascript或jQuery统计元素持有指定class的时长
核心问题定位
现有逻辑仅在主题切换时才结算前一个主题的停留时长,当没有主题切换发生(比如用户停在最后一个主题、直接关闭页面、切走浏览器标签),当前活跃主题的时长就没有机会被统计。另外原有代码还存在时长累加逻辑错误的隐藏bug。
现有代码优化方案
优化点说明
- 修复时长累加错误的bug,原逻辑错误地将新主题的累计时长加给旧主题
- 抽离公共的时长结算函数,避免重复代码
- 新增页面卸载、页面隐藏的兜底结算逻辑,覆盖用户直接离开页面的场景
- 可选添加定时同步逻辑,避免页面意外崩溃丢失数据
优化后代码
var prevTopicId=0, curTopicId=0, prev_idx=null, scrollItems = $('div.topicrow-header'), anchors = $('.navanchor'), topMenu = curles.find('#lessontopics-'+curlesid), topMenuHeight = topMenu.outerHeight()+15, cur_idx=null; // 抽离公共结算函数 function settleTopicTime(index, forceSettle = false) { if (index === null || !topic_times[index] || topic_times[index].startTime === 0) return; const nowtime = Date.now() / 1000 | 0; const difftime = nowtime - topic_times[index].startTime; // 强制结算时是否校验10秒阈值可根据需求调整 if (difftime > 10 || forceSettle) { topic_times[index].totalTime += difftime; topic_times[index].endTime = nowtime; } topic_times[index].startTime = 0; } // 兜底结算:页面隐藏/卸载时结算当前活跃主题 window.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { settleTopicTime(cur_idx, true); } }); window.addEventListener('beforeunload', () => { settleTopicTime(cur_idx, true); }); // 可选:每30秒同步一次当前活跃主题的时长,避免意外崩溃丢失数据 setInterval(() => { if (cur_idx !== null && topic_times[cur_idx].startTime !== 0) { const nowtime = Date.now() / 1000 | 0; const difftime = nowtime - topic_times[cur_idx].startTime; if (difftime > 10) { topic_times[cur_idx].totalTime += difftime; topic_times[cur_idx].startTime = nowtime; // 更新开始时间避免重复累加 } } }, 30 * 1000); $(window).on('scroll', _.throttle(function() { // Get container scroll position var fromTop = $(this).scrollTop()+topMenuHeight; var cur = anchors.map(function(){ if (($(this).offset().top < fromTop) && (this.getBoundingClientRect().top < .25*topMenuHeight )) { return this; } }); if (cur.length !== 0) { cur = cur[cur.length-1]; var id = cur.id; if ((cur.classList.contains('topicrow-header'))) { curTopicId = id; if (curTopicId !== prevTopicId) { // 先结算上一个主题的时长 settleTopicTime(prev_idx); // 给新主题加sticky类,记录开始时间 console.log('setting active sticky item first time for '+curTopicId); $.each(scrollItems, function( index, value ) { if (value.id === curTopicId) { $(value).addClass("stick"); topic_times[index].startTime = Date.now() / 1000 | 0; cur_idx = index; return false; } }); // 移除上一个主题的sticky类 if (prevTopicId !== 0) { console.log('removing previous active sticky item for ' + prevTopicId); scrollItems.filter('#' + prevTopicId).removeClass('stick'); } prev_idx = cur_idx; prevTopicId = curTopicId; } } } }, 100)); // 建议给throttle加固定时间,避免滚动触发太频繁
通用成熟实现方案(与业务逻辑解耦)
如果需要通用的「元素持有指定class的时长统计」能力,不要和滚动逻辑耦合,推荐用MutationObserver监听元素class属性变化,稳定性更高,适配所有class变更场景:
const watchClass = 'stick'; const topicDurationMap = {}; // 存储每个主题的时长数据,key为主题id const observerMap = new Map(); // 存储每个元素的observer实例 // 初始化监听所有主题元素 scrollItems.each(function() { const topicId = this.id; // 初始化时长数据 topicDurationMap[topicId] = { totalTime: 0, startTime: 0 }; // 监听class变化 const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === 'class') { const hasSticky = mutation.target.classList.contains(watchClass); const topicData = topicDurationMap[topicId]; if (hasSticky && topicData.startTime === 0) { // 刚添加sticky类,记录开始时间 topicData.startTime = Date.now() / 1000 | 0; } else if (!hasSticky && topicData.startTime !== 0) { // 刚移除sticky类,结算时长 const now = Date.now() / 1000 | 0; const diff = now - topicData.startTime; if (diff > 10) { topicData.totalTime += diff; } topicData.startTime = 0; } } }); }); observer.observe(this, { attributes: true }); observerMap.set(topicId, observer); }); // 兜底结算逻辑 window.addEventListener('beforeunload', () => { Object.keys(topicDurationMap).forEach(topicId => { const data = topicDurationMap[topicId]; if (data.startTime !== 0) { const now = Date.now() / 1000 | 0; const diff = now - data.startTime; if (diff > 10) { data.totalTime += diff; } } }); // 这里可以上报topicDurationMap的统计数据 });
内容的提问来源于stack exchange,提问作者Brian
相关产品推荐
相关产品推荐

