在使用jQuery向页面动态插入HTML时,很多人会发现一个有趣的现象:直接调用原生innerHTML插入的片段中script标签不会执行,而通过jQuery的html方法插入时脚本却能跑起来。这个差异的根源,就藏在jQuery内部的getAll函数以及它配合的脚本提取流程里。理解这段逻辑,不仅能帮你看懂jQuery源码的设计思路,也能在遇到动态脚本不执行的坑时快速定位问题。

getAll函数在jQuery源码中的定位与实现
getAll函数定义在jQuery的manipulation模块中,代码非常短,但作用关键。它的职责是在给定的上下文节点中,收集所有匹配标签名的元素。先看它的原始实现:
function getAll( context, tag ) {
// 支持HTML标签名作为context直接传入
var ret;
if ( typeof context.getElementsByTagName !== "undefined" ) {
// 通过getElementsByTagName收集所有指定标签
ret = context.getElementsByTagName( tag || "*" );
} else if ( typeof context.querySelectorAll !== "undefined" ) {
ret = context.querySelectorAll( tag || "*" );
} else {
ret = [];
}
if ( tag === undefined && context.nodeType === 1 ) {
// 确保顶层节点本身也被包含进来
ret = [].concat( ret );
if ( context.nodeName === tag.toUpperCase() ) {
ret.push( context );
}
}
return ret;
}这段代码的精妙之处在于它的兼容性设计。优先使用getElementsByTagName,这个方法在所有浏览器中都有最好的性能表现,返回的是动态NodeList。只有在context不支持该方法时才降级到querySelectorAll。当tag参数未传时默认匹配所有元素,也就是tag或星号,这为后续筛选脚本创造了条件。
getAll被调用的主要场景有两个。一是在buildFragment中查找克隆节点里的script标签,二是在domManip流程里对刚插入的DOM做一次全局扫描。看domManip中的一段关键代码:
// 如果传入的不是fragment,则收集脚本
if ( !fragment ) {
jQuery.merge( nodes, getAll( fragment ) );
}这里传给getAll的只有context没有tag,意味着会把fragment下的所有后代节点全部取出来,放进nodes数组。这个数组随后会被过滤出script元素,进入脚本执行分支。可以这么理解:getAll是jQuery对DOM树做地毯式搜索的基础工具,脚本提取只是它诸多用途中的一个。
从domManip到evalScript的完整执行链路
光有getAll收集节点还不够,真正决定脚本是否执行的是domManip中的一段判断逻辑。简化后的关键流程如下:
if ( node.src && ( node.type || "" ).toLowerCase() === "text/javascript" ) {
// 异步加载外部脚本,通过script标签注入
if ( jQuery._evalUrl ) {
jQuery._evalUrl( node.src );
}
} else {
// 内联脚本直接调用globalEval执行
DOMEval( false, node.textContent, node.ownerDocument );
}注意这里对脚本做了明确的二分处理。带src属性的外部脚本走jQuery._evalUrl,本质是创建一个新的script标签挂到head上,借助浏览器自己的加载机制异步执行;而没有src的内联脚本则交给DOMEval(旧版本叫globalEval)直接在全局作用域中eval执行。
DOMEval的实现同样值得一看,它并没有直接调用eval,而是创建一个临时script节点:
function DOMEval( code, doc, node ) {
doc = doc || document;
var i, script = doc.createElement( "script" );
script.text = code;
if ( node ) {
for ( i in node ) {
if ( node[ i ] ) {
script.setAttribute( i, node[ i ] );
}
}
doc.head.appendChild( script ).parentNode.removeChild( script );
}之所以绕这么一圈而不直接用eval,是因为eval的作用域问题。直接eval会让脚本运行在当前闭包内,里面用var声明的变量无法成为全局变量。而通过appendChild一个script节点执行,代码天然运行在全局作用域,var声明的变量会挂到window上,这更符合用户对页面脚本的预期。执行完立刻removeChild,避免残留DOM节点。
keepScripts参数与data脚本的特殊处理
脚本的提取和执行并不是无条件的。jQuery提供了两个开关来控制这一行为。第一个是loadScripts判断,在domManip中能看到类似这样的代码:
var loadScripts = function( node ) {
if ( jQuery.contains( doc, node ) ) {
// 节点已在文档中,准备执行
}
};第二个则是更外层的keepScripts参数,出现在jQuery.htmlPrefilter和append等方法的调用链上。jQuery.trim对传入HTML做预处理时,如果发现是纯文本则直接返回,不进入脚本处理流程。而对于type属性被设置为text/x-template或带有data开头自定义类型的脚本,jQuery会刻意跳过。看这段过滤逻辑:
// 跳过模板脚本和data类型脚本
if ( node.type && node.type.indexOf( "text/javascript" ) === -1 ) {
return;
}这个设计的意图很实用。现代前端常用<script type="text/x-template">来存放模板字符串,如果这类脚本被当作可执行代码eval一遍,模板内容里的特殊字符会直接抛语法错误。所以jQuery只认type为空或text/javascript的脚本,其他一律放行不动。
常见坑与最佳实践建议
理解了源码逻辑,再来看实际开发中几个高频问题就容易解释了。第一,为什么innerHTML插入的脚本不执行?这是W3C规范层面的限制,浏览器出于安全考虑明确规定了通过innerHTML解析出的script节点不会被执行。jQuery能执行脚本,靠的正是getAll重新提取节点加手动eval这套自己的流程,而不是浏览器行为。
第二,脚本执行顺序问题。evalScript对内联脚本是同步执行的,顺序有保障;但外部脚本通过_evalUrl加载是异步的,无法保证与后续内联脚本的先后关系。如果内联脚本依赖某个外部脚本里的全局变量,很可能会报undefined错误。稳妥的做法是避免在动态插入的片段里混排依赖关系复杂的脚本,或者在插入前手动剥离script再按顺序处理。
第三,重复执行问题。同一个fragment被多次append到不同位置时,脚本每次都会被执行一遍,因为jQuery的执行逻辑不会给节点打已执行的标记。这在需要复用HTML片段的场景下要格外小心。总的来说,getAll本身只是一个朴素的节点收集器,真正的设计精髓在于它与domManip、DOMEval组成的这条链路:收集、分类、按类型分发执行。读懂这条链路,也就读懂了jQuery处理动态HTML时在便利性与安全性之间做出的取舍。
jQuery getAll函数内联脚本执行DOM操作修改时间:2026-09-16 19:24:44