jQuery 4.0 源码架构与工程实践-第 01 章 · 全景与工程结构:一个 20 岁库的自省

jQuery 4.0 源码架构与工程实践-第 01 章 · 全景与工程结构:一个 20 岁库的自省

本章导览

任何一份源码阅读笔记,都应该从"这份文件是什么"开始。本章不写任何 API 用法,只做一件事:把 jquery-4.0.0.js 这份 9680 行的文件,从物理结构、构建来源、模块拓扑、版本演进、工程规范五个维度彻底剖开 ,让你在翻开第 4 章那个最难的 matcherFromTokens 之前,已经知道它在整个系统里处于什么位置、为什么必须是那个样子。

本章的具体产出是三张图和三张表:

  • 源码地图表:行号区间 → 模块 → 职责,覆盖全部 9680 行,不留死角;
  • 模块依赖拓扑图 :解释为什么 core 必须在最前面、为什么选择器引擎必须早于遍历模块、为什么 offset 会出现在文件倒数第二段;
  • Breaking Changes 全景表:jQuery 4.0 删掉了什么、改了什么、替代方案是什么。

读完之后,你应该能回答这几个问题:

  1. 为什么 jQuery.fn.init.prototype = jQuery.fn 这行代码是 jQuery 对象模型的"结绳",而没有它整个链式调用会崩塌?
  2. 为什么源码里到处是 document$1、rquickExpr$1、documentElement$1 这种带 $1 的变量名?
  3. 为什么 jQuery( "<div>" ) 在 jQuery 4 里会走 isObviousHtml 的快路径,而 jQuery 3 不会?
  4. 为什么 jQuery 4 删掉了 $.isArray、$.type、$.trim 这些"人畜无害"的小函数?
  5. 为什么 ready 在 2026 年还需要存在?

1.1 为什么在 2026 年还要精读 jQuery 4.0

先把话说清楚:本系列不推销 jQuery。如果你的项目是全新的、跑在现代浏览器上、团队熟悉组件化开发,那么你不应该用 jQuery 启动一个新项目。这个结论没有争议。

但是,"不用 jQuery 写新项目"和"不需要理解 jQuery"是两件完全不同的事。以下五个理由,每一个都足够支撑一次系统性的源码阅读。

1.1.1 存量代码的规模决定了它不会消失

我们不需要精确的统计数据也能确认这一点:jQuery 曾长期占据全球网站 70% 以上的使用率。即便在 2026 年,大量仍在产生商业价值的系统------银行核心柜面、政务申报平台、医院信息系统、制造业 MES、航空订座前端、保险理赔后台------其核心交互层仍然构建在 jQuery 之上。

这些系统有三个共同特征:

  1. 重写成本极高:一个 200 万行的 jQuery 后台,重写预算可能高达数千万元,而它的业务价值并不因为重写而提升;
  2. 正确性要求极高:它们往往是 To B 或 To G 系统,一次 DOM 操作错误可能意味着一笔错误的账务;
  3. 维护周期极长:很多系统的设计寿命是 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
 */

