在jQuery中,removeClass和addClass是使用频率极高的DOM操作。当一个元素在短时间内被反复添加或移除class时,如果每次都直接读取或写入elem.className,浏览器会强制进行样式计算甚至布局重排。jQuery为了避免这种开销,在内部使用了局部变量缓存className字符串的做法:先读取一次,后续处理都在内存中完成,最后统一写回。本文将剖析这个缓存机制,并结合自定义实现说明如何在高频操作中应用同样的优化思路。

DOM读写成本与className缓存的意义
每次访问元素的className属性,都可能触发浏览器的样式系统重新计算。className是内联状态的一部分,读取时浏览器需要确保返回的值是最新的,这会迫使它刷新样式队列;写入时则会使元素样式失效,并在下一次读取布局信息时触发reflow。当removeClass或addClass在循环中被大量调用时,如果每个操作都直接读写DOM,页面会出现明显的卡顿。
缓存的意义在于将多次DOM访问合并为一次。可以先通过elem.className获取当前class字符串,然后在JavaScript字符串层面完成包含判断、拆分、去重、拼接,所有中间结果保存在局部变量中。整个处理过程中不再触碰DOM,最后只把拼接好的新字符串赋值给elem.className。这样无论执行多少次addClass或removeClass逻辑,实际DOM读写仍然只有两次。
对于批量操作多个元素,这种优化更加明显。如果循环内先整体收集需要变更的class集合,再逐个元素一次性更新,就能避免每个元素因为频繁读写className而导致反复样式计算。jQuery在removeClass和addClass的实现中正是采用类似思路,借助局部变量保存原始className字符串,并通过正则表达式或classList完成更新。
jQuery removeClass/addClass 的缓存实现原理
以jQuery 3.x的addClass方法为例,它的核心逻辑会先遍历匹配到的元素,对每个元素读取elem.className并存入局部变量curValue,后续的class合并、拆分、去重都基于这个局部变量进行。代码会先判断是否需要处理,然后根据是否支持classList选择不同的路径,但两条路径都遵循先读后写的原则,避免重复读取DOM。
在内部实现中,函数会构造一个临时数组或字符串,保存最终要写入的class列表。如果原生支持classList,jQuery会优先使用classList.add或classList.remove,因为浏览器对classList的批量操作比手动拼接字符串更高效,而且能避免字符串分割带来的额外开销。但即使是classList路径,jQuery也会在前面读取一次className作为基准,并不会在每个单独的class操作时都重新读取DOM。
下面是一段简化后的源码逻辑,展示了如何用局部变量缓存className并完成更新:
function addClass(elem, value) {
var curValue = elem.className,
classes = (value || '').match(/\S+/g) || [],
curClasses = curValue.match(/\S+/g) || [],
finalClasses = curClasses.slice(),
i, len, cls;
for (i = 0, len = classes.length; i < len; i++) {
cls = classes[i];
if (finalClasses.indexOf(cls) === -1) {
finalClasses.push(cls);
}
}
elem.className = finalClasses.join(' ');
}
这个方法只读取一次elem.className,将所有判断和拼接在内存中完成,最后一次性写回。removeClass的实现类似,只不过方向是从finalClasses中移除指定项。这种缓存策略避免了在频繁操作时反复触发DOM读写。
在实际jQuery源码中还会考虑SVG元素、旧浏览器兼容以及classList的降级处理,但缓存className的核心思想不变:用局部变量代替直接DOM属性访问。
实现一个带缓存的class操作工具
如果不想引入jQuery,也可以自己实现一个带缓存的addClass和removeClass工具。核心是维护一个缓存对象,保存每个元素的class列表,更新时基于缓存计算变化,减少对className频繁读取。下面是一个示例:
var classCache = (function() {
var cache = new WeakMap();
function getClasses(el) {
if (!cache.has(el)) {
cache.set(el, (el.className || '').match(/\S+/g) || []);
}
return cache.get(el);
}
function flush(el, classes) {
el.className = classes.join(' ');
cache.set(el, classes.slice());
}
function addClass(el, cls) {
var list = getClasses(el).slice();
if (list.indexOf(cls) === -1) {
list.push(cls);
flush(el, list);
}
}
function removeClass(el, cls) {
var list = getClasses(el).slice();
var idx = list.indexOf(cls);
if (idx !== -1) {
list.splice(idx, 1);
flush(el, list);
}
}
return {
addClass: addClass,
removeClass: removeClass
};
})();
这个实现使用WeakMap缓存元素的class数组,调用addClass时先检查缓存,如果class不存在则更新缓存并写回DOM。当class已经存在或已经移除时,不触发DOM写操作,进一步减少样式计算。对于高频场景,比如在requestAnimationFrame回调中切换状态,这种缓存能明显减少布局抖动。
与直接操作classList相比,带缓存的版本可以避免在add/remove判断时出现重复的DOM写入。原生classList.add本身也有去重功能,但在某些旧浏览器中,classList会为每个操作触发样式失效,多次连续调用仍然有性能损耗。缓存机制帮助我们在调用前就知道是否需要真正修改DOM,从而跳过无意义的写操作。
缓存机制的性能边界与避坑建议
缓存className虽然能提升性能,但必须保证缓存与实际DOM状态一致。如果代码中其他部分直接修改了elem.className或使用classList,而没有同步更新缓存,后续的缓存操作就可能产生错误结果。因此,如果选择在项目中自建缓存,要么完全封装所有class操作入口,要么在每次操作前重新读取DOM来刷新缓存,权衡一致性与性能。
另外,对于高度动态的场景,例如由框架自动管理class的组件,手写缓存可能带来维护风险。现代浏览器的classList已经做了大量优化,连续多个classList.add调用通常会被合并到同一次渲染帧中,不一定需要手动缓存。实际上,jQuery在新版本中也更倾向于依赖classList,只在兼容路径中才使用字符串拼接缓存。
在高频removeClass/addClass优化中,更优先的手段是减少DOM操作频率本身,比如将多个class变更合并成一次调用,使用requestAnimationFrame节流,或者在动画循环外统一更新。className缓存只是这些优化中的一环,它解决的是多次DOM属性读写带来的强制同步布局问题,而不是替代整体架构上的批量更新。
jQuery className缓存removeClass addClass优化高频操作修改时间:2026-08-21 15:54:05