You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Javascript或jQuery统计元素持有指定class的时长

核心问题定位

现有逻辑仅在主题切换时才结算前一个主题的停留时长,当没有主题切换发生(比如用户停在最后一个主题、直接关闭页面、切走浏览器标签),当前活跃主题的时长就没有机会被统计。另外原有代码还存在时长累加逻辑错误的隐藏bug。

现有代码优化方案

优化点说明

  1. 修复时长累加错误的bug,原逻辑错误地将新主题的累计时长加给旧主题
  2. 抽离公共的时长结算函数,避免重复代码
  3. 新增页面卸载、页面隐藏的兜底结算逻辑,覆盖用户直接离开页面的场景
  4. 可选添加定时同步逻辑,避免页面意外崩溃丢失数据

优化后代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 16:15:04