几个值得注意的信息:

  1. /*! 而不是 /* :这是给压缩器(UglifyJS / Terser)的信号------"这个注释块必须保留"。所有许可证头都遵循这个约定。如果你在自己的库里写了 /* Copyright ... */,压缩后许可证信息会丢失,这在 MIT/BSD 合规上是风险。
  2. 版权主体是 "OpenJS Foundation and other contributors":jQuery 于 2019 年将知识产权归属转移到 OpenJS Foundation(与 Node.js 同属)。这不是形式主义------它意味着 jQuery 的商标与代码由中立基金会持有,避免了单一公司控制的风险。
  3. MIT 许可:最宽松的开源许可之一。意味着你可以自由使用、修改、闭源分发,只需保留版权声明。
  4. 构建日期 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 的破坏性变更可以分成四类:

  1. 彻底移除 (Removed):API 不存在了,调用会 TypeError;
  2. 语义变更(Changed):API 还在,但行为变了;
  3. 内部重构(Internal):私有 API 变化,影响依赖内部实现的插件;
  4. 平台要求变更(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 这种无害的小函数都要删? 这不是洁癖,而是三个真实原因:

  1. 体积:每保留一个这样的函数,就多一份 API 文档、多一份测试、多一份"用户以为 jQuery 提供了字符串处理能力"的误导;
  2. 语义漂移风险 :jQuery.trim 在历史上曾经用正则实现(str.replace( /^\s+|\s+$/g, "" )),而原生 trim() 遵循 Unicode 空白定义。保留它意味着 jQuery 要持续跟进标准;
  3. 教育意义:删掉它们,等于向社区明确传达"这些能力浏览器已经有了,不需要库"。

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 年初发布)。核心目标:

  1. 移除所有被标准覆盖的 API(见 1.5.1);
  2. 彻底内部化 Sizzle(不再作为可独立使用的库);
  3. 安全增强 :Trusted Types、DOMParser 惰性解析、XSS 面收敛;
  4. Deferred 完全对齐 Promises/A+;
  5. 支持现代构建:ESM 输出、更好的 tree-shaking;
  6. 修复长期存在的语义 bug :attr(false)、:hidden 的级联判定等。

从源码的 Date: 2026-01-18T00:20Z 看,这份构建产物属于 4.0.x 的一个后期补丁版本。

1.6.5 演进规律总结

回顾这条 20 年的演进线,可以提炼出三条规律:

  1. 每一次"删除"都伴随着"标准已经提供" 。jQuery 的历史就是"浏览器追赶 jQuery"的历史:querySelectorAll 取代了 Sizzle 的大部分功能、fetch 与 Promise 取代了 $.ajax 的一部分、classList 取代了 addClass/removeClass 的一部分、Array.isArray 取代了 $.isArray。jQuery 4 的删除清单,本质上是一份"浏览器已经赢了"的清单。

  2. 每一次"新增"都指向"浏览器仍然不一致的地方" 。jQuery 4 保留并强化的能力------委托事件的边界判定、盒模型的跨浏览器测量、表格尺寸探测、:disabled 的 fieldset 继承------都是标准有定义但实现有分歧的领域。

  3. 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

自定义构建的注意事项:

  1. 模块间有隐式依赖 ,随便排除会导致运行时错误。例如排除 css 会让 effects 失效,排除 data 会让 event 失效(事件存储在 dataPriv 里);
  2. 排除 deprecated 会移除 bind / unbind / delegate / undelegate / hover / push 等兼容方法,如果你的老代码或第三方插件用了它们,会报错;
  3. 构建产物不会包含 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 复制一份,直接在里面加中文注释。

具体做法:

  1. 复制为 jquery-4.0.0.annotated.js(不要改动原文件);

  2. 每读透一个模块,就在里面加注释块,格式统一为:

    js 复制代码
    /* @annotate 2026-01-21
     * 模块:selector / 函数:matcherFromTokens
     * 作用:...
     * 关键:...
     * 陷阱:...
     */
  3. 配合编辑器的折叠功能,逐步积累。

这样做的好处是:注释与代码同处一个文件,调试时断点打到哪,注释就在旁边。比起另写笔记,效率高得多。


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 的"兼容性知识库"。它们有四个价值:

  1. 防止误删:后人看到这段奇怪代码,知道它是有原因的;
  2. 便于清理 :当某个浏览器版本退出支持列表时,可以直接搜索 Support: IE 9 批量删除;
  3. 问题定位:当你遇到诡异 bug 时,搜索相关关键词能快速找到历史原因;
  4. 知识传承:新贡献者能理解"为什么这里要这么写"。

统计一下这份文件里的 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()?

  1. 可读性 :看到 jQuery400... 能立刻知道是 jQuery 4.0 的实例;
  2. 调试友好 :在 DevTools 里查看元素属性时,jQuery400123... 比 jQuery0123... 信息量更大;
  3. 降低碰撞概率 :虽然 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,就能构造恶意数据。但在实践中这不是威胁,因为:

  1. expando 只是属性名,不承载权限;
  2. 攻击者在页面里执行 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,
	...
};

