在jQuery内部,样式读写并不是简单映射至元素的style对象。当调用$.css()或.css()方法时,若所操作的属性名在jQuery.cssHooks中注册了钩子,框架便会把取值与赋值过程委托给钩子中的get与set函数。这种机制让开发者能够拦截原生CSS属性不易处理的场景,例如带厂商前缀的transform、需要单位换算的opacity,以及完全自定义的抽象属性。理解cssHooks的运作方式,是编写可维护动画与样式逻辑的基础。

cssHooks的基本结构与注册方式
jQuery把所有的钩子集中存放在jQuery.cssHooks这个普通对象中。每一个键对应一个CSS属性名(或自定义属性名),值则是一个包含get和set方法的对象。当执行$('#box').css('rotate', 45)时,jQuery先检查cssHooks['rotate']是否存在,若存在则调用其set方法,并把元素、值、以及内部缓存作为参数传入,从而完全接管写入行为。
这种设计的优势在于调用方无需关心底层兼容。比如旧版WebKit需要-webkit-transform,而标准写法为transform。如果没有钩子,业务代码里要写一堆特性判断;有了钩子,判断逻辑只写一次。下面的代码演示了如何为空属性占位,实际项目里通常会结合支持检测来动态选择前缀。
// 注册一个名为 rotate 的自定义CSS钩子
jQuery.cssHooks['rotate'] = {
get: function(elem, computed, extra) {
// 从元素的transform中解析出角度,这里简化为返回0
return 0;
},
set: function(elem, value) {
// value可能是数字,也可能是带单位的字符串
var angle = typeof value === 'number' ? value + 'deg' : value;
elem.style.transform = 'rotate(' + angle + ')';
elem.style.webkitTransform = 'rotate(' + angle + ')';
}
};
要注意,get方法接收的第二个参数computed表示是否获取计算样式,第三方插件常利用它来决定返回字符串还是数值。而set中直接操作elem.style是最直接的方式,但在复杂动画里,我们往往还要和jQuery.fx步进器配合,这就要用到钩子里的adjust或cssProps映射。
以rotate为例实现跨浏览器旋转控制
现代浏览器基本支持无前缀的transform,但在一些老旧环境中仍需前缀。我们希望让$('.icon').css('rotate', 90)这样的语句在所有环境生效,并且在使用.animate({rotate: 180})时也能平滑过渡。这就需要钩子不仅写style,还要告诉jQuery如何把rotate映射到底层真实属性。
完整的rotate钩子通常配合jQuery.fx.step使用。因为animate默认只认数值型标准属性,对于自定义属性,它会按帧调用step函数,而step内部可再调用cssHooks的set。下面示例展示了如何把rotate接入动画系统,并自动补全前缀。
(function($) {
var prefixes = ['', '-webkit-', '-moz-', '-ms-', '-o-'];
$.cssHooks['rotate'] = {
get: function(elem) {
var val = $.css(elem, 'transform');
// 简单示例:无法解析时返回0
if (!val || val === 'none') return 0;
var m = val.match(/matrix(([^)]+))/);
if (m) {
var parts = m[1].split(',');
var a = parseFloat(parts[0]);
return Math.round(Math.atan2(parseFloat(parts[1]), a) * (180 / Math.PI));
}
return 0;
},
set: function(elem, value) {
var angle = typeof value === 'number' ? value + 'deg' : value;
prefixes.forEach(function(p) {
elem.style[p + 'transform'] = 'rotate(' + angle + ')';
});
}
};
// 让.animate支持rotate
$.fx.step['rotate'] = function(fx) {
$.cssHooks['rotate'].set(fx.elem, fx.now);
};
})(jQuery);
上述代码里,get通过解析matrix来反推角度,虽然不完美但足以应付多数场景;set遍历前缀数组批量赋值,保证视觉一致。注册$.fx.step['rotate']之后,$('#box').animate({rotate: 360}, 1000)就能按帧把fx.now传给钩子,实现连续旋转。
使用这种方式,业务层再也不用写if (navigator.userAgent...)之类的判断。所有复杂度收敛在钩子内部,且能被多个模块复用。若项目还用到scale或translate,只需以同样模式扩展cssHooks['scale']即可,结构高度一致。
cssHooks与常规css方法的边界及注意事项
虽然cssHooks很强,但并非所有属性都该写成钩子。标准且无需处理的属性,直接走原生.css()效率更高。钩子适合三类情况:一是需要前缀兼容的属性;二是取值需要计算的属性,如将rgb颜色转成hsla;三是抽象属性,如本文的rotate,它背后对应多个真实CSS属性。
另一个常见误区是认为钩子能自动响应样式表变更。实际上get在computed为true时读取的是计算样式,若外部样式表被动态修改,需手动触发重取。此外,在IE旧版本中,某些前缀属性通过currentStyle获取时格式不同,钩子内要做兼容分支,否则get会返回空。
// 错误示例:在set里遗漏单位导致旋转失效
$.cssHooks['rotate'] = {
set: function(elem, value) {
// value为数字时未加deg,浏览器忽略该规则
elem.style.transform = 'rotate(' + value + ')';
}
};
// 正确做法:统一补全单位
$.cssHooks['rotate'] = {
set: function(elem, value) {
var v = typeof value === 'number' ? value + 'deg' : value;
elem.style.transform = 'rotate(' + v + ')';
}
};
从架构角度看,cssHooks相当于在jQuery样式层加了一层适配。它与jQuery.cssProps(用于重命名属性,如将float映射为cssFloat)互补:cssProps只改名字,cssHooks改逻辑。团队在封装UI组件时,推荐把视觉相关的自定义属性全部以钩子形式沉淀到基础库,页面业务仅调用语义化属性名,这样既降低耦合,也方便后续统一升级前缀策略。
最后提醒,若使用模块化打包,需确保钩子注册代码在jQuery加载之后、业务代码之前执行。很多诡异的“动画突然不转”问题,根源就是注册时机晚于首次.animate调用,导致jQuery走了默认原生路径而忽略了自定义逻辑。