写在最前
本次分享一下在一次 jQuery 赋值样式失效的结果中来分析背后原因的过程在翻 jQuery 源码的过程中, 感觉真是还不能说自己只是会用 jQuery, 我好像连会用都达不到(逃
一个很简单的赋值问题
$('#' + id).CSS({"left": "200"})
我只是单纯的想控制一个 left 值, 大家都懂, 但是竟然失败了, 打印出的元素属性中可以看到 left 为 ""; 我其实一开始没想到可能是 jQuery 本身的原因导致的, 我先考虑的是我这个元素是不是当前要赋值的? js 的问题? 等等干想了半天, 认为可能还是本身的写法问题所以进行了如下实验:
$('#' + id).css({"left": 200})
看起来是字符串和数字的区别! omg, 从来没想过字符串和数字的效果竟然会不一致你以为事情已经结束了? no, 看下面这个:
$('#' + id).css({"width": "200"})
好的为什么, width 设定字符串就可以被添加 px 后缀, left 就不可以??
现在我们可以总结一下通过 jQuery.fn.css 方法来设定元素属性的时候会有一些不一致的情况, 以 width 和 left 为例子(因为属性很多, 不一致的情况很多, 了解原理即可):
left 通过 number 类型可以补全 px 完成样式设定, string 类型无法设定属性
width 均可以通过 number 或 string 类型完成设定属性
从而可以抛出由一开始的奇怪现象的底层问题: 为什么通过 jQuery.fn.css 方法设定样式时, string 类型的值在某些属性上无法生效?
从源码中找线索
jQuery 的源码相比 reactvue 相比应该是很直接的了, 就是一个 js(不过我仍然看不懂?
首先引入一个没有压缩过的 jQuery, 里面保留了所有的注释和代码结构, 很方便大家阅读
https://cdn.bootcss.com/jquery/3.3.1/jquery.js
先找到我们本次设定样式的方法 jQuery.fn.css:
- jQuery.fn.extend({
- css: function(name, value) {
- return access(this,
- function(elem, name, value) {
- var styles, len, map = {},
- i = 0;
- if (Array.isArray(name)) {
- styles = getStyles(elem);
- len = name.length;
- for (; i < len; i++) {
- map[name[i]] = jQuery.css(elem, name[i], false, styles);
- }
- return map;
- }
- return value !== undefined ? jQuery.style(elem, name, value) : jQuery.css(elem, name);
- },
- name, value, arguments.length > 1);
- }
- });
如何通过浏览器来调试源码呢?(因为直接看源码太繁琐了, 通过 debug 的形式可以看到每次的调用栈)我们可以通过 console.log 的形式, 在这段源码中将 console 写入, 之后在控制台中就可以看到对应源码的调用:
进入 jQuery.style 之后就会来到最终产生区别的地方:
- style: function( elem, name, value, extra ) {
- ...
- hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
- if ( value !== undefined ) {
- type = typeof value;
- if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) {
- value = adjustCSS( elem, name, ret );
- type = "number";
- }
- ...
- if ( type === "number" ) {
- value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "":"px" );
- }
- ...
- if ( !hooks || !( "set" in hooks ) ||( value = hooks.set( elem, value, extra ) ) !== undefined ) {
- // 此时的 value 到底是 200 还是 200px; 只有添加了后缀才能赋值成功
- if ( isCustomProp ) {
- style.setProperty( name, value );
- } else {
- style[ name ] = value;
- }
- }
- }
- ...
- },
源码中可以看到在传入的 value 中确实对 string 和 number 做了区分; 而不是我之前所认为的, string 应该和 number 差不多:)如果传入 number 类型, 便会为其添加 px 后缀; 但是这仍然没有解释为什么 left 和 width 均传入 string 而结果不同的问题重点在于这句话:
- hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
- ...
- if ( !hooks || !( "set" in hooks ) ||
- ( value = hooks.set( elem, value, extra ) ) !== undefined ) {
- ...
- }
在 value 是 string 类型, 到最终赋值之前, 还会经过
value = hooks.set(elem, value, extra)) !== undefined
的判断, 也就是说如果 hooks.set 方法存在, 我们还有一次通过这个方法来将 string 类型的 value 进行后缀补全的机会而这个 hooks 是由 jQuery.cssHooks 得到的, 那么 jQuery.cssHooks 是什么:
从源码中可以看出, cssHooks 中包含了属性的一些方法, 其中 left 只有 get;width 有 get 和 set 再结合上面的判断条件就可以推断出, 由于 width 存在了 set 方法, 在其方法中对 string 类型的 value 完成了后缀的补齐, 而 left 则不行从而形成了文中一开始的神奇现象
cssHooks
直接向 jQuery 中添加钩子, 用于覆盖设置或获取特定 CSS 属性时的方法, 目的是为了标准化 CSS 属性名或创建自定义属性
$.cssHooks 对象提供了一种通过定义函数来获取或设置特定 CSS 值的方法可以用它来创建新的 cssHooks 用于标准化 CSS3 功能, 例如, 盒子阴影 (box shadows) 及渐变(gradients)
例如, 某些基于 webkit 的浏览器会使用 -webkit-border-radius 来设置对象的 border-radius, 然而, 早先版本的 Firefox 则使用 -moz-border-radiuscssHook 就可以将这些不同的写法进行标准化, 从而让 .css() 可以使用统一的标准化属性名(border-radius 或对应的 DOM 属性写法 borderRadius)
该方法除了提供了对特定样式的处理可以采用更加细致的控制外,$.cssHooks 同时还扩展了 .animate() 方法上的属性集
简单来说, jQuery 给我们暴露了一个钩子, 我们可以自己定义方法比如 set, 来实现针对某个属性的特定行为所以出现 left 和 width 的问题就是有没有 set 这个钩子方法 so 我们还剩最后一个问题:
为什么 width 要对其设定钩子函数?
答案可以从其 set 方法来窥探一下:
- set: function( elem, value, extra ) {
- var matches,
- styles = getStyles( elem ),
- isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
- subtract = extra && boxModelAdjustment(
- elem,
- dimension,
- extra,
- isBorderBox,
- styles
- );
- // Account for unreliable border-box dimensions by comparing offset* to computed and
- // faking a content-box to get border and padding (gh-3699)
- if ( isBorderBox && support.scrollboxSize() === styles.position ) {
- subtract -= Math.ceil(
- elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
- parseFloat( styles[ dimension ] ) -
- boxModelAdjustment( elem, dimension, "border", false, styles ) -
- 0.5
- );
- }
- // Convert to pixels if value adjustment is needed
- if ( subtract && ( matches = rcssNum.exec( value ) ) &&
- ( matches[ 3 ] || "px" ) !== "px" ) {
- elem.style[ dimension ] = value;
- value = jQuery.css( elem, dimension );
- }
- return setPositiveNumber( elem, value, subtract );
- }
从这个钩子函数中我们可以看出, 要对 width 做特殊处理是因为 css 的盒模型有好几种, content-box|border-box|inherit 分别代表不包括 paddingbordermargin | 包含 border 和 padding | 继承; 故为了统一外界的调用, 隐藏这些背后的判断, 从而增加了这个 set 方法顺带着在其中把 px 补全了同时 left 这种没什么需要兼容的故没有设定 set 方法
小结
虽然 cssHooks 不常用(我反正从来没用过, 现在对于标准化格式有很多其他的方法来做, cssHooks 的钩子感觉还是有些复杂了), 但这次通过页面上一个很小的问题从而引发思考并且试图深挖一些的过程还是值得总结下来的虽然我们不是造轮子的人, 但理解别人的轮子也是比会用好一些的; 更何况看了 cssHooks 我感觉我都不会用 jQuery:)
来源: https://segmentfault.com/a/1190000013274020