这些值在库加载时就通过计算得出。问题:

  1. 启动开销:每次加载 jQuery 都要执行十几次 DOM 探测(创建元素、插入、测量、移除),这会拖慢页面启动;
  2. 准确性问题:某些探测在特定条件下会误判(如页面尚未渲染时测量不准);
  3. 维护负担:浏览器修了 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 嗅探?

  1. UA 可被伪造;
  2. UA 解析逻辑复杂且易错;
  3. 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 自己的引擎。

两个已知缺陷:

  1. :enabled / :disabled:IE 的 qSA 不会选中 disabled fieldset 里的子元素。而 CSS 规范规定,fieldset 被 disabled 时,其所有可提交后代都被视为 disabled。
  2. [name=''] :IE 11 在某些情况下找不到 name="" 的元素。这个 bug 的 workaround 很奇特------"Adding a temporary attribute to the document before the selection works around the issue"(在选择前给文档临时加个属性就好了)。这属于典型的"触发浏览器重新计算某些内部状态"的黑魔法。

这个机制的启示:当你想"借用原生 API 但担心它有 bug"时,jQuery 的模式是------

  1. 建立一份"已知缺陷模式"清单(正则);
  2. 每次调用前用清单检测;
  3. 命中则走自己的实现,未命中则走原生(性能最优)。

这比"全用原生"(有 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-]+))$/,

关键:

  1. ^ 锚定 + $ 锚定:整个字符串必须完全匹配,不能是"部分匹配";

  2. <...> 必须从头开始 :<img onerror=...> 匹配,#<img...> 也匹配(前面允许空白),但 #foo <img...> 不匹配;

  3. #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 → 语法错误或找不到元素。

结果 :#<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 对象),不是字符串

三个关键点:

  1. selector + "":用字符串拼接触发 toString(),得到 TrustedHTML 的内容用于检测;
  2. isObviousHtml( match ):检测这个内容是不是"明显的 HTML";
  3. 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() 返回内容),但:

  1. 语义上不对:TrustedHTML 已经被策略背书,不需要再用正则"猜"它是不是 HTML;
  2. 性能:正则匹配大字符串有开销;
  3. 正确性 :某些 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 全章总结

这一章我们做了五件事:

  1. 论证了阅读价值:存量代码、4.0 的实质重构、浏览器行为知识、可迁移的架构手法、完整可读的规模;
  2. 给出了源码地图 :9680 行的完整分区表,覆盖到 noConflict 的每一个角落;
  3. 解释了打包痕迹 :$1 后缀的由来、两个 document 的区分、模块顺序的约束;
  4. 梳理了 Breaking Changes :移除清单、语义变更(特别是 attr(false) 和 parseHTML 的 DOMParser 化)、内部重构、平台要求;
  5. 建立了阅读方法论:五轮路线、五个断点位置、五个控制台实验、性能测量与反模式。

三个应该被记住的结论:

  • 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 系统都建立在它之上。

相关推荐
徐小夕1 小时前
doc2docx方案,开源!
前端·vue.js·github
徐小夕2 小时前
JitOFD:一款面向信创与档案场景的 Web 端 OFD 编辑器
前端·vue.js·github
孟健2 小时前
Kimi K3.1 Pro 编程实测:3个月27万行代码,每年1.7万预算下如何9分钟修好顽固Bug
前端·开源·ai编程
inksci2 小时前
在线编辑表格,功能强大,json互转
前端·javascript·html
咩咩啃树皮3 小时前
JavaScript 中 this 指向为什么总在变?
开发语言·前端·javascript
彭亚川Allen3 小时前
从零手搓 RAG 之 Embedding 技术
前端·后端·面试
OpenTiny社区3 小时前
GenUI SDK v1.4.0 的 React 渲染器到底做对了什么?
前端·github·ai编程
咩咩啃树皮3 小时前
JavaScript 异步流程为什么容易失控?从回调到可维护设计
开发语言·javascript·ecmascript
jerrywus4 小时前
chrome-devtools-mcp 总弹授权、抢标签页?我是这样一步步配好的
前端·agent·mcp