jQuery 4.0 源码架构与工程实践-第 01 章 · 全景与工程结构:一个 20 岁库的自省
本章导览
任何一份源码阅读笔记,都应该从"这份文件是什么"开始。本章不写任何 API 用法,只做一件事:把 jquery-4.0.0.js 这份 9680 行的文件,从物理结构、构建来源、模块拓扑、版本演进、工程规范五个维度彻底剖开 ,让你在翻开第 4 章那个最难的 matcherFromTokens 之前,已经知道它在整个系统里处于什么位置、为什么必须是那个样子。
本章的具体产出是三张图和三张表:
- 源码地图表:行号区间 → 模块 → 职责,覆盖全部 9680 行,不留死角;
- 模块依赖拓扑图 :解释为什么
core必须在最前面、为什么选择器引擎必须早于遍历模块、为什么offset会出现在文件倒数第二段; - Breaking Changes 全景表:jQuery 4.0 删掉了什么、改了什么、替代方案是什么。
读完之后,你应该能回答这几个问题:
- 为什么
jQuery.fn.init.prototype = jQuery.fn这行代码是 jQuery 对象模型的"结绳",而没有它整个链式调用会崩塌? - 为什么源码里到处是
document$1、rquickExpr$1、documentElement$1这种带$1的变量名? - 为什么
jQuery( "<div>" )在 jQuery 4 里会走isObviousHtml的快路径,而 jQuery 3 不会? - 为什么 jQuery 4 删掉了
$.isArray、$.type、$.trim这些"人畜无害"的小函数? - 为什么
ready在 2026 年还需要存在?
1.1 为什么在 2026 年还要精读 jQuery 4.0
先把话说清楚:本系列不推销 jQuery。如果你的项目是全新的、跑在现代浏览器上、团队熟悉组件化开发,那么你不应该用 jQuery 启动一个新项目。这个结论没有争议。
但是,"不用 jQuery 写新项目"和"不需要理解 jQuery"是两件完全不同的事。以下五个理由,每一个都足够支撑一次系统性的源码阅读。
1.1.1 存量代码的规模决定了它不会消失
我们不需要精确的统计数据也能确认这一点:jQuery 曾长期占据全球网站 70% 以上的使用率。即便在 2026 年,大量仍在产生商业价值的系统------银行核心柜面、政务申报平台、医院信息系统、制造业 MES、航空订座前端、保险理赔后台------其核心交互层仍然构建在 jQuery 之上。
这些系统有三个共同特征:
- 重写成本极高:一个 200 万行的 jQuery 后台,重写预算可能高达数千万元,而它的业务价值并不因为重写而提升;
- 正确性要求极高:它们往往是 To B 或 To G 系统,一次 DOM 操作错误可能意味着一笔错误的账务;
- 维护周期极长:很多系统的设计寿命是 10-15 年,远超任何前端框架的流行周期。
这意味着:未来十年内,仍然会有大量工程师需要维护、修复、扩展 jQuery 代码。而"会用 API"和"能定位问题"之间的差距,恰好就是源码阅读的价值。
举个真实的场景:一个后台页面在批量插入 5000 行表格时卡死 8 秒。只会用 API 的工程师会尝试"分批插入";读过 domManip 和 buildFragment 的工程师会立刻意识到------domManip 对每个目标元素都会 jQuery.clone( node, true, true )(行 4531),也就是说 $("tbody").append(html) 在 tbody 匹配了 1 个元素时是 1 次克隆,但 $("tr").after(html) 在 tr 匹配 5000 个元素时是 4999 次深克隆 ,其中还包括对每个克隆体再次执行 getAll( node, "script" ) 扫描。这类问题的根因,只有读源码才能一眼看穿。
1.1.2 jQuery 4.0 是一次面向现代规范的实质性重构
常见的误解是:jQuery 4.0 只是"删了一些废弃 API"。事实上,从源码看,jQuery 4 在主线做了几件相当硬核的事:
(1)选择器引擎彻底并入主线。 在 jQuery 3.x 里,Sizzle 虽然被合并进主仓库,但仍保持相对独立的命名空间和一套"对外 API"(Sizzle.compile、Sizzle.select、Sizzle.tokenize)。在 4.0 的这份构建产物里,我们看到的是:
js
// 行 2459-2466
jQuery.find = find;
// These have always been private, but they used to be documented as part of
// Sizzle so let's maintain them for now for backwards compatibility purposes.
find.compile = compile;
find.select = select;
find.setDocument = setDocument;
find.tokenize = tokenize;
注释说得很直白:"这些一直是私有的,只是过去作为 Sizzle 的一部分被文档化了,为了向后兼容先保留着。"------这就是一次降级声明 :Sizzle 作为一个可独立引用的库,在 jQuery 4 里正式退场,只剩下 jQuery.find 这个入口。
(2)安全模型升级到 Trusted Types。 jQuery 4 引入了 isObviousHtml(行 2501):
js
function isObviousHtml( input ) {
return input[ 0 ] === "<" &&
input[ input.length - 1 ] === ">" &&
input.length >= 3;
}
这个看似平凡的函数,是 jQuery 支持 TrustedHTML 包装类型的关键。因为在启用了 require-trusted-types-for 'script' 的 CSP 环境下,innerHTML = userString 会直接抛异常,只有传入 TrustedHTML 对象才被允许。isObviousHtml 的存在,让 $( trustedHTMLWrapper ) 这类调用可以绕过正则检测(正则无法作用于包装对象),直接进入 HTML 解析路径。行 2629-2636 的注释把这件事说得很清楚:
js
// Handle obvious HTML strings
match = selector + "";
if ( isObviousHtml( match ) ) {
// Assume that strings that start and end with <> are HTML and skip
// the regex check. This also handles browser-supported HTML wrappers
// like TrustedHTML.
match = [ null, selector, null ];
注意"skip the regex check "------跳过的正是那个曾经引发 XSS 争议的 rquickExpr 正则(行 2602)。这是 jQuery 在"严格 HTML 识别"与"可信类型"之间做的权衡:既然内容已经被 Trusted Types 策略背书,就没必要再用正则猜一次。
(3)Deferred 对齐 Promises/A+。 行 3211 的 then 实现里,密密麻麻地引用了 Promises/A+ 规范的条款号:
js
// Support: Promises/A+ section 2.3.3.3.3
// https://promisesaplus.com/#point-59
// Ignore double-resolution attempts
if ( depth < maxDepth ) {
return;
}
returned = handler.apply( that, args );
// Support: Promises/A+ section 2.3.1
// https://promisesaplus.com/#point-48
if ( returned === deferred.promise() ) {
throw new TypeError( "Thenable self-resolution" );
}
从"只支持自己的 Deferred"到"明确对齐规范条款、处理自解析、处理 thenable 只取一次 then、处理异常转为 rejection",这是一次规范层面的现代化。第 12 章会用整章篇幅拆这个 then。
(4)attr 的 false 语义变更。 行 938-949:
js
if ( value !== undefined ) {
if ( value === null ||
// For compat with previous handling of boolean attributes,
// remove when `false` passed. For ARIA attributes -
// many of which recognize a `"false"` value - continue to
// set the `"false"` value as jQuery <4 did.
( value === false && name.toLowerCase().indexOf( "aria-" ) !== 0 ) ) {
jQuery.removeAttr( elem, name );
return;
}
这是 jQuery 4 里最容易踩坑的行为变更 :$(el).attr("checked", false) 在 jQuery 3 会把属性设成字符串 "false"(而 "false" 作为布尔属性的存在性判断是 true,所以实际上是"选中"),在 jQuery 4 会移除属性 。同时为了照顾 ARIA 生态(aria-expanded="false" 是有意义的字符串值),特意为 aria-* 前缀保留了旧行为。
一个 attr 调用里同时体现了"修正历史 bug"和"照顾无障碍生态"两种考量------这就是读源码的乐趣所在。
1.1.3 源码里沉淀的是"浏览器行为知识",不是"jQuery 知识"
这是最重要的一条理由。
jQuery 之所以"大",绝大部分体积不是花在 API 糖上,而是花在抹平浏览器差异上。而这些差异,在 2026 年并没有完全消失。举几个从这份源码里随手拈来的例子:
| 源码位置 | 处理的浏览器问题 | 该知识在今天是否仍有用 |
|---|---|---|
行 4095 isHiddenWithinTree |
display:none 的祖先不应强制子元素被判定为 hidden(gh-2404、gh-2863) |
有用。任何需要判断元素可见性的代码都要面对"级联 vs 内联"的区分 |
行 5767 getStyles 中 elem.ownerDocument.defaultView |
IE 弹窗里 window.getComputedStyle 会取到 opener window |
有用。跨 document(iframe、popup、Web Worker DOM 模拟)取样式的正确姿势 |
行 5949 reliableTrDimensions |
Firefox 70+ 在表格行的 computed height 里包含了 border 宽度(gh-4529) | 有用。测量表格行高的唯一可靠方式是 offsetHeight |
行 5949 reliableColDimensions |
Safari 18.3+ 对 <col> 的 computed width 恒为 0 |
有用。列宽测量必须走 offsetWidth |
行 4161 adjustCSS 的 maxIterations = 20 |
Firefox 54-66 在 CSS 属性存在上界时,单位换算会被截断(gh-2144) | 有用。迭代逼近法本身是一个通用的"未知单位换算"解法 |
行 5580 cleanData 里"赋 undefined 而非 delete" |
Chrome 35-45 在从 DOM 节点 delete 属性时性能暴跌(chromium bug 378607) | 有用。DOM 上的属性删除仍是隐藏类失效的诱因 |
行 4922 !( event.type === "click" && event.button >= 1 ) |
Firefox 42-66 会让非主键点击冒泡 click(trac-3861) | 有用。委托 click 时必须考虑鼠标键位 |
行 1831 elem.parentNode.selectedIndex 强制求值 |
访问 selectedIndex 会迫使浏览器把 optgroup 里的默认 option 视为 selected | 有用。<select> 的 selected 状态存在惰性求值语义 |
这张表可以再列三十行。这些知识的载体是 jQuery 源码,但它们的价值不依附于 jQuery 。当你下次需要判断"一个元素是否真的可见"、"如何安全地把 em 换算成 px"、"如何避免删除 DOM 属性引发性能塌方"时,你会庆幸自己读过这些代码。
1.1.4 jQuery 的架构手法是可迁移的工程范式
jQuery 在架构上最值得学习的一点,是它系统性地把"差异"收敛到表里。全书中我们会反复遇到这个模式,它至少出现了七次:
| 抽象层 | 收敛的差异 | 源码位置 |
|---|---|---|
jQuery.attrHooks |
各属性 get/set 的特殊语义 | 行 970 |
jQuery.propHooks / propFix |
DOM property 名与用户名的映射(for→htmlFor、class→className) | 行 7188 / 7217 |
jQuery.valHooks |
表单控件 value 的多态(select 返回数组、checkbox 接受数组) | 行 7494 |
jQuery.cssHooks |
样式属性的取值/设值/展开(width/height、margin/padding/border 展开) | 行 6138 |
jQuery.event.special |
事件类型差异(click 的原生接管、focus/blur 的模拟、mouseenter 的边界判定) | 行 4996 |
prefilters / transports |
AJAX 的 dataType 与传输方式(xhr / script tag / jsonp) | 行 7948 / 7955 |
jQuery.expr.filter / pseudos |
选择器中各类 token 与伪类的匹配算法 | 行 1506 / 1716 |
每当主干逻辑遇到"这里可能不一样",jQuery 的做法不是写 if (type === "x"),而是查一张表,表里有就用表里的,没有就走默认。这让主干代码保持线性可读,同时赋予极强的可扩展性(插件可以往这些表里塞东西)。
这套手法在任何大型系统里都适用:支付系统用它抽象渠道差异、表单引擎用它抽象控件差异、CI 系统用它抽象执行器差异。学会识别并复制这种"表驱动差异收敛",比记住 jQuery 的任何一个 API 都有价值。
1.1.5 最后一条:它是少数"完整可读"的大型前端库
React 的 Fiber 架构、Vue 的响应式系统,都值得读,但它们的代码量是 jQuery 的十倍以上,且大量代码在处理调度、并发、编译------读通它们需要数月。
jQuery 4.0 的这份构建产物是 9680 行 。一个熟练的前端工程师,用两到三周的完整业余时间可以把它从头到尾读一遍,并且真的读懂每一行 。这是一次难得的"完整征服一个大型库"的体验------读完之后,你会建立起一种稀缺的自信:再复杂的前端代码,也只是更多的行数和更多的分支,没有魔法。
1.2 文件的物理结构:9680 行的源码地图
在深入任何一行代码之前,我们先拿到全局视角。这一节的产出是那张"源码地图表"。
1.2.1 文件头:元信息
js
// 行 1-10
/*!
* jQuery JavaScript Library v4.0.0
* https://jquery.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.com/license/
*
* Date: 2026-01-18T00:20Z
*/
几个值得注意的信息:
/*!而不是/*:这是给压缩器(UglifyJS / Terser)的信号------"这个注释块必须保留"。所有许可证头都遵循这个约定。如果你在自己的库里写了/* Copyright ... */,压缩后许可证信息会丢失,这在 MIT/BSD 合规上是风险。- 版权主体是 "OpenJS Foundation and other contributors":jQuery 于 2019 年将知识产权归属转移到 OpenJS Foundation(与 Node.js 同属)。这不是形式主义------它意味着 jQuery 的商标与代码由中立基金会持有,避免了单一公司控制的风险。
- MIT 许可:最宽松的开源许可之一。意味着你可以自由使用、修改、闭源分发,只需保留版权声明。
- 构建日期
2026-01-18T00:20Z:这是一个精确到分钟的时间戳,格式遵循 ISO 8601 并使用 Zulu(UTC)时区。它是构建脚本自动注入的。这个日期对我们有一个实际用途:它标示了这份文件的"代码年龄",当你发现某个浏览器 bug 的修复"应该在里面"时,可以据此判断。
1.2.2 全文件源码地图
下面这张表是对全部 9680 行的完整划分。行号区间依据实际源码中的模块边界(主要通过 jQuery.extend / jQuery.fn.extend 的调用块、以及 rollup 生成的模块拼接痕迹确定)。
| 行号区间 | 行数 | 模块 | 核心内容 | 对应章节 |
|---|---|---|---|---|
| 11-31 | 21 | UMD 包装 | CommonJS / 全局导出、factory 模式、window 前置校验 | 01 |
| 33-130 | 98 | 核心工具层(core) | arr / getProto / slice / flat / push / indexOf / class2type / toType / isWindow / isArrayLike / DOMEval / jQuery 工厂函数 |
02 |
| 131-216 | 86 | jQuery.prototype 骨架 | jquery 版本号、length、toArray / get / pushStack / each / map / slice / first / last / even / odd / eq / end |
03 |
| 218-288 | 71 | jQuery.extend | 深浅拷贝、原型污染防护、__proto__ 拦截、循环引用防护 |
02 |
| 290-520 | 231 | jQuery 静态工具 | expando / isReady / error / noop / isPlainObject / isEmptyObject / globalEval / each / text / makeArray / inArray / isXMLDoc / contains / merge / grep / map / guid / support;末尾挂载 Symbol.iterator 与 class2type 填充 |
02 |
| 522-902 | 381 | 选择器·支撑层 | nodeName / whitespace / isIE / rbuggyQSA / rtrimCSS / identifier / createCache / testContext / attributes / pseudos / filterMatchExpr / escapeSelector / tokenize / preFilter / toSelector / access |
04 |
| 903-1027 | 125 | 属性模块(attributes) | rnothtmlwhite、jQuery.fn.attr/removeAttr、jQuery.attr/attrHooks/removeAttr、IE 的 type hook、jQuery.escapeSelector |
08 |
| 1029-1172 | 144 | 选择器·文档序 | sortOrder、jQuery.uniqueSort、jQuery.fn.uniqueSort、选择器模块的全部实例变量(dirruns / classCache / compilerCache / nonnativeSelectorCache / matchExpr / rquickExpr$1 / inDisabledFieldset) |
05 / 06 |
| 1174-1459 | 286 | 选择器·find | find 主函数(快路径 getElementById / gEBTN / gEBCN、qSA 借用、:scope 与 ID 前缀技巧)、markFunction / createInputPseudo / createButtonPseudo / createDisabledPseudo / createPositionalPseudo / setDocument / find.matches / find.matchesSelector |
05 |
| 1463-1937 | 475 | jQuery.expr 定义 | cacheLength / createPseudo / match / find(ID/TAG/CLASS)/ relative(4 种组合子)/ preFilter / filter(ID/TAG/CLASS/ATTR/CHILD/PSEUDO)/ pseudos(not/has/contains/lang/target/root/focus/enabled/disabled/checked/selected/empty/parent/header/input/button/text/first/last/eq/even/odd/lt/gt);末尾批量注册 input/button 类型伪类与 setFilters |
05 |
| 1939-2147 | 209 | 选择器·组合子与 setMatcher | addCombinator(含 dirruns 缓存)/ elementMatcher / multipleContexts / condense / setMatcher |
05 |
| 2149-2370 | 222 | 选择器·编译器 | matcherFromTokens / matcherFromGroupMatchers(superMatcher)/ compile |
05 |
| 2371-2454 | 84 | select 主入口 | select 的 ID 降维、右到左种子获取、needsContext 短路 |
05 |
| 2456-2466 | 11 | 选择器导出 | setDocument() 初始化、jQuery.find = find、find.compile/select/setDocument/tokenize 的兼容挂载 |
05 |
| 2468-2591 | 124 | 遍历模块(traversing) | dir / siblings / rneedsContext / rsingleTag / isObviousHtml / winnow / jQuery.filter / jQuery.fn.find/filter/not/is |
06 |
| 2593-2759 | 167 | jQuery.fn.init | rquickExpr、init 的七条分支、init.prototype = jQuery.fn、rootjQuery 初始化 |
03 |
| 2710-2799 | 90 | 遍历·补充 | has / closest / index / add / addBack |
06 |
| 2801-2888 | 88 | 遍历·族方法生成 | sibling helper + jQuery.each 批量生成 parent/parents/parentsUntil/next/prev/nextAll/prevAll/nextUntil/prevUntil/siblings/children/contents |
06 |
| 2899-3113 | 215 | Callbacks | createOptions、jQuery.Callbacks 完整实现(add/remove/has/empty/disable/disabled/lock/locked/fireWith/fire/fired) |
12 |
| 3115-3150 | 36 | adoptValue | thenable / promise / 普通值的统一收养 | 12 |
| 3152-3499 | 348 | Deferred / when | 三元组表、promise 对象(state/always/catch/pipe/then/promise)、deferred 方法生成、jQuery.when |
12 |
| 3501-3596 | 96 | ready 机制 | rerrorNames、exceptionHook、readyException、readyList、jQuery.fn.ready、jQuery.ready、completed 事件绑定 |
12 |
| 3598-3623 | 26 | camelCase | rdashAlpha / fcamelCase / camelCase |
08 / 09 |
| 3614-3774 | 161 | Data 类 | acceptData、Data 构造与 uid、Data.prototype(cache/set/get/access/remove/hasData) |
15 |
| 3776-3946 | 171 | data / removeData | dataPriv / dataUser 双实例、rbrace / rmultiDash、getData / dataAttr、jQuery.data/removeData/_data/_removeData、jQuery.fn.data/removeData |
15 |
| 3948-4080 | 133 | queue / dequeue | jQuery.queue/dequeue/_queueHooks、jQuery.fn.queue/dequeue/clearQueue/promise |
13 |
| 4082-4204 | 123 | CSS 支撑 | pnum / rcssNum / cssExpand / isHiddenWithinTree / rautoPx / isAutoPx / adjustCSS |
09 |
| 4206-4310 | 105 | display 处理 | rmsPrefix / cssCamelCase / defaultDisplayMap / getDefaultDisplay / showHide、jQuery.fn.show/hide/toggle |
09 |
| 4312-4474 | 163 | buildFragment | isAttached / composed、rtagName、wrapMap、getAll、rscriptType、setGlobalEval、rhtml、buildFragment |
07 |
| 4476-4574 | 99 | domManip | disableScript / restoreScript、domManip 主函数 |
07 |
| 4576-4686 | 111 | 事件·on | rcheckableType、rtypenamespace、returnTrue / returnFalse、on 参数洗牌与 one 包装 |
10 |
| 4649-5067 | 419 | jQuery.event | add / remove / dispatch / handlers / addProp / fix / special(load / click / beforeunload) |
10 / 11 |
| 5069-5179 | 111 | leverageNative / removeEvent | 原生事件借力机制、jQuery.removeEvent |
11 |
| 5181-5365 | 185 | jQuery.Event | 构造函数、原型(preventDefault/stopPropagation/stopImmediatePropagation)、addProp 批量注册 30+ 事件属性、focus/blur 的 special 定义 |
11 |
| 5367-5442 | 76 | mouseenter 族 / on/off 导出 | mouseenter/mouseleave/pointerenter/pointerleave 的 special、jQuery.fn.on/one/off |
10 / 11 |
| 5444-5757 | 314 | DOM 操作 | rnoInnerhtml、manipulationTarget、cloneCopyEvent、remove、jQuery.htmlPrefilter/clone/cleanData、jQuery.fn.detach/remove/text/append/prepend/before/after/empty/clone/html/replaceWith、appendTo 等 5 个反向方法生成 |
07 |
| 5759-6132 | 374 | 样式计算 | rnumnonpx / rcustomProp / getStyles / swap / curCSS / vendorPropName / finalPropName / 表格样式探测(computeTableStyleTests)/ setPositiveNumber / boxModelAdjustment / getWidthOrHeight |
09 |
| 6134-6353 | 220 | jQuery.style / css / cssHooks | style / css 双通道、width/height hook、margin/padding/border 展开 hook、jQuery.fn.css |
09 |
| 6355-6458 | 104 | Tween | Tween 构造与原型(init/cur/run)、Tween.propHooks._default、jQuery.easing |
13 |
| 6460-6686 | 227 | 动画前置处理 | schedule / createFxNow / genFx / createTween / defaultPrefilter |
13 |
| 6688-6885 | 198 | Animation | propFilter、Animation 主函数、jQuery.Animation(tweeners/tweener/prefilters/prefilter) |
13 |
| 6887-7138 | 252 | 动画 API | jQuery.speed、jQuery.fn.fadeTo/animate/stop/finish、toggle/show/hide 的动画重载、slideDown/slideUp/slideToggle/fadeIn/fadeOut/fadeToggle 生成、jQuery.timers / fx.tick / fx.timer / fx.start / fx.stop / fx.speeds、delay |
13 |
| 7140-7270 | 131 | prop 模块 | jQuery.fn.prop/removeProp、jQuery.prop、propHooks.tabIndex、propFix 映射表与批量注册 |
08 |
| 7271-7427 | 157 | class 模块 | stripAndCollapse / getClass / classesToArray、addClass / removeClass / toggleClass / hasClass |
08 |
| 7429-7590 | 162 | val 模块 | jQuery.fn.val、valHooks.select(get/set)、IE 的 option hook、radio/checkbox hook |
08 |
| 7592-7774 | 183 | trigger | rfocusMorph、jQuery.event.trigger / simulate、jQuery.fn.trigger/triggerHandler |
11 |
| 7776-7926 | 151 | 序列化 | location / nonce / rquery、jQuery.parseXML、buildParams / jQuery.param、serialize / serializeArray |
14 |
| 7928-8201 | 274 | AJAX 基础设施 | r20 / rhash / rantiCache / rheaders / rlocalProtocol / rnoContent / rprotocol、prefilters / transports、originAnchor、addToPrefiltersOrTransports / inspectPrefiltersOrTransports / ajaxExtend / ajaxHandleResponses / ajaxConvert |
14 |
| 8203-8780 | 578 | jQuery.ajax | ajaxSettings 全量配置、ajaxSetup、ajaxPrefilter/ajaxTransport、jQuery.ajax 主函数(含 jqXHR 伪造对象、done 回调)、getJSON / getScript、get/post 生成 |
14 |
| 8782-8813 | 32 | _evalUrl | content-type prefilter、jQuery._evalUrl |
14 |
| 8815-8879 | 65 | wrap 族 | wrapAll / wrapInner / wrap / unwrap |
07 |
| 8881-8890 | 10 | :hidden / :visible | jQuery.expr.pseudos.hidden/visible、xhr 工厂 |
06 / 14 |
| 8892-8997 | 106 | xhr transport | xhrSuccessStatus、jQuery.ajaxTransport 的 xhr 实现 |
14 |
| 8999-9184 | 186 | script / jsonp | canUseScriptTag、script dataType 注册、script prefilter、script transport、rjsonp / oldCallbacks、jsonp prefilter、通用 prefilter(FormData / 二进制) |
14 |
| 9186-9224 | 39 | parseHTML | jQuery.parseHTML |
07 |
| 9226-9287 | 62 | load | jQuery.fn.load |
14 |
| 9289-9293 | 5 | :animated | jQuery.expr.pseudos.animated |
13 |
| 9295-9534 | 240 | offset | jQuery.offset.setOffset、offset / position / offsetParent、scrollLeft/scrollTop 生成、innerHeight/innerWidth/height/width/outerHeight/outerWidth 生成 |
09 |
| 9536-9589 | 54 | 事件快捷方法 | ajax 全局事件方法、bind/unbind/delegate/undelegate/hover、25 个事件快捷方法生成 | 10 |
| 9591-9620 | 30 | proxy | jQuery.proxy |
10 |
| 9622-9630 | 9 | holdReady / expr 别名 | jQuery.holdReady、jQuery.expr[ ":" ] = jQuery.expr.filters = jQuery.expr.pseudos |
12 / 04 |
| 9632-9649 | 18 | AMD 注册 | define( "jquery", ... ) |
01 |
| 9651-9681 | 31 | noConflict / 导出 | _jQuery / _$ 备份、jQuery.noConflict、全局挂载、return jQuery |
01 |
合计:9680 行,无一遗漏。
这张表建议收藏。后续每一章开头都会引用它,告诉你"我们现在在地图的哪个格子"。
1.2.3 rollup 打包痕迹:$1 后缀从哪来
第一次读这份文件的人,最困惑的通常是这几个变量:
js
var document$1 = window.document; // 行 92
var documentElement$1 = document$1.documentElement; // 行 564
var rquickExpr$1 = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/; // 行 1157
而在文件后面(行 1126)又出现了:
js
var i,
outermostContext,
// Local document vars
document, // ← 没有 $1!
documentElement,
documentIsHTML,
以及行 2602:
js
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, // ← 没有 $1
为什么有两个 document?两个 rquickExpr?
原因是 rollup 的作用域重命名(de-conflict)。
jQuery 的 src/ 目录是分模块的,大致结构(依据 jQuery 官方仓库)是:
csharp
src/
├── core.js
├── core/
│ ├── DOMEval.js
│ ├── access.js
│ ├── camelCase.js
│ ├── init.js
│ ├── isAttached.js
│ ├── nodeName.js
│ ├── parseHTML.js
│ ├── ready-no-deferred.js
│ ├── stripAndCollapse.js
│ ├── support.js
│ └── var/
│ ├── ObjectFunctionString.js
│ ├── arr.js
│ ├── class2type.js
│ ├── document.js
│ ├── documentElement.js
│ ├── flat.js
│ ├── fnToString.js
│ ├── getProto.js
│ ├── hasOwn.js
│ ├── indexOf.js
│ ├── isIE.js
│ ├── pop.js
│ ├── push.js
│ ├── rcheckableType.js
│ ├── rnothtmlwhite.js
│ ├── rsingleTag.js
│ ├── slice.js
│ ├── toString.js
│ └── whitespace.js
├── selector.js
├── selector-native.js
├── traversing.js
├── manipulation.js
├── attributes.js
├── event.js
├── css.js
├── effects.js
├── ajax.js
├── deferred.js
├── callbacks.js
├── data.js
├── queue.js
├── offset.js
├── dimensions.js
├── serialize.js
├── wrap.js
├── deprecated.js
├── exports/
│ ├── amd.js
│ └── global.js
└── .eslintrc.json
多个模块里都有顶层变量叫 document(src/var/document.js 里是 window.document,src/selector.js 里是"当前 setDocument 选中的 document")、都有 rquickExpr(src/selector.js 里是"简单选择器"正则,src/core/init.js 里是"HTML 或 #id"正则)。
当 rollup 把它们拼进同一个 IIFE 作用域时,两个同名变量会发生冲突 。rollup 的重命名策略是:保留其中一个原名(通常是先出现的),给另一个加 $1;如果还冲突,加 $2、$3。
所以:
document$1(行 92)=src/var/document.js的核心模块常量,恒等于当前 window 的 document,不随选择器上下文变化;document(行 1126)=src/selector.js的模块变量,由setDocument()动态改写,表示当前正在查询的文档(可能是 iframe 的文档)。
这个区分非常重要,读源码时不能混淆:
js
// 行 1412-1428
function setDocument( node ) {
var subWindow,
doc = node ? node.ownerDocument || node : document$1;
// Return early if doc is invalid or already selected
if ( doc == document || doc.nodeType !== 9 ) {
return;
}
// Update global variables
document = doc; // ← 改写模块级 document
documentElement = document.documentElement;
documentIsHTML = !jQuery.isXMLDoc( document );
...
看:默认取 document$1(宿主文档),但查询 iframe 里的元素时会改成 iframe 的文档。而 document$1 永远指向宿主文档,用于 DOMEval 的默认 doc(行 102)、emptyStyle 的创建(行 5858)、parseHTML 的默认上下文(行 2657)等"必须与宿主文档绑定"的场景。
记忆口诀 :$1 结尾的 = "编译期确定的宿主常量";不带 $1 的(在 selector 模块内)= "运行时可切换的当前文档"。
同类现象还有:
| 变量 | 位置 | 含义 |
|---|---|---|
document$1 |
92 | 宿主 document,永不变 |
document |
1126 | selector 当前 document,由 setDocument 改写 |
documentElement$1 |
564 | 宿主的 documentElement,用于 matches 取值、isAttached 探测 |
rquickExpr$1 |
1157 | selector 的"简单选择器"正则:#id / tag / .class |
rquickExpr |
2602 | init 的"HTML 或 #id"正则:\s*(<[\w\W]+>)[^>]* 或 #([\w-]+) |
nodeName(函数) |
522 | 比较元素 nodeName(大小写不敏感) |
nodeName(局部变量) |
4222 | getDefaultDisplay 里的局部变量,遮蔽了同名函数! |
最后一条特别阴险:getDefaultDisplay(行 4219)里 var ... nodeName = elem.nodeName; 会遮蔽 外层的 nodeName 函数。这是 rollup 打包后局部变量与模块级函数同名造成的。它不会引发 bug(因为函数体内不再需要调用同名函数),但阅读时容易被误导。
1.2.4 一个有趣的细节:为什么 var i 出现在行 1122
js
// 行 1122
var i,
outermostContext,
...
i 这个循环变量被提升到了 selector 模块所有实例变量的最前面。它和行 1932 的 for ( i in { radio: true, ... } ) 是同一个变量------也就是说,selector 模块在初始化阶段复用了这个 i 来批量注册伪类。这种"一个 i 贯穿整个模块生命周期"的写法,是为了压缩率:var 声明越少、i/j/l 这类单字母变量复用越多,minify 后的体积越小(虽然 gzip 后差异不大,但 jQuery 团队历史上的确以此为优化目标之一)。
1.3 UMD 包装头:21 行里的五种环境兼容
js
// 行 11-31
( function( global, factory ) {
"use strict";
if ( typeof module === "object" && typeof module.exports === "object" ) {
// For CommonJS and CommonJS-like environments where a proper `window`
// is present, execute the factory and get jQuery.
module.exports = factory( global, true );
} else {
factory( global );
}
// Pass this if window is not defined yet
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
"use strict";
if ( !window.document ) {
throw new Error( "jQuery requires a window with a document" );
}
这是经典的 UMD(Universal Module Definition) 模式。我们逐段拆解。
1.3.1 第一层:环境探测与 factory 调用
js
( function( global, factory ) { ... } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { ... } )
这个 IIFE 接收两个参数:
-
global:typeof window !== "undefined" ? window : this- 在浏览器里:
window - 在 Node.js / Web Worker / 严格模式的裸环境里:
this(Node 的 CommonJS 模块顶层this等于module.exports,Web Worker 里this等于self) - 用
typeof window !== "undefined"而不是window !== undefined,是为了避免未声明变量的 ReferenceError 。这是一个必须养成的习惯:判断"某个全局是否存在"时,永远用typeof x !== "undefined"。
- 在浏览器里:
-
factory:真正的 jQuery 实现(行 25 到行 9680 的整个函数),接收( window, noGlobal ),返回 jQuery 对象。
1.3.2 第二层:CommonJS 分支
js
if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = factory( global, true );
} else {
factory( global );
}
判断条件为什么是 typeof module === "object" && typeof module.exports === "object" 两个都要?
- 只判断
module会误判某些 AMD 环境或打包工具的 shim; - 只判断
module.exports会在module未定义时抛错(必须先检查module); - 用
typeof而非真值判断,是为了避免某些环境下module被定义为null。
关键点是 factory( global, true ) 的第二个参数 true ------它对应 factory 签名里的 noGlobal。当 noGlobal 为真值时,文件末尾的这段不会执行:
js
// 行 9674-9676
if ( typeof noGlobal === "undefined" ) {
window.jQuery = window.$ = jQuery;
}
也就是说:在 CommonJS 环境下,jQuery 不会污染全局。这是一个重要的行为差异:
js
// Node.js 或 webpack 环境
const jQuery = require( "jquery" );
// window.jQuery 未定义,window.$ 未定义
// jQuery 只在模块作用域内可用
而在浏览器 <script> 直接引入时,factory( global ) 只传了一个参数,noGlobal 是 undefined,于是 window.jQuery = window.$ = jQuery 执行,全局被污染(这正是 jQuery 的经典行为)。
1.3.3 第三层:factory 内部的前置校验
js
if ( !window.document ) {
throw new Error( "jQuery requires a window with a document" );
}
这是一个快速失败(fail fast) 设计。如果在没有 document 的环境里(比如某些 Node.js 场景,或者被误用在 Web Worker 里),jQuery 会在最开头抛出一个清晰的错误,而不是在后面的几百行里抛出各种莫名其妙的 undefined is not a function。
这个设计值得借鉴:库的第一行就应该验证运行环境,并给出人类可读的错误信息。
1.3.4 AMD 的"迟到"注册
值得注意的是,AMD 注册不在这个 UMD 头里,而是在文件末尾:
js
// 行 9632-9649
// Register as a named AMD module, since jQuery can be concatenated with other
// files that may use define, but not via a proper concatenation script that
// understands anonymous AMD modules. A named AMD is safest and most robust
// way to register. ...
if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
} );
}
为什么要"迟到"?注释解释了:jQuery 可能被和其他文件简单拼接(concat),而如果其他文件里有匿名 define 调用,AMD 加载器会无法识别模块。使用具名 define( "jquery", ... ) 是最稳妥的。而放在末尾的原因,注释里也写了:
"Do this after creating the global so that if an AMD module wants to call noConflict to hide this version of jQuery, it will work."
即:先挂载全局,再注册 AMD,这样 AMD 模块内部如果调用 noConflict() 隐藏全局 jQuery,仍然能拿到实例。
关于 define.amd 的检查:AMD 规范要求加载器给 define 函数挂一个 amd 属性(通常是 {})。只检查 typeof define === "function" 会误伤某些自定义了 define 函数的代码。这是 AMD 社区的一个历史约定。
1.3.5 五种环境的完整兼容矩阵
| 环境 | global 的值 |
CommonJS 分支 | noGlobal |
全局挂载 | AMD |
|---|---|---|---|---|---|
浏览器 <script> |
window |
否 | undefined |
是(window.jQuery / window.$) |
若有 define.amd 则注册 |
| CommonJS(Node + jsdom) | window(若 jsdom 注入)或 this |
是 | true |
否 | 不适用 |
| webpack / Rollup / Vite | 取决于 target | 是 | true |
否 | 不适用 |
| AMD(RequireJS) | window |
否 | undefined |
是(但可被 noConflict 撤销) | 注册为 "jquery" |
| Web Worker | self |
否 | undefined |
self.jQuery |
不适用,且在行 29 处抛错(无 document) |
注意 :jQuery 4 加强了 ESM 支持。虽然这份构建产物是 UMD,但 npm 包里提供了
dist/module/jquery.js(ESM 版本),可以通过import jQuery from "jquery/src/jquery.js"使用。官方推荐的做法是import $ from "jquery"配合打包工具的别名解析。
1.4 模块依赖拓扑:为什么是这个顺序
打包后的单文件,模块顺序不是随意的。它必须满足每个模块在被"执行"时,它所依赖的东西已经就绪 。注意这里的关键词是"执行"而不是"引用"------由于函数提升(function declaration hoisting),很多函数可以在定义前被引用,但变量(var)不会提升其值。
我们来看几个关键的"必须如此"的约束。
1.4.1 约束一:jQuery 工厂必须先于 jQuery.fn
js
// 行 124-129
jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context );
};
// 行 131
jQuery.fn = jQuery.prototype = { ... };
jQuery 函数体内引用了 jQuery.fn.init,但它在调用时 才求值,所以 jQuery.fn 在后面定义没问题。这是函数体的惰性求值特性。
反过来,如果写成:
js
var init = jQuery.fn.init; // 立即求值 → jQuery.fn 还是 undefined → 报错
就不行。jQuery 里所有跨模块的"前向引用"都遵循这个原则:引用写在函数体内,不在顶层立即求值。
1.4.2 约束二:jQuery.extend 必须先于任何 jQuery.extend( {...} ) 调用
js
// 行 218
jQuery.extend = jQuery.fn.extend = function() { ... };
// 行 290 ------ 第一次真正使用
jQuery.extend( {
expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
...
} );
这是显然的,但引出一个重要观察:jQuery.extend 是 jQuery 的"元操作" 。它定义了整个库的扩展协议------所有静态工具用 jQuery.extend 挂载,所有实例方法用 jQuery.fn.extend 挂载。第 2、3 章会详细拆解它的深拷贝算法与安全防护。
1.4.3 约束三:access 必须早于所有用它的模块
access 定义在行 841,而它的使用者分布在:
- 行 907:
jQuery.fn.attr - 行 3904:
jQuery.fn.data - 行 5605:
jQuery.fn.text - 行 5678:
jQuery.fn.html - 行 6331:
jQuery.fn.css - 行 7144:
jQuery.fn.prop - 行 9460:
jQuery.fn.scrollLeft/scrollTop - 行 9500:
jQuery.fn.width/height/inner*/outer*
access 是 jQuery 的统一存取器,它把"get / set / 批量 set / 函数式 set"四种调用形态收敛成一个函数。它必须在所有这些模块之前定义。
为什么 access 定义在 841 行这么"早"的位置(紧跟选择器正则之后)?因为在 src/ 里它是 src/core/access.js,被 rollup 按依赖顺序排在 attributes 模块之前------而 attributes(903 行)是它的第一个使用者。
1.4.4 约束四:选择器引擎必须早于 traversing
jQuery.fn.find 定义于行 2550,它调用 jQuery.find(即 find 函数,行 1174)。而 find 又依赖:
setDocument(行 1412)------依赖jQuery.isXMLDoc(行 416)tokenize(行 674)------依赖jQuery.expr.preFilter(行 1504)、jQuery.expr.match(行 1470)compile(行 2339)------依赖matcherFromTokens(行 2149)select(行 2380)------依赖compile
而 jQuery.expr 的完整定义(行 1463-1927)本身又依赖:
jQuery.attr(行 918)→ 因为filter.ATTR(行 1543)用jQuery.attr( elem, name )取值jQuery.contains(行 426)→ 因为sortOrder和dir需要它jQuery.text(行 363)→ 因为pseudos.contains(行 1758)用jQuery.text( elem )
所以真实的依赖链是:
bash
core (toType/isArrayLike/contains/merge/grep/map/text)
↓
attributes (jQuery.attr) ──────────┐
↓ │
selector-var (正则/cache/testContext)│
↓ │
tokenize / preFilter │
↓ │
jQuery.expr (find/filter/pseudos) ←┘
↓
compile / matcherFromTokens / setMatcher
↓
select / find
↓
traversing (dir/siblings/winnow/jQuery.fn.find)
↓
init (jQuery.fn.init) ← 注意:init 在 traversing 之后!
1.4.5 一个反直觉的点:init 为什么在 2604 行这么晚
很多人以为 jQuery.fn.init 应该在文件开头(毕竟 jQuery() 就是调它)。但源码里它在行 2604,几乎在文件的 27% 处。
原因是:init 内部要调用 jQuery.parseHTML(行 9190)、jQuery.merge、jQuery.find、rootjQuery.ready 。虽然这些调用发生在运行时(惰性),但 init 函数体里还有一句立即执行的:
js
// 行 2708
rootjQuery = jQuery( document$1 );
这行在模块加载时立即执行,它调用 jQuery() → new jQuery.fn.init( document )。此时 init 必须已经定义(是的,就在上面),但更重要的是------init 内部如果走"选择器"分支会调用 ( context || rootjQuery ).find( selector ),而 find 方法定义在行 2550,刚好在 init 之前。
如果 init 排在 traversing 之前,rootjQuery = jQuery( document$1 ) 这行在极端情况下(虽然 document 走的是 nodeType 分支,不会调 find)可能出错。而 jQuery 团队选择了更保险的顺序:先定义好所有遍历能力,再定义 init。
顺带一提,rootjQuery 的注释(行 2595)说明了它的用途:
js
// A central reference to the root jQuery(document)
var rootjQuery,
它的核心作用是给 $( selector ) 无上下文时提供一个默认上下文(行 2693):
js
return ( context || rootjQuery ).find( selector );
以及给 $( function ) 提供 ready 入口(行 2621):
js
return rootjQuery.ready !== undefined ?
rootjQuery.ready( selector ) :
selector( jQuery );
1.4.6 约束五:为什么 offset 在倒数第二段(9295 行)
offset 模块依赖:
jQuery.css/jQuery.style(行 6134-6255)jQuery.fn.width/height系列(行 9488-9534,就在 offset 之后!)isWindow(行 75)access(行 841)
而 dimensions(width/height)又依赖 jQuery.css( elem, type, extra ) 的 extra 参数语义和 cssHooks.width/height。
所以顺序是:css 模块 → dimensions 生成 → offset。源码里 offset(9295)确实在 dimensions(9488)之前,这看似违反直觉,但仔细看:offset 里用到的 curElem.offset() 是自身递归,而 position() 里用的 jQuery( offsetParent ).offset() 也是自身。offset 并不依赖 dimensions 的 width/height。反过来,dimensions 里的 isWindow( elem ) 判断和 elem.document.documentElement[ "client" + name ] 也不依赖 offset。
两者是并列 的,顺序可以互换。rollup 按 src/offset.js 和 src/dimensions.js 的 import 顺序排布,而 src/jquery.js 里的 import 顺序决定了最终顺序。
1.4.7 约束六:deprecated 模块永远在最后
文件末尾的这些方法:
js
// 行 9551-9573
bind: function( types, data, fn ) { return this.on( types, null, data, fn ); },
unbind: function( types, fn ) { return this.off( types, null, fn ); },
delegate: function( selector, types, data, fn ) { return this.on( types, selector, data, fn ); },
undelegate: function( selector, types, fn ) { ... },
hover: function( fnOver, fnOut ) { ... }
它们全部是转发到已有方法的兼容层 。这类"依赖一切、被谁都不依赖"的模块,必须放在最后。这也是 src/deprecated.js 在 src/jquery.js 里最后一个 import 的原因。
1.4.8 拓扑图(依赖层次)
把上面的约束画成层次图:
bash
┌──────────────────────────────────────────────────────────────┐
│ L0 环境层:UMD 包装(11-31)、window.document 校验 │
├──────────────────────────────────────────────────────────────┤
│ L1 语言层:arr/slice/push/indexOf/flat/getProto/hasOwn │
│ toString/class2type/toType/isWindow/isArrayLike │
├──────────────────────────────────────────────────────────────┤
│ L2 框架层:jQuery 工厂 + jQuery.fn + jQuery.extend │
│ (定义"扩展协议") │
├──────────────────────────────────────────────────────────────┤
│ L3 工具层:each/map/grep/merge/makeArray/inArray/contains │
│ isPlainObject/isEmptyObject/text/globalEval │
├──────────────────────────────────────────────────────────────┤
│ L4 存取层:access(统一存取器) │
├──────────────────────────────────────────────────────────────┤
│ L5 属性层:jQuery.attr/attrHooks/removeAttr │
│ jQuery.prop/propHooks/propFix │
├──────────────────────────────────────────────────────────────┤
│ L6 选择器:tokenize → jQuery.expr → compile → find → select │
├──────────────────────────────────────────────────────────────┤
│ L7 遍历层:dir/siblings/winnow/jQuery.fn.find/filter/not/is │
├──────────────────────────────────────────────────────────────┤
│ L8 对象层:jQuery.fn.init + pushStack 链 │
├──────────────────────────────────────────────────────────────┤
│ L9 异步层:Callbacks → adoptValue → Deferred/when → ready │
├──────────────────────────────────────────────────────────────┤
│ L10 存储层:Data 类 → dataPriv/dataUser → queue │
├──────────────────────────────────────────────────────────────┤
│ L11 渲染层:buildFragment → domManip → css/style → 尺寸 │
├──────────────────────────────────────────────────────────────┤
│ L12 事件层:jQuery.event(add/remove/dispatch/trigger) │
├──────────────────────────────────────────────────────────────┤
│ L13 动画层:Tween → Animation → fx.tick │
├──────────────────────────────────────────────────────────────┤
│ L14 网络层:param → ajax → xhr/script/jsonp transports │
├──────────────────────────────────────────────────────────────┤
│ L15 兼容层:deprecated 方法、AMD、noConflict、导出 │
└──────────────────────────────────────────────────────────────┘
值得注意的是:L9(异步)排在 L11(渲染)之前 。这是因为动画层(L13)需要 Deferred 来实现 animate().promise(),而动画又属于渲染范畴。jQuery 把 Deferred 提前到渲染之前,正是为了满足动画的依赖。
1.5 jQuery 4.0 的 Breaking Changes 全景
这一节是本章最实用的部分。如果你的团队正在从 jQuery 3 迁移到 4,这张表就是你的 checklist。
jQuery 4.0 的破坏性变更可以分成四类:
- 彻底移除 (Removed):API 不存在了,调用会
TypeError; - 语义变更(Changed):API 还在,但行为变了;
- 内部重构(Internal):私有 API 变化,影响依赖内部实现的插件;
- 平台要求变更(Platform):支持的浏览器/环境变了。
1.5.1 彻底移除的 API
(1)类型判断与语言工具
| 移除的 API | 移除原因 | 替代方案 |
|---|---|---|
jQuery.isArray() |
ES5 起 Array.isArray 是标准,jQuery 从 3.2 起内部已直接用 Array.isArray |
Array.isArray( x ) |
jQuery.isFunction() |
typeof x === "function" 完全等价,且 jQuery 内部实现就是它 |
typeof x === "function" |
jQuery.isWindow() |
极少被外部使用,内部仍保留同名函数但不导出 | obj != null && obj === obj.window |
jQuery.type() |
语义模糊(new Number(1) 返回 "number" 而非 "object"),ES 生态已有 Object.prototype.toString 方案 |
Object.prototype.toString.call( x ).slice( 8, -1 ).toLowerCase() |
jQuery.trim() |
ES5 起 String.prototype.trim 是标准 |
str.trim() |
jQuery.now() |
Date.now() 是标准 |
Date.now() |
jQuery.isNumeric() |
语义复杂且极少使用,jQuery 3 起已废弃 | !isNaN( parseFloat( x ) ) && isFinite( x ) |
jQuery.parseJSON() |
JSON.parse 是标准 |
JSON.parse( x ) |
jQuery.nodeName() |
内部函数,外部几乎不用 | elem.nodeName.toLowerCase() === name |
为什么连 $.trim 这种无害的小函数都要删? 这不是洁癖,而是三个真实原因:
- 体积:每保留一个这样的函数,就多一份 API 文档、多一份测试、多一份"用户以为 jQuery 提供了字符串处理能力"的误导;
- 语义漂移风险 :
jQuery.trim在历史上曾经用正则实现(str.replace( /^\s+|\s+$/g, "" )),而原生trim()遵循 Unicode 空白定义。保留它意味着 jQuery 要持续跟进标准; - 教育意义:删掉它们,等于向社区明确传达"这些能力浏览器已经有了,不需要库"。
jQuery 团队的官方说法是:jQuery 4 的目标是"移除所有已被标准覆盖的工具函数"。这是一个清晰的边界声明。
(2)数组与集合方法
| 移除的 API | 移除原因 | 替代方案 |
|---|---|---|
jQuery.fn.size() |
与 .length 完全重复(内部就是 return this.length),从 1.8 起废弃 |
$el.length |
jQuery.fn.andSelf() |
1.8 起被 addBack() 取代(语义更清晰) |
$el.addBack() |
jQuery.unique() |
3.0 起改名 uniqueSort(因为它是"排序+去重"而不只是去重) |
jQuery.uniqueSort( arr ) |
jQuery.fn.push/sort/splice |
jQuery 对象伪装成数组的副产品,jQuery 3 移除 | 用 .get() 转真数组再操作 |
jQuery.fn.push/sort/splice 的移除值得一提。在 jQuery 1.x,jQuery 对象真的继承了 Array.prototype 的这些方法(jQuery.fn.push = Array.prototype.push)。这意味着 $el.push( domNode ) 能工作------但它会破坏 jQuery 对象的不变量(比如绕过 prevObject 链)。jQuery 3 移除了它们,jQuery 4 延续。
取而代之的是 jQuery.fn.toArray()(行 141)和 .get()(行 147),它们返回真数组,语义明确。
(3)事件相关
| 移除的 API | 移除原因 | 替代方案 |
|---|---|---|
jQuery.fn.bind() / unbind() |
1.7 起被 .on() / .off() 取代 |
.on() / .off() |
jQuery.fn.delegate() / undelegate() |
同上 | .on( events, selector, handler ) |
jQuery.fn.error() |
与 window.onerror 语义混淆,且 error 事件在 <img> 上有特殊行为 |
.on( "error", handler ) |
jQuery.fn.unload() |
unload 事件被浏览器废弃(破坏 bfcache),W3C 已标记删除 |
.on( "beforeunload", handler ) 或 pagehide |
jQuery.fn.toggle( fn1, fn2 ) |
与 toggle() 显示/隐藏语义冲突(一个名字两种行为) |
自己写状态机 |
重要澄清 :在这份 4.0 构建产物里,
bind/unbind/delegate/undelegate仍然存在 (行 9551-9567)。这说明 jQuery 4 并没有完全删掉它们,而是保留但标记为 deprecated。官方 4.0 的 changelog 里,这批方法计划在 5.0 移除。所以上表中"移除"应理解为"已废弃、计划移除"。请以你实际使用的 4.x 小版本的 changelog 为准。而
error/unload/toggle(fn1, fn2)则确实不在这份源码的事件快捷方法列表里(行 9576-9580 的 25 个事件名中没有它们),属于已移除。
(4)Deferred / Callbacks
| 移除的 API | 移除原因 | 替代方案 |
|---|---|---|
Deferred.pipe() 的独立地位 |
与 then() 功能重叠 |
.then()(pipe 作为别名保留,见行 3180 注释 "Keep pipe for back-compat") |
Deferred 的非 Promise/A+ 行为 |
对齐规范 | 使用标准 Promise 语义 |
(5)DOM 与选择器
| 移除的 API | 移除原因 | 替代方案 |
|---|---|---|
jQuery.fn.context 属性 |
jQuery 3 移除,属于内部状态暴露 | 无直接替代(用闭包保存上下文) |
jQuery.fn.selector 属性 |
同上,且选择器字符串无法反映 pushStack 后的真实状态 |
无直接替代 |
jQuery.boxModel |
早已无意义(所有支持浏览器都用 W3C 盒模型) | 无 |
jQuery.support 的部分属性 |
特性探测大幅简化 | 见 1.11 节 |
jQuery.expr[ ":" ] 作为扩展点 |
内部实现细节 | jQuery.expr.pseudos(行 9630 仍保留别名) |
(6)AJAX
| 移除的 API | 移除原因 | 替代方案 |
|---|---|---|
jQuery.ajax 的同步 XHR 默认路径 |
同步 XHR 已被标准废弃(主线程阻塞) | 使用 async + Promise |
jqXHR.success() / error() / complete() 方法 |
1.8 起废弃 | jqXHR.done() / fail() / always() |
jQuery.ajaxSetup 的部分可配置项 |
简化 | 见第 14 章 |
注意:
jQuery.ajax里仍然保留了options.async(行 8218 默认true),因为_evalUrl(行 8791)内部用了async: false来保证脚本执行顺序。这是 jQuery 内部的一个例外。
1.5.2 语义变更(最容易踩坑)
(1)attr( name, false ) 现在会移除属性
源码行 938-949:
js
if ( value !== undefined ) {
if ( value === null ||
// For compat with previous handling of boolean attributes,
// remove when `false` passed. For ARIA attributes -
// many of which recognize a `"false"` value - continue to
// set the `"false"` value as jQuery <4 did.
( value === false && name.toLowerCase().indexOf( "aria-" ) !== 0 ) ) {
jQuery.removeAttr( elem, name );
return;
}
变更内容:
| 调用 | jQuery 3 结果 | jQuery 4 结果 |
|---|---|---|
$(el).attr("checked", false) |
属性被设为字符串 "false" → 因为属性存在,元素被勾选 |
属性被移除 → 元素未勾选 |
$(el).attr("disabled", false) |
设为 "false" → 元素被禁用(反直觉!) |
移除 → 元素可用 |
$(el).attr("aria-expanded", false) |
设为 "false" |
仍设为 "false"(例外保留) |
$(el).attr("checked", null) |
移除属性 | 移除属性(不变) |
迁移建议:
js
// jQuery 3 写法(4.x 下行为改变)
$el.attr( "disabled", isDisabled ); // isDisabled 为 false 时,3.x 会禁用,4.x 会启用
// 推荐写法:显式区分
if ( isDisabled ) {
$el.attr( "disabled", "disabled" );
} else {
$el.removeAttr( "disabled" );
}
// 或者直接用 prop(语义更准确,布尔属性应该用 prop)
$el.prop( "disabled", isDisabled );
为什么 ARIA 例外? 因为 aria-* 属性不是布尔属性,它们是枚举字符串属性 。aria-expanded="false" 表示"展开状态为假",与"属性不存在"(表示"不适用")语义不同。屏幕阅读器依赖这个区分。如果 jQuery 4 统一移除,会破坏大量无障碍实现。所以 jQuery 团队做了这个针对性豁免------这是可访问性优先于一致性的典型决策。
(2)parseHTML 的脚本处理
行 9190-9224:
js
jQuery.parseHTML = function( data, context, keepScripts ) {
if ( typeof data !== "string" && !isObviousHtml( data + "" ) ) {
return [];
}
if ( typeof context === "boolean" ) {
keepScripts = context;
context = false;
}
var parsed, scripts;
if ( !context ) {
// Stop scripts or inline event handlers from being executed immediately
// by using DOMParser
context = ( new window.DOMParser() )
.parseFromString( "", "text/html" );
}
...
关键变化 :当不传 context 时,jQuery 4 使用 DOMParser 创建一个"死文档" (inert document)来解析 HTML。这个文档里的脚本不会执行 、<img src> 不会发起请求 、内联事件处理器不会生效。
这是重要的安全增强:过去 $.parseHTML( userInput ) 在没有 context 时会用 document.createElement("div") + innerHTML,虽然 <script> 标签不会执行(innerHTML 插入的 script 不执行),但 <img src=x onerror=alert(1)> 会执行(因为 img 会立即发起请求,onerror 会触发)。
用 DOMParser().parseFromString( "", "text/html" ) 创建的文档是惰性的:它的节点不会被加载资源、不会执行脚本。这从根本上消除了这条攻击路径。
对比:
| 场景 | jQuery 3(默认) | jQuery 4(默认,无 context) |
|---|---|---|
$.parseHTML('<img src=x onerror=alert(1)>') |
onerror 执行 | onerror 不执行 |
$.parseHTML('<script>alert(1)</script>') |
不执行(返回 script 节点) | 不执行 |
$.parseHTML(html, document) |
使用真实文档,img 会加载 | 同(显式传 context = 你要负责) |
含义 :如果你传了 context(通常是 document),就回到了"活跃文档"语义。所以安全准则是不传 context,或传一个 DOMParser 创建的惰性文档。
(3):hidden / :visible 与 isHiddenWithinTree 的分野
jQuery 4 引入了 isHiddenWithinTree(行 4095),它与 :hidden 伪类有两个明确差异(源码注释写得很清楚):
js
// isHiddenWithinTree reports if an element has a non-"none" display style (inline and/or
// through the CSS cascade), which is useful in deciding whether or not to make it visible.
// It differs from the :hidden selector (jQuery.expr.pseudos.hidden) in two important ways:
// * A hidden ancestor does not force an element to be classified as hidden.
// * Being disconnected from the document does not force an element to be classified as hidden.
// These differences improve the behavior of .toggle() et al. when applied to elements that are
// detached or contained within hidden ancestors (gh-2404, gh-2863).
| 判断函数 | 祖先 display:none 时 | 脱离文档时 | 用途 |
|---|---|---|---|
:hidden(行 8881) |
元素被判为 hidden(因为 offsetWidth 为 0) | 判为 hidden | 选择器 |
isHiddenWithinTree(行 4095) |
不因此判为 hidden | 不因此判为 hidden | show() / hide() / toggle() / fadeTo() |
为什么改? 考虑这个场景:
html
<div id="container" style="display:none">
<div id="target">内容</div>
</div>
js
$( "#target" ).toggle();
- jQuery 3:
:hidden判定 target 为 hidden(因为祖先不可见,offsetWidth=0)→toggle()会show()→ 给 target 设置display:block,但container 还是 none,所以视觉上什么都没变,而 target 的 inline style 被污染了。再 toggle 一次,它又"隐藏"了。用户体验是"点了没反应"。 - jQuery 4:
isHiddenWithinTree只看 target 自己 → target 的display不是none→ 判为 visible →toggle()执行hide()→ 给 target 设display:none。再 toggle 就显示。行为符合直觉。
这就是注释里说的 "improve the behavior of .toggle() et al."。
(4)jQuery.fn.ready 的返回与异常
行 3528-3541:
js
jQuery.fn.ready = function( fn ) {
readyList
.then( fn )
// Wrap jQuery.readyException in a function so that the lookup
// happens at the time of error handling instead of callback
// registration.
.catch( function( error ) {
jQuery.readyException( error );
} );
return this;
};
ready 回调里的异常现在会走 readyException(行 3519):
js
jQuery.readyException = function( error ) {
window.setTimeout( function() {
throw error;
} );
};
window.setTimeout 包装是为了让异常在独立的宏任务 里抛出,从而能被 window.onerror / unhandledrejection 捕获,而不是被 Deferred 的异常吞掉逻辑静默处理。
1.5.3 内部重构(影响插件作者)
如果你的插件依赖了以下内部机制,jQuery 4 会让它失效:
| 内部 API | jQuery 4 的变化 | 影响 |
|---|---|---|
Sizzle 全局对象 |
完全不存在,只剩 jQuery.find |
直接用 Sizzle(...) 的插件会崩 |
jQuery.find.matchesSelector |
仍在(行 1446),但语义依赖 setDocument 已被调用 |
需先调用 jQuery.find.setDocument() |
jQuery.expr.filters |
仍是 pseudos 的别名(行 9630) |
无影响 |
jQuery._data / _removeData |
仍在(行 3856/3860),但注释说 "these can be deprecated" | 计划废弃,改用 dataPriv |
jQuery.event.special 结构 |
新增字段语义不变 | 无影响 |
jQuery.cssHooks |
结构不变 | 无影响 |
jQuery.support |
属性大幅减少(只剩 reliableTrDimensions / reliableColDimensions,见行 5960) |
依赖 support.boxSizing 等的插件会拿到 undefined |
最后一条值得展开:jQuery 4 的 jQuery.support 只剩两个属性。行 5960-5970:
js
jQuery.extend( support, {
reliableTrDimensions: function() {
computeTableStyleTests();
return reliableTrDimensionsVal;
},
reliableColDimensions: function() {
computeTableStyleTests();
return reliableColDimensionsVal;
}
} );
而在 jQuery 3.x 里,jQuery.support 曾有十几个属性(boxSizing、boxSizingReliable、pixelPosition、reliableHiddenOffsets、reliableMarginRight、clearCloneStyle、focusinBubbles、ajax 等)。
jQuery 4 把这些都删了,原因是:支持的浏览器集合收敛后,大部分探测不再必要 。剩下的两个(表格行/列尺寸可靠性)是因为它们涉及真实的、仍未修复的浏览器 bug(Firefox 的 tr computed height 含 border、Safari 的 col computed width 为 0)。
而且这两个属性是函数 而不是布尔值------因为它们需要延迟到第一次真正需要时才执行探测 (computeTableStyleTests 会创建一个真实的 table 并插入 DOM,这是一次布局开销)。这是"惰性探测"的设计。
1.5.4 平台要求变更
| 项目 | jQuery 3.x | jQuery 4.x |
|---|---|---|
| IE | 支持 IE 9+(3.x),3.0 起 IE 9+ | 支持 IE 11(移除了 IE 9/10 的 workaround,但保留 IE 11 的) |
| 从源码看 | --- | var isIE = document$1.documentMode;(行 531)+ 大量 Support: IE 11+ 注释 |
| Android | 支持 4.0+ | 支持现代 Android WebView |
| Node | 需要 jsdom | 需要 jsdom(提供 window/document) |
| Trusted Types | 不感知 | 支持 TrustedHTML 包装(isObviousHtml) |
| ESM | 仅 UMD/CJS | 提供 ESM 构建 |
关于 IE 11:这份源码里仍有大量 IE 分支,例如:
js
// 行 531
var isIE = document$1.documentMode;
documentMode 是 IE 特有的属性(IE 8+ 有,标准浏览器没有)。jQuery 4 用它作为 IE 检测的唯一手段------这比 UA 嗅探可靠得多。
IE 相关的分支包括但不限于:
| 位置 | 处理的 IE 问题 |
|---|---|
行 990 attrHooks.type |
input 变成 radio 后会丢失 value |
| 行 1436 iframe unload | 访问已卸载 iframe 的文档会抛 "Permission denied" |
行 1828 selected 伪类 |
需要访问 parentNode.selectedIndex 才能让浏览器正确处理 optgroup 默认选中 |
| 行 5171 clone 时 textarea | IE 克隆 textarea 时 defaultValue 不正确 |
行 5307 focusMappedHandler |
IE 的 focusin/focusout 与其他浏览器时序不同 |
| 行 5330 focus/blur setup | IE 需要显式绑定 focusin/focusout |
| 行 6188 background 属性 | IE 克隆元素时 background-* 会影响源元素(trac-8908) |
行 7228 propHooks.selected |
IE 的 selected 需要强制访问 selectedIndex |
行 7564 valHooks.option |
IE 的 option.text 会抛异常(trac-14686, trac-14858) |
这解释了为什么 jQuery 4 尽管"现代化"了,仍有 30+ 处 IE 分支------因为 IE 11 仍在支持范围内。如果你的项目不需要支持 IE 11,可以使用 jQuery 的自定义构建(见 1.7 节)移除这些分支。
1.6 版本演进史:从 1.0 到 4.0 的关键节点
理解"为什么现在是这个样子",需要知道"过去是什么样子"。这一节梳理 jQuery 的四个大版本中,对当前源码形态影响最深远的决策。
1.6.1 jQuery 1.x(2006-2016):建立范式
| 版本 | 年份 | 关键贡献 | 今日源码中的遗存 |
|---|---|---|---|
| 1.0 | 2006-08 | 首次发布。链式调用、$() 选择器、AJAX 封装 |
jQuery.fn 原型链、pushStack |
| 1.1 | 2007-01 | 引入 Sizzle 前身的选择器引擎 | jQuery.find |
| 1.2 | 2007-09 | 引入 .animate() 与 effects |
Tween / Animation |
| 1.3 | 2009-01 | 引入 Sizzle 作为独立选择器引擎 (这是最大的一次架构升级);引入事件委托 .live() |
jQuery.expr / compile |
| 1.4 | 2010-01 | 性能大幅优化、.delegate() |
jQuery.event.dispatch 的委托路径 |
| 1.5 | 2011-01 | 引入 jQuery.Deferred 和 jQuery.Callbacks,重写 AJAX |
第 12 章的全部内容 |
| 1.6 | 2011-05 | 引入 .prop(),区分 attribute 与 property(这是 jQuery 历史上最重要的概念澄清之一) |
jQuery.prop / attrHooks |
| 1.7 | 2011-11 | 引入 .on() / .off(),统一事件绑定 |
on() 的参数洗牌 |
| 1.8 | 2012-08 | 引入 $.Deferred 的 then 重写;addBack() 取代 andSelf() |
addBack(行 2794) |
| 1.9 | 2013-01 | 大规模清理废弃 API;引入 $.CSS hooks 体系 |
cssHooks |
| 1.12 | 2016-01 | 1.x 最后一个版本 | --- |
1.6 的 attr/prop 分裂 值得单独说。在 1.6 之前,jQuery 只有 attr()。这导致一个经典 bug:
js
$( "#checkbox" ).attr( "checked", true ); // 期望勾选
// 1.5 及之前:设置 attribute="checked",视觉上勾选
// 但读取时:$( "#checkbox" ).attr( "checked" ) 返回 "checked"(字符串),
// 而用户点击后,attribute 不再变化,所以读到的永远是初始值
1.6 之后:
js
$( "#checkbox" ).prop( "checked", true ); // 操作 DOM property(当前状态)
$( "#checkbox" ).attr( "checked", "checked" ); // 操作 HTML attribute(初始值)
这个区分在今天已成为常识,但它花了 jQuery 五年才理清。第 8 章会完整对比 attr 与 prop 的实现差异。
1.6.2 jQuery 2.x(2013-2016):瘦身实验
2.0 的唯一目标是移除 IE 6/7/8 支持,体积减少约 12%。它和 1.9 是同步开发的(API 完全一致,只是兼容层不同)。
2.x 的历史意义是证明了"放弃旧浏览器能带来实质收益 ",为 3.x/4.x 的进一步瘦身铺路。但 2.x 也留下了教训:同时维护两条产品线(1.x 和 2.x)的成本极高,所以 3.x 又合并回了单线。
1.6.3 jQuery 3.x(2016-2023):现代化与 Promise 化
| 版本 | 年份 | 关键变化 | 在 4.0 源码中的体现 |
|---|---|---|---|
| 3.0 | 2016-06 | Deferred 兼容 Promises/A+ ;.then() 语义对齐;移除 .load() / .unload() / .error();requestAnimationFrame 用于动画;.unwrap( selector ) 支持参数 |
then 里的 maxDepth 逻辑(行 3212) |
| 3.1 | 2016-09 | 移除 jQuery.fn.push/sort/splice;改进 SVG 支持 |
--- |
| 3.2 | 2017-03 | 弃用 $.isArray 等;内部改用 Array.isArray |
全文件用 Array.isArray |
| 3.3 | 2018-01 | 弃用 $.isFunction / $.isWindow;改进 .css() 对自定义属性的支持 |
rcustomProp(行 5761) |
| 3.4 | 2019-04 | 修复 jQuery.extend(true, {}, {__proto__: ...}) 的原型污染(CVE-2019-11358) |
行 256 的 __proto__ 检查 |
| 3.5 | 2020-04 | 修复 htmlPrefilter 的 XSS(CVE-2020-11022/11023) |
htmlPrefilter(行 5507)+ rnoInnerhtml |
| 3.6 | 2021-03 | JSONP 的 CSP 兼容改进;:visible / :hidden 改进 |
canUseScriptTag(行 8999) |
| 3.7 | 2023-05 | Sizzle 并入主仓库 (不再是独立依赖);.uniqueSort 改进 |
选择器代码全在同一文件 |
3.4 的原型污染修复是理解行 256 的关键:
js
// 行 254-258
// Prevent Object.prototype pollution
// Prevent never-ending loop
if ( name === "__proto__" || target === copy ) {
continue;
}
在 jQuery 3.4 之前,$.extend( true, {}, JSON.parse('{"__proto__":{"polluted":true}}') ) 会真的修改 Object.prototype ,导致所有对象都获得 polluted 属性------这是一个严重的原型污染漏洞。修复方式就是在深拷贝时跳过 __proto__ 键。
注意它同时检查了 target === copy(防止循环引用导致无限递归)。这两个检查共用一行 continue,是压缩优化的写法。
3.5 的 XSS 修复 则涉及 htmlPrefilter。在 3.5 之前,htmlPrefilter 会对 HTML 字符串做正则替换以修正自闭合标签(如 <div/> → <div></div>),而这个正则可以被构造的输入绕过,导致 <style> 标签被注入。3.5 的修复是让 htmlPrefilter 只在必要时工作。
在 4.0 里,htmlPrefilter 已经退化为空函数:
js
// 行 5507-5509
htmlPrefilter: function( html ) {
return html;
},
但它保留为一个公开的可扩展点(插件可以覆盖它)。这是"保留钩子、移除默认行为"的做法。
1.6.4 jQuery 4.0(2024-2026):本系列的主角
4.0 的正式发布经历了较长的 beta 周期(4.0.0-beta 在 2024 年初发布)。核心目标:
- 移除所有被标准覆盖的 API(见 1.5.1);
- 彻底内部化 Sizzle(不再作为可独立使用的库);
- 安全增强 :Trusted Types、
DOMParser惰性解析、XSS 面收敛; - Deferred 完全对齐 Promises/A+;
- 支持现代构建:ESM 输出、更好的 tree-shaking;
- 修复长期存在的语义 bug :
attr(false)、:hidden的级联判定等。
从源码的 Date: 2026-01-18T00:20Z 看,这份构建产物属于 4.0.x 的一个后期补丁版本。
1.6.5 演进规律总结
回顾这条 20 年的演进线,可以提炼出三条规律:
-
每一次"删除"都伴随着"标准已经提供" 。jQuery 的历史就是"浏览器追赶 jQuery"的历史:
querySelectorAll取代了 Sizzle 的大部分功能、fetch与 Promise 取代了$.ajax的一部分、classList取代了addClass/removeClass的一部分、Array.isArray取代了$.isArray。jQuery 4 的删除清单,本质上是一份"浏览器已经赢了"的清单。 -
每一次"新增"都指向"浏览器仍然不一致的地方" 。jQuery 4 保留并强化的能力------委托事件的边界判定、盒模型的跨浏览器测量、表格尺寸探测、
:disabled的 fieldset 继承------都是标准有定义但实现有分歧的领域。 -
API 设计的教训会被固化成约定 。attr/prop 的分裂教会了 jQuery "attribute 与 property 是两个东西";
bind/on的更替教会了它"参数重载不要太复杂";toggle的双重语义教会了它"一个函数名只能有一个含义"。这些教训在 4.0 的 API 设计里都有体现。
1.7 构建体系:从 src/ 到 dist/
你手上的 jquery-4.0.0.js 是构建产物,不是手写源码。要真正参与 jQuery 开发或做定制构建,必须理解它的构建管线。
1.7.1 构建工具链
jQuery 4 使用的主要工具:
| 工具 | 用途 | 说明 |
|---|---|---|
| npm | 包管理与脚本入口 | package.json 里定义 build / test / lint 等脚本 |
| Rollup | 模块打包(生成 UMD / ESM) | 替代了 jQuery 3 时代的自行 concat 方案;$1 后缀就是它的产物 |
| Terser / UglifyJS | 压缩 | 生成 jquery.min.js |
| ESLint | 代码风格与静态检查 | 配置文件在 src/.eslintrc.json 等 |
| QUnit | 单元测试 | jQuery 自研的测试框架,全量测试有 6000+ 断言 |
| jsdom | Node 环境下的 DOM 模拟 | 让 jQuery 能在 Node 里跑测试 |
| BrowserStack | 跨浏览器测试 | 覆盖 IE 11 到最新 Chrome/Safari/Firefox |
| Grunt(历史) | jQuery 3 时代的构建工具 | jQuery 4 已迁移到 npm scripts + Rollup |
1.7.2 构建产物一览
jQuery 4 的 dist/ 目录通常包含:
| 文件 | 说明 | 适用场景 |
|---|---|---|
jquery.js |
完整未压缩版(就是你手上这份) | 开发、调试、源码阅读 |
jquery.min.js |
压缩版(约 85KB,gzip 后约 30KB) | 生产环境 |
jquery.min.map |
Source map | 生产环境调试 |
jquery.slim.js |
精简版(移除 AJAX 与 effects) | 只需要 DOM 操作的场景 |
jquery.slim.min.js |
精简压缩版 | 生产 |
jquery.module.js |
ESM 版本 | 现代打包工具 |
jquery.factory.js |
工厂版本(不自动污染全局) | 需要完全控制全局的场景 |
slim 版本移除了:
- 整个
ajax模块(含$.ajax/$.get/$.post/$.getJSON/$.getScript/$.param/$.fn.load) - 整个
effects模块(含$.fn.animate/slideDown/fadeIn/$.fn.stop/$.fn.finish/$.fn.delay) - 相关的
queue/dequeue中的 fx 特化逻辑
slim 版大约能省 25-30% 的体积。如果你在做的是一个只需要"选元素 → 改 class → 绑事件"的轻量页面,slim 是合理选择。
1.7.3 自定义构建
jQuery 支持通过命令行排除模块:
bash
# 排除 ajax 与 effects(等价于 slim)
npm run build -- --exclude=ajax --exclude=effects
# 只保留 core / selector / traversing / attributes / event
npm run build -- --exclude=ajax,effects,css,offset,dimensions,wrap,serialize
# 排除 deprecated 模块(进一步瘦身)
npm run build -- --exclude=deprecated
可排除的模块名(对应 src/ 下的文件名):
bash
ajax, ajax/jsonp, ajax/script, ajax/xhr,
attributes/attr, attributes/classes, attributes/prop, attributes/val,
callbacks, core, css, css/addGetHookIf, css/curCSS, css/support,
data, deferred, deprecated, dimensions, effects, event, event/focusin,
manipulation, offset, queue, selector, serialize, traversing, wrap
自定义构建的注意事项:
- 模块间有隐式依赖 ,随便排除会导致运行时错误。例如排除
css会让effects失效,排除data会让event失效(事件存储在 dataPriv 里); - 排除
deprecated会移除bind/unbind/delegate/undelegate/hover/push等兼容方法,如果你的老代码或第三方插件用了它们,会报错; - 构建产物不会包含
jQuery.fn.load(它在 ajax 模块)和$.fn.animate(effects 模块),对应代码需要改写。
1.7.4 从构建产物反推源码结构
既然手上是打包后的文件,如何找到对应的 src/ 文件?几个技巧:
技巧 1:用 $1 后缀作为模块标记。 出现 xxx$1 说明有两个模块定义了 xxx,其中一个被重命名。据此可以推断模块边界。
技巧 2:用注释里的 "Support:" 和 issue 编号定位。 在 jQuery 仓库里搜索 gh-4529,就能找到对应的源码文件和 commit。
技巧 3:用函数名直接搜索。 jQuery 的构建不做函数名混淆(jquery.min.js 才混淆),所以 matcherFromGroupMatchers 在 src/selector.js 里能直接搜到。
技巧 4:看 jQuery.fn.extend / jQuery.extend 的调用块。 每一个调用块通常对应一个源文件,例如:
js
// 这一段对应 src/manipulation.js
jQuery.fn.extend( {
detach: ...,
remove: ...,
text: ...,
append: ...,
...
} );
1.7.5 一个实践建议:建立自己的" annotated 源码"
如果你要长期维护 jQuery 项目,建议做一件事:把这份 jquery-4.0.0.js 复制一份,直接在里面加中文注释。
具体做法:
-
复制为
jquery-4.0.0.annotated.js(不要改动原文件); -
每读透一个模块,就在里面加注释块,格式统一为:
js/* @annotate 2026-01-21 * 模块:selector / 函数:matcherFromTokens * 作用:... * 关键:... * 陷阱:... */ -
配合编辑器的折叠功能,逐步积累。
这样做的好处是:注释与代码同处一个文件,调试时断点打到哪,注释就在旁边。比起另写笔记,效率高得多。
1.8 代码风格与工程规范
jQuery 的代码风格极其一致,这是二十年、数百名贡献者维护的结果。这些约定值得在任何团队项目中借鉴。
1.8.1 缩进与空白
- Tab 缩进(不是空格);
- 括号内侧加空格 :
function( a, b )而不是function(a, b);if ( x )而不是if(x); - 对象字面量的冒号后加空格 :
{ a: 1 }; - 逗号后加空格 :
[ 1, 2, 3 ]; - 数组/对象字面量内侧加空格 :
[ 1, 2 ]而不是[1, 2](这条比较特殊,很多项目不这么做);
这套风格由 ESLint 的 space-in-parens、array-bracket-spacing、object-curly-spacing 等规则强制执行。
为什么要这样? 主要是历史原因 + 可读性:jQuery 早期代码里大量使用 for ( i = 0; i < len; i++ ) 这样的紧凑循环,加空格能让密集的符号更易分辨。现代项目不一定需要照搬,但一致性本身是必须学习的。
1.8.2 严格模式
文件里有两处 "use strict":
js
// 行 13(UMD 包装内部)
( function( global, factory ) {
"use strict";
...
} )( ... );
// 行 27(factory 内部)
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
"use strict";
为什么要两处?因为严格模式的传播不跨函数边界 。外层 IIFE 的 "use strict" 只作用于它自己的函数体,factory 是另一个函数,需要自己的 "use strict"。
严格模式带来的实际影响(在 jQuery 代码里能看到应对):
| 严格模式约束 | jQuery 的应对 |
|---|---|
| 未声明变量赋值报错 | 所有变量都有 var / let 声明 |
delete 不可删除属性报错 |
Data.remove 里用 delete 时确保属性 configurable: true(行 3656-3659) |
函数内 this 不再自动装箱为对象 |
需要 this 时显式传入(如 access 里的 fn.call( elems, value )) |
arguments.callee 禁用 |
用具名函数表达式(如行 3008 的 ( function add( args ) {...} )( arguments )) |
with 禁用 |
无 |
| 八进制字面量禁用 | 无 |
行 3008 这个例子很典型:
js
( function add( args ) {
jQuery.each( args, function( _, arg ) {
if ( typeof arg === "function" ) {
if ( !options.unique || !self.has( arg ) ) {
list.push( arg );
}
} else if ( arg && arg.length && toType( arg ) !== "string" ) {
// Inspect recursively
add( arg ); // ← 用具名函数表达式实现递归
}
} );
} )( arguments );
如果不用具名函数表达式,就得用 arguments.callee(严格模式禁用)。这是严格模式下的标准递归写法。
1.8.3 ESLint 的特殊注释
源码里散落着 ESLint 的开关注释,它们本身也是信息:
js
// 行 1054
// eslint-disable-next-line eqeqeq
compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ?
eqeqeq 规则要求用 === 而非 ==。这里故意用 ==,注释解释了原因(行 1051-1053):
IE sometimes throws a "Permission denied" error when strict-comparing two documents; shallow comparisons work.
也就是说,在 IE 里跨 iframe 严格比较两个 document 对象会抛异常 ,用 == 反而安全。这是一个"为了兼容性主动违反 lint 规则"的实例,而注释是必须的------否则下一个人会以为这是疏忽并"修复"它。
另一处:
js
// 行 1829
// eslint-disable-next-line no-unused-expressions
elem.parentNode.selectedIndex;
no-unused-expressions 会警告"这条语句没有副作用"。而这里就是要它的副作用 (访问 selectedIndex 会迫使浏览器更新 selected 状态)。注释说明了意图。
工程启示 :当你需要违反 lint 规则时,必须写注释说明原因 ,并且尽量用 eslint-disable-next-line(精确到行)而不是 eslint-disable(整个文件)。
1.8.4 注释规范:Support: 标签
jQuery 有一个独特的注释约定:所有兼容性相关的代码,都加 Support: 前缀的说明,格式为:
js
// Support: <浏览器> <版本范围>
// <问题描述>
例如:
js
// Support: IE 9 - 11+
// IE's :disabled selector does not pick up the children of disabled fieldsets
js
// Support: Chrome <=35 - 45+
// Webkit & Blink performance suffers when deleting properties
// from DOM nodes, so set to undefined instead
js
// Support: Firefox 135+
// Firefox always reports computed width as if `span` was 1.
这三段信息------哪个浏览器、什么版本范围、什么问题------构成了 jQuery 的"兼容性知识库"。它们有四个价值:
- 防止误删:后人看到这段奇怪代码,知道它是有原因的;
- 便于清理 :当某个浏览器版本退出支持列表时,可以直接搜索
Support: IE 9批量删除; - 问题定位:当你遇到诡异 bug 时,搜索相关关键词能快速找到历史原因;
- 知识传承:新贡献者能理解"为什么这里要这么写"。
统计一下这份文件里的 Support: 注释:IE 相关约 25 处、Firefox 约 6 处、Chrome 约 3 处、Safari 约 2 处。这个分布本身就说明了 jQuery 4 的兼容性重心。
1.8.5 命名约定
| 约定 | 例子 | 说明 |
|---|---|---|
正则变量以 r 开头 |
rquickExpr、rnothtmlwhite、rtypenamespace、rcssNum |
一眼看出是 RegExp |
正则的 replace 回调以 f 开头 |
fcamelCase(行 3602)、funescape(行 644)、fcssescape(行 1009) |
与正则配对 |
布尔返回的函数以 is 开头 |
isArrayLike、isWindow、isObviousHtml、isHiddenWithinTree、isAutoPx、isAttached |
|
| 内部工具以小写驼峰 | toType、access、winnow、condense、dir、sibling |
|
| 构造函数大写 | Data、Tween、Animation、jQuery.Event |
|
"私有"方法以 _ 开头 |
_data、_removeData、_queueHooks、_evalUrl |
约定意义上的私有(JS 无真正私有) |
缓存对象以 Cache 结尾 |
tokenCache、classCache、compilerCache、nonnativeSelectorCache、defaultDisplayMap |
1.8.6 一个值得学习的细节:变量声明的"分组"
看行 1122-1172 的声明块:
js
var i,
outermostContext,
// Local document vars
document,
documentElement,
documentIsHTML,
// Instance-specific data
dirruns = 0,
done = 0,
classCache = createCache(),
compilerCache = createCache(),
nonnativeSelectorCache = createCache(),
// Regular expressions
...
jQuery 用空行 + 注释 把一大坨 var 分成语义组:循环变量、文档变量、实例数据、正则、其他。这比"一个 var 一行"更易读,也比"全部挤在一起"清晰。
类似的分组在 jQuery.ajax(行 8308-8359)里达到了极致------用注释分成"URL 相关"、"响应相关"、"超时相关"、"状态相关"、"Deferred 相关"、"请求头相关"等七八组,一共 50 多个变量。这种在长函数里用注释做分区的技巧,对于无法进一步拆分的大函数很实用。
1.9 全局单例、多版本共存与 noConflict
1.9.1 单例的本质
jQuery 是一个 IIFE 返回的对象,整个库的状态都在这个闭包里。这意味着:
- 每加载一次 jQuery 文件,就创建一个全新的 jQuery 实例 (新的闭包、新的
class2type、新的Data.uid序列、新的expando); - 同一页面加载两次 jQuery(不同版本),就有两个完全独立的 jQuery,"互不相认"。
这既是优点(隔离性)也是问题(重复加载、体积翻倍、跨版本对象不兼容)。
1.9.2 _jQuery / _$ 的备份时机
js
// 行 9651-9657
var
// Map over jQuery in case of overwrite
_jQuery = window.jQuery,
// Map over the $ in case of overwrite
_$ = window.$;
这两行在文件的最末尾 (行 9651),也就是在 window.jQuery = window.$ = jQuery(行 9675)之前执行。
顺序至关重要:
javascript
9651: _jQuery = window.jQuery // 先备份"上一个" jQuery(可能是 3.7 版本的)
9654: _$ = window.$ // 先备份"上一个" $
9675: window.jQuery = window.$ = jQuery // 再覆盖为"我"(4.0)
这样 noConflict 才能把 3.7 版本的引用恢复回去。
1.9.3 noConflict 的实现
js
// 行 9659-9669
jQuery.noConflict = function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
};
两个细节:
(1)条件判断 window.$ === jQuery。 如果页面上在 jQuery 4 之后又加载了另一个库(比如 Prototype.js)也占用了 $,那么 window.$ 就不等于 jQuery 4 了,此时不应该 恢复(否则会覆盖掉 Prototype 的 $)。这个防御很必要。
(2)deep 参数控制是否也恢复 jQuery。 默认只恢复 $(更常用,因为 $ 冲突最频繁),传 true 才连 jQuery 一起恢复。
典型用法:
html
<script src="jquery-3.7.1.js"></script>
<script src="jquery-4.0.0.js"></script>
<script>
// 此时 window.jQuery 和 window.$ 都指向 4.0.0
var $jq4 = jQuery.noConflict( true );
// 现在 window.jQuery 和 window.$ 恢复到 3.7.1
// $jq4 是 4.0.0 的引用
// 老代码用 $(3.7.1),新代码用 $jq4(4.0.0)
$( "#old" ).on( "click", ... ); // 3.7.1
$jq4( "#new" ).on( "click", ... ); // 4.0.0
</script>
这是渐进式升级 jQuery 版本的标准手法:让新旧版本共存,逐步迁移代码。
1.9.4 多版本共存的陷阱
虽然 noConflict 让共存成为可能,但有几个坑:
坑 1:跨版本的对象不兼容。
js
var $3 = jQuery.noConflict( true ); // 3.7.1
// $jq4 是 4.0.0
var el = $3( "#foo" );
$jq4( el ).addClass( "bar" ); // 能工作吗?
能工作。$( jQueryObject ) 会走 jQuery.makeArray( selector, this )(行 2642),因为 jQuery 对象是类数组。但:
js
$jq4( "#foo" ).data( "x", 1 );
$3( "#foo" ).data( "x" ); // undefined!
不能工作 ,因为两个版本的 expando 不同(expando 里含随机数,见 1.10 节),数据存储完全隔离。
坑 2:事件不互通。
js
$jq4( "#btn" ).on( "click", handler );
$3( "#btn" ).trigger( "click" ); // handler 不会执行
因为 4.0 的 trigger 走的是自己的 dataPriv(自己的 expando),找不到 3.7 注册的事件。
坑 3:体积翻倍。 两个版本各 85KB(min),共 170KB。
最佳实践 :共存只是过渡手段,应该设定明确的迁移截止日期。同时,避免在同一元素上混用两个版本。
1.9.5 expando:多实例隔离的核心
js
// 行 292-293
// Unique for each copy of jQuery on the page
expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
这行代码非常精妙,值得逐字符分析:
js
version + Math.random()
version是"4.0.0"(字符串);Math.random()返回类似0.123456789012345;- 字符串 + 数字 = 字符串拼接:
"4.0.00.123456789012345"。
js
.replace( /\D/g, "" )
\D匹配所有非数字字符,全局替换为空;- 结果:
"400123456789012345"(去掉了所有小数点)。
js
"jQuery" + "400123456789012345"
- 最终:
"jQuery400123456789012345"。
为什么要用 version + Math.random() 而不是只用 Math.random()?
- 可读性 :看到
jQuery400...能立刻知道是 jQuery 4.0 的实例; - 调试友好 :在 DevTools 里查看元素属性时,
jQuery400123...比jQuery0123...信息量更大; - 降低碰撞概率 :虽然
Math.random()已经足够随机,但加上版本号进一步降低与其他库碰撞的可能。
为什么要用 Math.random()? 这正是为了多实例隔离 。假设 expando 是固定的 "jQuery":
- 页面加载 jQuery 3.7(expando =
"jQuery")和 jQuery 4.0(expando ="jQuery"); - jQuery 3.7 在元素上存数据:
el.jQuery = { data: ... }; - jQuery 4.0 也在元素上存数据:
el.jQuery = { data: ... }; - 后者覆盖前者,两个版本的数据互相污染。
有了随机数,两个版本的 expando 不同,各存各的属性,互不干扰。
expando 的三个用途:
| 用途 | 源码位置 | 说明 |
|---|---|---|
| Data 存储的键名 | 行 3626:this.expando = jQuery.expando + Data.uid++ |
dataPriv.expando = "jQuery400...1",dataUser.expando = "jQuery400...2" |
| 标记"这是 jQuery 包装的函数" | 行 1306:fn[ jQuery.expando ] = true |
markFunction 用它标记需要参数的伪类工厂 |
| 组合子匹配的缓存键 | 行 1630:parent[ jQuery.expando ] |
:nth-child 的索引缓存 |
注意 Data 的 expando 还加了 Data.uid++:
js
// 行 3625-3629
function Data() {
this.expando = jQuery.expando + Data.uid++;
}
Data.uid = 1;
dataPriv = new Data()→expando = "jQuery400...1"(uid 从 1 开始,1++ 后变 2)dataUser = new Data()→expando = "jQuery400...2"
所以同一个 jQuery 实例内部,私有数据和用户数据也用不同的键 。这保证了 $.removeData() 不会误删 jQuery 内部事件数据。
1.9.6 一个关于 Math.random() 的隐患
Math.random() 不是密码学安全的随机数。理论上,如果攻击者能预测 expando,就能构造恶意数据。但在实践中这不是威胁,因为:
- expando 只是属性名,不承载权限;
- 攻击者在页面里执行 JS 时,已经可以做任何事。
不过 jQuery 团队在历史上确实考虑过这个问题。在某些早期版本里用过 jQuery.expando = "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ) 这个写法,一直沿用至今。
1.10 support:特性探测的极简化
前面提到 jQuery 4 的 jQuery.support 只剩两个函数属性(行 5960)。这一节展开讲它的设计。
1.10.1 从"属性表"到"惰性函数"
jQuery 3.x 的 support 是一个布尔属性表:
js
// jQuery 3.x 的示意(非本文源码)
jQuery.support = {
boxSizing: true,
boxSizingReliable: true,
pixelPosition: false,
reliableHiddenOffsets: true,
reliableMarginRight: true,
clearCloneStyle: true,
focusinBubbles: false,
ajax: true,
...
};
这些值在库加载时就通过计算得出。问题:
- 启动开销:每次加载 jQuery 都要执行十几次 DOM 探测(创建元素、插入、测量、移除),这会拖慢页面启动;
- 准确性问题:某些探测在特定条件下会误判(如页面尚未渲染时测量不准);
- 维护负担:浏览器修了 bug 后,这些探测就变成了无用代码。
jQuery 4 的做法是把探测延迟到真正需要时,并且只保留真正必要的:
js
// 行 5883-5970
var reliableTrDimensionsVal, reliableColDimensionsVal,
table = document$1.createElement( "table" );
// Executing table tests requires only one layout, so they're executed
// at the same time to save the second computation.
function computeTableStyleTests() {
if (
// This is a singleton, we need to execute it only once
!table ||
// Finish early in limited (non-browser) environments
!table.style
) {
return;
}
var trStyle,
col = document$1.createElement( "col" ),
tr = document$1.createElement( "tr" ),
td = document$1.createElement( "td" );
table.style.cssText = "position:absolute;left:-11111px;" +
"border-collapse:separate;border-spacing:0";
tr.style.cssText = "box-sizing:content-box;border:1px solid;height:1px";
td.style.cssText = "height:9px;width:9px;padding:0";
col.span = 2;
documentElement$1
.appendChild( table )
.appendChild( col )
.parentNode
.appendChild( tr )
.appendChild( td )
.parentNode
.appendChild( td.cloneNode( true ) );
// Don't run until window is visible
if ( table.offsetWidth === 0 ) {
documentElement$1.removeChild( table );
return;
}
trStyle = window.getComputedStyle( tr );
// ... 计算 reliableColDimensionsVal / reliableTrDimensionsVal
documentElement$1.removeChild( table );
// Nullify the table so it wouldn't be stored in the memory;
// it will also be a sign that checks were already performed.
table = null;
}
这段代码有几个精妙之处:
(1)"一次布局,两个探测"。 注释说得很清楚:"Executing table tests requires only one layout, so they're executed at the same time to save the second computation."------创建一次 table,同时测出行和列的尺寸可靠性。因为插入 DOM 会触发布局(layout),而布局是昂贵的。
(2)position:absolute;left:-11111px。 把测试用的 table 放到屏幕外,避免影响用户可见的布局。这是一个标准技巧。
(3)if ( table.offsetWidth === 0 ) { ... return; }。 如果窗口不可见(比如页面在后台标签,或者 display:none 的 iframe 里),offsetWidth 会是 0,测量结果无意义。此时不设置探测结果 (保持 undefined),并把 table 移除,下次再试。这是"探测失败优雅降级"。
(4)table = null 作为"已执行"的标志。 用 !table 作为单例检查(行 5889),同时释放内存引用(注释:"Nullify the table so it wouldn't be stored in the memory")。一举两得。
(5)!table.style 的提前返回。 在非浏览器环境(如某些受限的 jsdom 配置),table.style 可能是 undefined,直接返回。
1.10.2 探测的具体内容
列宽探测:
js
reliableColDimensionsVal = isIE || Math.round( parseFloat(
window.getComputedStyle( col ).width )
) === 18;
构造:col.span = 2,td 宽 9px,border-spacing: 0。所以这个 col 覆盖 2 个 9px 的 td,期望宽度是 18px。
- 如果
getComputedStyle( col ).width四舍五入等于 18 → 浏览器正确上报列宽 →reliable = true; - Firefox 135+ 总是按
span=1上报 → 得到 9 →reliable = false; - Safari 18.3+ 恒为 0 →
reliable = false; - IE 直接判为 reliable(注释说明:IE 在没有显式 CSS width 时上报 "auto",测量本来就不准,但没有 workaround,所以"接受这个限制,把 IE 当作通过测试")。
行高探测:
js
reliableTrDimensionsVal = Math.round( parseFloat( trStyle.height ) +
parseFloat( trStyle.borderTopWidth ) +
parseFloat( trStyle.borderBottomWidth ) ) === tr.offsetHeight;
构造:tr 的 height: 1px、border: 1px solid、box-sizing: content-box,td 高 9px。
- content-box 下,
trStyle.height是内容高度,borderTopWidth+borderBottomWidth是边框; - 三者相加应该等于
tr.offsetHeight(border-box 高度); - Firefox 70+ 在 computed height 里已经包含了 border ,导致重复计算 → 不相等 →
reliable = false。
1.10.3 探测结果如何被使用
js
// 行 6097-6099
( !support.reliableColDimensions() && nodeName( elem, "col" ) ) ||
( !support.reliableTrDimensions() && nodeName( elem, "tr" ) )
在 getWidthOrHeight 里,如果元素是不可靠的 <col> 或 <tr>,就放弃 computed style,改用 offsetWidth / offsetHeight:
js
// 行 6110-6113
valueIsBorderBox = offsetProp in elem;
if ( valueIsBorderBox ) {
val = elem[ offsetProp ];
}
这是"探测 → 分流"的完整闭环。第 9 章会详细展开这个尺寸计算。
1.10.4 对插件作者的影响
如果你的插件依赖了 jQuery 3 时代的 jQuery.support.boxSizing 等属性,在 jQuery 4 里会拿到 undefined。正确做法是自己做探测:
js
// 不要这样
if ( jQuery.support.boxSizing ) { ... } // jQuery 4 里是 undefined
// 应该这样
var supportsBoxSizing = ( function() {
var div = document.createElement( "div" );
div.style.width = "10px";
div.style.padding = "2px";
div.style.boxSizing = "border-box";
document.documentElement.appendChild( div );
var result = div.offsetWidth === 10;
div.parentNode.removeChild( div );
return result;
} )();
或者更现代的方式:用 CSS.supports:
js
CSS.supports( "box-sizing", "border-box" ); // 所有现代浏览器都返回 true
1.11 兼容性策略:IE 11 的最后阵地
jQuery 4 保留了对 IE 11 的支持。这一节系统梳理源码里所有与兼容性相关的机制,让你明白"哪些代码是为了兼容、哪些是真正的算法"。
1.11.1 isIE 的唯一判据
js
// 行 531
var isIE = document$1.documentMode;
documentMode 是 IE 特有的属性:IE 8+ 有此属性,返回文档模式(8/9/10/11)。标准浏览器没有,返回 undefined。
为什么不用 UA 嗅探?
- UA 可被伪造;
- UA 解析逻辑复杂且易错;
documentMode直接反映 IE 的渲染模式,比 UA 更准确(例如 IE 11 在兼容模式下documentMode可能是 10)。
现代前端通常用特性探测(feature detection)而非浏览器探测。但 jQuery 的做法是:用 documentMode 确认"这是 IE",再用特性探测确认具体能力。这是务实的混合策略。
1.11.2 isIE 分支的完整清单
| 行号 | 场景 | IE 的具体问题 |
|---|---|---|
| 990 | attrHooks.type |
input 的 type 改为 radio 后,value 丢失 |
| 1436 | iframe unload | 访问已卸载 iframe 的文档抛 "Permission denied"(trac-13936) |
| 1828 | :selected 伪类 |
需访问 parentNode.selectedIndex 才能让浏览器正确处理 optgroup 默认选中 |
| 5307 | focusMappedHandler |
IE 的 focusin/focusout 是同步的,而其他浏览器 focus/blur 是异步的 |
| 5330 | focus/blur setup | IE 需显式绑定 delegateType(focusin/focusout) |
| 5517 | jQuery.clone |
IE 克隆 textarea 时 defaultValue 不正确 |
| 5949 | reliableColDimensions |
IE 的 <col> computed width 为 "auto",无 workaround,直接判为 reliable |
| 6095 | getWidthOrHeight |
IE 的 box-sizing 不可靠,用 offsetWidth/offsetHeight |
| 6188 | jQuery.style |
IE 克隆元素时 background-* 属性会影响源元素(trac-8908),需要设 "inherit" |
| 6828 | boxSizingNeeded |
IE 强制需要 boxSizing 探测 |
| 7228 | propHooks.selected |
IE 的 selected 需要强制访问 selectedIndex |
| 7564 | valHooks.option |
IE 的 option.text 会抛异常(trac-14686, trac-14858) |
这些分支的共同模式 :不是"IE 缺少某个 API",而是"IE 实现了 API 但行为不同"。这正是最难处理的兼容问题类型------if ( !foo ) 检测不出来,只能靠浏览器探测。
1.11.3 rbuggyQSA:querySelectorAll 的缺陷清单
js
// 行 531-546
var isIE = document$1.documentMode;
var rbuggyQSA = isIE && new RegExp(
// Support: IE 9 - 11+
// IE's :disabled selector does not pick up the children of disabled fieldsets
":enabled|:disabled|" +
// Support: IE 11+
// IE 11 doesn't find elements on a `[name='']` query in some cases.
// Adding a temporary attribute to the document before the selection works
// around the issue.
"\\[" + whitespace + "*name" + whitespace + "*=" +
whitespace + "*(?:''|\"\")"
);
rbuggyQSA 是"在 IE 里 querySelectorAll 会出错的选择器模式 "。它只在 IE 下构造(非 IE 时 rbuggyQSA 是 undefined),逻辑是:
js
// 行 1234-1235
if ( !nonnativeSelectorCache[ selector + " " ] &&
( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) {
// 可以安全使用 querySelectorAll
}
即:如果选择器匹配了已知缺陷模式,就退回 Sizzle 自己的引擎。
两个已知缺陷:
:enabled/:disabled:IE 的 qSA 不会选中 disabled fieldset 里的子元素。而 CSS 规范规定,fieldset 被 disabled 时,其所有可提交后代都被视为 disabled。[name='']:IE 11 在某些情况下找不到name=""的元素。这个 bug 的 workaround 很奇特------"Adding a temporary attribute to the document before the selection works around the issue"(在选择前给文档临时加个属性就好了)。这属于典型的"触发浏览器重新计算某些内部状态"的黑魔法。
这个机制的启示:当你想"借用原生 API 但担心它有 bug"时,jQuery 的模式是------
- 建立一份"已知缺陷模式"清单(正则);
- 每次调用前用清单检测;
- 命中则走自己的实现,未命中则走原生(性能最优)。
这比"全用原生"(有 bug)和"全用自己的"(慢)都好,是渐进增强在工程层面的落地。
1.11.4 nonnativeSelectorCache:运行时的缺陷累积
js
// 行 1286-1288
} catch ( qsaError ) {
nonnativeSelectorCache( selector, true );
}
除了"预先知道的缺陷"(rbuggyQSA),jQuery 还处理"运行时发现的缺陷 ":如果 querySelectorAll 抛异常(比如某些浏览器不支持 :has() 之类的新选择器),就把这个选择器记入 nonnativeSelectorCache,下次直接用内部引擎。
这是一个自适应缓存:第一次失败,之后不再尝试。它避免了每次都 try-catch(try-catch 在某些引擎里会阻止优化)。
1.11.5 兼容性代码的四种形态
纵观整份源码,兼容性处理可以归纳为四种形态:
形态 1:能力检测 + 分支
js
// 行 41-45
var flat = arr.flat ? function( array ) {
return arr.flat.call( array );
} : function( array ) {
return arr.concat.apply( [], array );
};
Array.prototype.flat 是 ES2019 的。老浏览器没有,用 concat.apply 模拟。在模块加载时决定用哪个实现,之后不再判断------这是"一次性分支",性能最优。
形态 2:浏览器探测 + 分支
js
// 行 990-1003
if ( isIE ) {
jQuery.attrHooks.type = {
set: function( elem, value ) { ... }
};
}
在加载时根据 isIE 决定是否安装 hook。这样非 IE 浏览器完全不会有这段代码的运行时开销。
形态 3:try-catch + 降级
js
// 行 1281-1292
try {
push.apply( results, newContext.querySelectorAll( newSelector ) );
return results;
} catch ( qsaError ) {
nonnativeSelectorCache( selector, true );
} finally {
if ( nid === jQuery.expando ) {
context.removeAttribute( "id" );
}
}
注意 finally 块:无论成功失败,都要清理临时加的 id 属性。这是资源清理的正确位置。
形态 4:惰性探测 + 缓存
js
// 行 5960-5970(support 的表格尺寸探测)
reliableTrDimensions: function() {
computeTableStyleTests();
return reliableTrDimensionsVal;
}
推迟到第一次使用,且只做一次。
1.12 安全模型:从 XSS 面到 Trusted Types
jQuery 历史上出过多次安全公告。4.0 在安全上做了实质增强。这一节分析 jQuery 的 XSS 攻击面,以及 4.0 的收敛措施。
1.12.1 jQuery 的 XSS 攻击面清单
| API | 是否危险 | 说明 |
|---|---|---|
.text( str ) |
安全 | 走 textContent(行 5610) |
.attr( name, str ) |
较安全 | 走 setAttribute,不会执行脚本(但 href="javascript:..." 仍是 XSS) |
.prop( name, str ) |
视属性而定 | innerHTML 属性就危险 |
.html( str ) |
危险 | 解析 HTML,会执行 <img onerror> 等 |
.append( str ) / .prepend / .before / .after / .replaceWith |
危险 | 同上,走 buildFragment |
.wrap( str ) / .wrapAll / .wrapInner |
危险 | 内部调用 domManip |
$( str ) |
危险 | 若 str 是 HTML 字符串,走 parseHTML |
$.parseHTML( str ) |
中等 | jQuery 4 默认用 DOMParser 惰性文档(见 1.5.2) |
$.globalEval( code ) |
危险 | 直接执行代码(行 336) |
$.getScript( url ) |
危险 | 加载并执行远程脚本 |
$.ajax( { dataType: "script" } ) |
危险 | 同上 |
| JSONP 请求 | 危险 | 服务端返回的代码会被执行 |
.css( prop, str ) |
较安全 | 但 css("background", "url(javascript:...)") 在特定浏览器可能有问题 |
$( location.hash ) |
历史危险 | jQuery 3 前会解析为 HTML,见下 |
1.12.2 历史漏洞一:$( location.hash )
在 jQuery 1.x,$( "#<img src=x onerror=alert(1)>" ) 会被当作 HTML 解析。攻击者可以构造:
xml
https://example.com/#<img src=x onerror=alert(1)>
如果页面里有 $( location.hash ),就会 XSS。
jQuery 的修复体现在行 2598-2602 的注释里:
js
// A simple way to check for HTML strings
// Prioritize #id over <tag> to avoid XSS via location.hash (trac-9521)
// Strict HTML recognition (trac-11290: must start with <)
// Shortcut simple #id case for speed
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
关键:
-
^锚定 +$锚定:整个字符串必须完全匹配,不能是"部分匹配"; -
<...>必须从头开始 :<img onerror=...>匹配,#<img...>也匹配(前面允许空白),但#foo <img...>不匹配; -
#id优先 :#([\w-]+)要求 id 只含单词字符和连字符,所以#<img...>不匹配 id 分支,会落到 HTML 分支------但这正是修复点:如果整串是#<img src=x onerror=alert(1)>,它匹配 HTML 分支吗?^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$:- 尝试 HTML 分支:
\s*匹配 0 个空白,<[\w\W]+>需要匹配<img src=x onerror=alert(1)>------但开头是#不是<,所以\s*后必须是<,失败; - 尝试 id 分支:
#([\w-]+)需要匹配#<img...>,[\w-]+不匹配<,失败; - 整个正则不匹配 →
match为 null → 走$( expr )选择器路径 → 传给jQuery.find→ 语法错误或找不到元素。
- 尝试 HTML 分支:
结果 :#<img src=x onerror=alert(1)> 在 jQuery 3+ 里既不当 HTML 也不当选择器,什么都不会发生。这就是 trac-9521 的修复。
1.12.3 历史漏洞二:htmlPrefilter 的正则绕过(CVE-2020-11022 / 11023)
htmlPrefilter 原本负责修正不规范的 HTML(如 <div/> → <div></div>)。它的实现用正则,而正则可以被构造的输入绕过,导致 <style> 标签内容被当作 HTML 处理,从而注入恶意内容。
jQuery 3.5 的修复是重写 htmlPrefilter,只允许特定的安全替换。而在 jQuery 4 里,它干脆变成空函数:
js
// 行 5507-5509
htmlPrefilter: function( html ) {
return html;
},
注意:保留函数、移除行为。这样插件仍可覆盖它做自定义过滤,但默认不做任何改写(把"修正 HTML"的责任交还给浏览器,浏览器现在都能正确处理)。
1.12.4 jQuery 4 的新防线:isObviousHtml 与 Trusted Types
js
// 行 2501-2505
function isObviousHtml( input ) {
return input[ 0 ] === "<" &&
input[ input.length - 1 ] === ">" &&
input.length >= 3;
}
Trusted Types 背景:
Trusted Types 是一项 Web 安全标准,用于根治 DOM XSS。启用方式:
css
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myPolicy;
启用后,所有"危险 sink"(innerHTML、outerHTML、document.write、eval、<script src> 的动态赋值等)只接受 TrustedHTML 对象,传字符串会抛异常。
jQuery 要在这个环境下工作,就必须能接受 TrustedHTML 包装对象。问题来了:
js
function isObviousHtml( input ) {
return input[ 0 ] === "<" && ... // TrustedHTML 不是字符串!
}
TrustedHTML 是一个不透明对象,不能用 [0] 索引,也不支持 String 方法。所以 jQuery 在调用处做了处理:
js
// 行 2630-2636
// Handle obvious HTML strings
match = selector + ""; // ← 转成字符串(TrustedHTML 的 toString 返回其内容)
if ( isObviousHtml( match ) ) {
// Assume that strings that start and end with <> are HTML and skip
// the regex check. This also handles browser-supported HTML wrappers
// like TrustedHTML.
match = [ null, selector, null ]; // ← 注意:存的是原始的 selector(TrustedHTML 对象),不是字符串
三个关键点:
selector + "":用字符串拼接触发toString(),得到 TrustedHTML 的内容用于检测;isObviousHtml( match ):检测这个内容是不是"明显的 HTML";match = [ null, selector, null ]:保留原始的selector(可能是 TrustedHTML 对象) ,后续传给jQuery.parseHTML(行 2655)。
这样 parseHTML 拿到的就是 TrustedHTML 对象,可以直接赋给 innerHTML 而不违反 CSP。
再看 parseHTML 的入口:
js
// 行 9190-9193
jQuery.parseHTML = function( data, context, keepScripts ) {
if ( typeof data !== "string" && !isObviousHtml( data + "" ) ) {
return [];
}
同样用 data + "" 来检测。而 buildFragment 里:
js
// 行 4425
tmp.innerHTML = jQuery.htmlPrefilter( elem );
如果 elem 是 TrustedHTML,htmlPrefilter 原样返回(它是恒等函数),赋值给 innerHTML 是合法的。
完整链路:
scss
$( trustedHTML )
↓ isObviousHtml( trustedHTML + "" ) → true
↓ match = [ null, trustedHTML, null ]
↓ jQuery.parseHTML( trustedHTML, doc, true )
↓ rsingleTag.exec( trustedHTML ) --- 正则会自动 toString
↓ buildFragment( [ trustedHTML ], ... )
↓ rtagName.exec( trustedHTML ) --- 同样自动 toString
↓ tmp.innerHTML = trustedHTML ← 赋值的是对象,CSP 允许
1.12.5 为什么需要"跳过正则检测"
注释说 "skip the regex check"。为什么必须跳过?
因为 rquickExpr(行 2602)是 RegExp,而 RegExp.prototype.exec 会先把参数转成字符串。对 TrustedHTML 转字符串是可行的 (toString() 返回内容),但:
- 语义上不对:TrustedHTML 已经被策略背书,不需要再用正则"猜"它是不是 HTML;
- 性能:正则匹配大字符串有开销;
- 正确性 :某些 TrustedHTML 实现可能不允许
toString()(虽然规范允许)。
所以 jQuery 选择:如果内容的首尾是 < 和 >,就直接认定是 HTML,不做正则。这是一个启发式 ------< 开头 > 结尾且长度 ≥ 3,几乎必然是 HTML。
为什么长度要 ≥ 3? 因为最短的 HTML 标签是 <a>,长度 3。长度 2 的 <> 不是合法标签。
1.12.6 给使用者的安全建议
基于以上分析,如果你在用 jQuery 处理用户输入:
js
// ❌ 危险:用户输入直接进 html()
$( "#container" ).html( userInput );
// ❌ 危险:用户输入进 $()(可能被当 HTML 解析)
$( userInput ).appendTo( "#container" );
// ❌ 危险:用户输入拼进选择器
$( "#" + userInput ).show(); // userInput 含特殊字符会导致选择器错误或注入
// ✅ 安全:用 text()
$( "#container" ).text( userInput );
// ✅ 安全:先创建元素再用 text() 填充
$( "<div>" ).text( userInput ).appendTo( "#container" );
// ✅ 安全:选择器用 escapeSelector
$( "#" + $.escapeSelector( userInput ) ).show();
// ✅ 安全:attr 值(但注意 javascript: 协议)
$( "a" ).attr( "href", userInput ); // 仍需校验协议白名单
// ✅ 推荐:使用 Trusted Types
const policy = trustedTypes.createPolicy( "myPolicy", {
createHTML: ( s ) => sanitize( s )
} );
$( "#container" ).html( policy.createHTML( userInput ) );
jQuery.escapeSelector(行 1025-1027) 是处理选择器注入的正确工具:
js
jQuery.escapeSelector = function( sel ) {
return ( sel + "" ).replace( rcssescape, fcssescape );
};
它按 CSS 规范转义特殊字符。用法:
js
// 用户输入: 'foo]'; alert(1); //
$( "#" + $.escapeSelector( userInput ) );
// → "#foo\\]\\;\\ alert\\(1\\)\\;\\ \\/\\/" 安全
1.13 源码阅读与调试方法论
最后这一节是"元技能":如何高效地读这份代码,以及在遇到问题时如何定位。
1.13.1 阅读路线建议
第一轮(1-2 天):建立地图。 只读本章的源码地图表,加上第 2、3 章。目标是能回答"某个 API 在哪个行号区间"。不要试图理解算法细节。
第二轮(3-5 天):打通主干。 读第 4-7 章(选择器 + 遍历 + DOM 操作)。这是 jQuery 最核心、最难的部分。建议配合断点调试。
第三轮(2-3 天):外围模块。 读第 8-11 章(属性、样式、事件)。这些模块相对独立,可以并行读。
第四轮(2-3 天):异步与网络。 读第 12-14 章(Deferred、动画、AJAX)。这部分与现代前端知识重叠最多,读起来会快。
第五轮(1-2 天):收尾。 读第 15-16 章(数据、安全、性能)。
1.13.2 断点调试的实操技巧
技巧 1:在 jQuery.fn.init 打断点,观察所有 $() 调用。
js
// 行 2604
init = jQuery.fn.init = function( selector, context ) {
// 在这里打断点
var match, elem;
这样任何 $( ... ) 都会停下来,你能看到 selector 的类型和走的分支。
技巧 2:在 access 打断点,理解所有 getter/setter。
js
// 行 841
function access( elems, fn, key, value, chainable, emptyGet, raw ) {
// 断点
var i = 0, len = elems.length, bulk = key == null;
attr / prop / css / val / text / html / width / height / data 全部经过这里。
技巧 3:在 jQuery.event.dispatch 打断点,看事件分发路径。
js
// 行 4840
dispatch: function( nativeEvent ) {
// 断点:看 handlers(行 4868)算出的 handlerQueue
handlerQueue 会告诉你哪些元素上的哪些处理器会被调用,以及顺序。
技巧 4:在 compile 打断点,观察选择器编译结果。
js
// 行 2339
function compile( selector, match ) {
var i, setMatchers = [], elementMatchers = [],
cached = compilerCache[ selector + " " ];
// 断点:看 cached(编译后的函数)
技巧 5:用 jQuery.find.tokenize 观察词法分析结果。
js
// 浏览器控制台
jQuery.find.tokenize( "div.foo > p:first-child" )
// 返回 token 数组,直观看到选择器被切成什么
1.13.3 用控制台实验验证理解
读完一段代码,最好的验证方式是在控制台里跑。
js
// 实验 1:验证 expando 的隔离性
jQuery.expando
// → "jQuery400123456789012345"
// 实验 2:验证选择器编译缓存
jQuery.find.tokenize( "div.foo" )
// → [ [ {value:"div", type:"TAG", matches:[...]}, {value:".foo", type:"CLASS", matches:[...]} ] ]
jQuery.find.tokenize( "div.foo" ) === jQuery.find.tokenize( "div.foo" )
// → false!因为 tokenize 返回的是 cached.slice(0),每次是新数组
// 实验 3:验证 pushStack 链
$( "body" ).find( "div" ).end().end().length
// → 观察 prevObject 链
// 实验 4:验证事件存储
$( document.body ).on( "click.test", function(){} );
// 然后在控制台查看:
// document.body[ jQuery.expando + "1" ] (dataPriv 的 expando 是 jQuery.expando + "1")
// → { events: { click: [...] }, handle: function }
// 实验 5:验证 Data 的 expando
// dataPriv.expando → "jQuery400...1"
// dataUser.expando → "jQuery400...2"
1.13.4 性能测量方法
要验证"某个 jQuery 操作的性能",正确做法是:
js
// ❌ 错误:只测一次,受 JIT 预热影响
console.time( "x" );
$( ".item" ).addClass( "active" );
console.timeEnd( "x" );
// ✅ 正确:预热 + 多次测量取中位数
function bench( fn, times ) {
for ( let i = 0; i < 100; i++ ) fn(); // 预热
const results = [];
for ( let i = 0; i < times; i++ ) {
const t = performance.now();
fn();
results.push( performance.now() - t );
}
results.sort( ( a, b ) => a - b );
return {
median: results[ Math.floor( times / 2 ) ],
min: results[ 0 ],
max: results[ times - 1 ]
};
}
bench( () => $( ".item" ).addClass( "active" ), 1000 );
注意强制同步布局(forced synchronous layout) :jQuery 的很多操作(.offset()、.width()、.css())会触发浏览器重新计算布局。如果你在循环里交替读写,会造成 "layout thrashing":
js
// ❌ 每次循环都触发布局
for ( let i = 0; i < items.length; i++ ) {
$( items[ i ] ).height( $( items[ i ] ).width() * 0.5 ); // 读 width → 布局,写 height → 布局
}
// ✅ 先全部读,再全部写
const widths = items.map( el => $( el ).width() ); // 批量读
for ( let i = 0; i < items.length; i++ ) {
$( items[ i ] ).height( widths[ i ] * 0.5 ); // 批量写
}
jQuery 内部也遵循这个原则------例如 showHide(行 4242-4288)用两个循环:第一个循环计算所有 display 值,第二个循环统一写入。注释明确说明了:
js
// 行 4280
// Set the display of the elements in a second loop to avoid constant reflow
1.13.5 一个"反常识"的阅读建议
不要试图读懂每一行。 jQuery 有些代码(尤其是 matcherFromTokens 的 setMatcher 递归、ajaxConvert 的转换器查找)是出了名的难懂。第一次读时,接受"我知道它在做什么,但不知道为什么每个细节都这样",然后继续往下。
第二遍回来时,用具体的输入去跑它 。例如理解 setMatcher,就在控制台跑:
js
jQuery.find.select( "p:first", document )
并单步跟踪。抽象的代码在有具体数据时才会变得具体。
1.14 全章总结
这一章我们做了五件事:
- 论证了阅读价值:存量代码、4.0 的实质重构、浏览器行为知识、可迁移的架构手法、完整可读的规模;
- 给出了源码地图 :9680 行的完整分区表,覆盖到
noConflict的每一个角落; - 解释了打包痕迹 :
$1后缀的由来、两个document的区分、模块顺序的约束; - 梳理了 Breaking Changes :移除清单、语义变更(特别是
attr(false)和parseHTML的 DOMParser 化)、内部重构、平台要求; - 建立了阅读方法论:五轮路线、五个断点位置、五个控制台实验、性能测量与反模式。
三个应该被记住的结论:
- jQuery 4 的删除清单,本质上是"浏览器已经赢了"的清单。删掉的都是标准已覆盖的,保留的都是实现仍有分歧的。
$1后缀 = 编译期宿主常量,无后缀 = 运行时可切换。这个区分贯穿选择器模块的每一处。- 表驱动差异收敛是 jQuery 最核心的架构手法,全书会遇到七次(attrHooks / propHooks / valHooks / cssHooks / event.special / prefilters-transports / expr.filter-pseudos)。
下一章我们进入核心地基 :toType 的类型判定体系、isArrayLike 的精妙定义、jQuery.extend 的深拷贝算法与原型污染防护,以及 each / map / grep / merge 这一组集合工具的实现细节与性能特征。这些是 jQuery 一切能力的起点。
附录 1-A · 第 1 章关键源码索引
| 行号 | 符号 | 类型 | 本章讨论点 |
|---|---|---|---|
| 11-31 | UMD 包装 | IIFE | 五种环境兼容、CommonJS 分支、noGlobal |
| 29-31 | if ( !window.document ) |
校验 | fail fast 设计 |
| 33-60 | arr / slice / push / indexOf / class2type / toString / hasOwn / fnToString / ObjectFunctionString |
变量 | 语言层常量(第 2 章详述) |
| 41-45 | flat |
函数 | 能力检测 + 一次性分支 |
| 65-73 | toType |
函数 | 类型判定(第 2 章) |
| 79-90 | isArrayLike |
函数 | 类数组判定(第 2 章) |
| 92 | document$1 |
变量 | 宿主文档常量 |
| 101-117 | DOMEval |
函数 | 脚本执行(第 7 章) |
| 124-129 | jQuery 工厂 |
函数 | new jQuery.fn.init() |
| 131-216 | jQuery.fn |
对象 | 原型骨架(第 3 章) |
| 160-170 | pushStack |
方法 | 链式栈(第 3 章) |
| 218-288 | jQuery.extend |
函数 | 深拷贝(第 2 章) |
| 254-258 | __proto__ 检查 |
代码 | CVE-2019-11358 修复 |
| 290-510 | jQuery.extend({...}) |
对象 | 静态工具(第 2 章) |
| 293 | expando |
属性 | "jQuery" + (version + Math.random()).replace(/\D/g,"") |
| 304-323 | isPlainObject |
方法 | 纯净对象判定(第 2 章) |
| 512-514 | Symbol.iterator |
代码 | jQuery 对象的可迭代性 |
| 531 | isIE = document$1.documentMode |
变量 | IE 探测 |
| 533-546 | rbuggyQSA |
正则 | qSA 缺陷清单 |
| 576-591 | createCache |
函数 | LRU 缓存(第 4 章) |
| 674-744 | tokenize |
函数 | 词法分析(第 4 章) |
| 841-898 | access |
函数 | 统一存取器(第 3 章) |
| 918-986 | jQuery.attr / attrHooks |
--- | jQuery 4 的 false 语义变更 |
| 1025-1027 | jQuery.escapeSelector |
方法 | 选择器转义 |
| 1036-1088 | sortOrder |
函数 | 文档序比较(第 6 章) |
| 1122-1172 | selector 模块变量块 | 声明 | 变量分组风格 |
| 1174-1299 | find |
函数 | 选择器主入口(第 5 章) |
| 1412-1440 | setDocument |
函数 | 文档切换 |
| 1463-1927 | jQuery.expr |
对象 | 选择器引擎定义(第 4、5 章) |
| 2339-2369 | compile |
函数 | 编译器(第 5 章) |
| 2459-2466 | jQuery.find 导出 |
代码 | Sizzle 退场声明 |
| 2501-2505 | isObviousHtml |
函数 | Trusted Types 支持 |
| 2604-2702 | jQuery.fn.init |
函数 | 对象构造(第 3 章) |
| 2705 | init.prototype = jQuery.fn |
代码 | 原型链结绳 |
| 2921-3113 | jQuery.Callbacks |
函数 | 回调列表(第 12 章) |
| 3154-3449 | jQuery.Deferred |
函数 | 异步(第 12 章) |
| 3508-3517 | jQuery.Deferred.exceptionHook |
方法 | 异常上报 |
| 3526-3573 | ready 机制 | 代码 | ready 的 Deferred 化 |
| 3625-3774 | Data |
类 | 存储抽象(第 15 章) |
| 4095-4105 | isHiddenWithinTree |
函数 | 可见性判定(jQuery 4 新增) |
| 5507-5509 | htmlPrefilter |
方法 | 已退化为恒等函数 |
| 5883-5970 | computeTableStyleTests |
函数 | 惰性特性探测 |
| 9632-9649 | AMD 注册 | 代码 | 具名 define |
| 9651-9669 | _jQuery / _$ / noConflict |
代码 | 多版本共存 |
| 9674-9676 | 全局挂载 | 代码 | window.jQuery = window.$ = jQuery |
附录 1-B · 本章涉及的浏览器缺陷编号
| 编号 | 主题 | 源码位置 |
|---|---|---|
| trac-9521 | $( location.hash ) 的 XSS |
行 2599 注释 |
| trac-11290 | 严格 HTML 识别(必须以 < 开头) |
行 2600 注释 |
| gh-2404 / gh-2863 | 隐藏祖先不应使子元素被判为 hidden | 行 4093-4094 |
| gh-2144 | Firefox 的 CSS 上界导致单位换算被截断 | 行 4162 |
| trac-8908 | IE 克隆元素的 background-* 影响源元素 | 行 6186 |
| trac-3861 | Firefox 非主键点击冒泡 click | 行 4918 |
| chromium 378607 | Chrome 删除 DOM 属性性能暴跌 | 行 5759-5761 |
| gh-4529 | Firefox 的 tr computed height 含 border | 行 5947 |
| trac-13936 | 访问已卸载 iframe 文档抛异常 | 行 1431 |
| trac-14686 / trac-14858 | IE 的 option.text 抛异常 |
行 7573 |
| trac-12072 | IE 的 tabIndex 未显式设置时不准 | 行 7193 |
| gh-4926 | Firefox 只 trim 自定义属性的前导空白 | 行 5826 |
下一章预告 :第 02 章进入核心地基,讲 jQuery 的"类型系统"与一整套工具函数。toType 与 class2type 映射表(行 65-73 / 516-520)、isWindow(行 75)、isArrayLike(行 79-90)四个条件的精确平衡、DOMEval(行 101)的脚本注入手法、jQuery.extend(行 218-288)的深拷贝算法与安全边界,以及 each / map / grep / merge / makeArray / inArray / isEmptyObject / isPlainObject(行 290-510)这批天天在用的静态方法。第 01 章提到的 expando(行 293)也会在这里第一次展开:它是"每份 jQuery 副本唯一"的钥匙,第 11 章的事件存储和第 20 章的 Data 系统都建立在它之上。