【发布时间】:2015-12-29 00:05:15
【问题描述】:
通过 jQuery 冒泡订阅存在性能问题。 IE9 和 IE11 表明 80% 的时间都花在了执行 querySelectorAll 上。 Analisis 展示了函数 $.event.dispatch(在 jQuery 1.8.1 中,在较新的版本 (1.11.3) 中,此功能已移至 $.event.handlers),其中包含以下代码:
for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
// Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764)
if ( cur.disabled !== true || event.type !== "click" ) {
selMatch = {};
matches = [];
for ( i = 0; i < delegateCount; i++ ) {
handleObj = handlers[ i ];
sel = handleObj.selector;
if ( selMatch[ sel ] === undefined ) {
selMatch[ sel ] = jQuery( sel, this ).index( cur ) >= 0;
}
if ( selMatch[ sel ] ) {
matches.push( handleObj );
}
}
if ( matches.length ) {
handlerQueue.push({ elem: cur, matches: matches });
}
}
}
注意以下几行:
// For each element from clicked and above
for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
// Clear the search cache
selMatch = {};
// For each subscriber
for ( i = 0; i < delegateCount; i++ ) {
// Take the subscriber's selector
sel = handleObj.selector;
// If it's out of cache
if ( selMatch[ sel ] === undefined ) {
// Search for elements matching to the selector
// And remember if current element is among of found ones
selMatch[ sel ] = jQuery( sel, this ).index( cur ) >= 0;
由于订阅是在正文上进行的,因此每次这样的搜索都会从整个文档中获取与选择器匹配的所有元素。并且重复次数与点击元素的深度一样多。
据我了解,外部循环是根据冒泡顺序保证处理程序的写入顺序。有一个缓存,但它只在一个级别上工作,并且在使用相同选择器的多个订阅的情况下有所帮助。
问题是为什么缓存是以这种方式实现的?为什么不保留 jQuery 集合并将index 移动到下一个if 条件?
但这还不是全部。我查看了 1.11.3 中的实际实现。它也使用了多次搜索,但是这行代码被改变了。
在 1.8.1 中是:
selMatch[ sel ] = jQuery( sel, this ).index( cur ) >= 0;
在 1.11.3 中它变成了:
matches[ sel ] = handleObj.needsContext ?
jQuery( sel, this ).index( cur ) >= 0 :
jQuery.find( sel, this, null, [ cur ] ).length;
在这种情况下进行相同的修改似乎并不合理。
所以,问题是:
- 什么原因会导致这个不是最佳的代码?
- 如何解决性能问题?
下面的sn-p说明了问题。
如果您打开浏览器控制台并单击Click me to get a lot of searches!。
您会看到以下行输出了 21 次:
qsa [id='sizcache041783330822363496'] section .smth
gbc smth-other
关于段[id='sizcache041783330822363496'] 有一个related question in Russian。很快,如果它是原始的,这种形式简化了 id 中特殊字符的转义。前段时间Sizzlehas updated this place,但即使是实际的jQuery版本也不包含它。
$(function () {
$("body")
.on("click", "section .smth", function () { console.log("clicked", "section .smth") })
.on("click", ".smth-other", function () { console.log("clicked", ".smth-other") });
$("h1").text("Click me to get a lot of searches!");
var qsa = Element.prototype.querySelectorAll, gbc = Element.prototype.getElementsByClassName;
Element.prototype.querySelectorAll = function(s) { console.log('qsa', s); return qsa.apply(this, arguments) };
Element.prototype.getElementsByClassName = function(s) { console.log('gbc', s); return gbc.apply(this, arguments) };
});
body { counter-reset: lev 1; }
div { counter-increment: lev; }
h1, h2 { cursor: pointer; }
h1:hover, h2:hover { background: silver; }
h1:after { content: " (" counter(lev) ")"; }
<div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><h1>
Loading...
</h1></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>
<section>
<h2 class="smth">I'm smth and i'm waiting for a click</h2>
</section>
<section>
<h2 class="smth-other">I'm smth other and i'm waiting for a click</h2>
</section>
<script src="//code.jquery.com/jquery-1.8.1.js"></script>
下一个sn-p展示了jQuery 1.8.1 dispatch函数的完整代码(sn-p是用来做可折叠的剧透,不是用来运行代码的):
dispatch: function( event ) {
// Make a writable jQuery.Event from the native event object
event = jQuery.event.fix( event || window.event );
var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related,
handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []),
delegateCount = handlers.delegateCount,
args = [].slice.call( arguments ),
run_all = !event.exclusive && !event.namespace,
special = jQuery.event.special[ event.type ] || {},
handlerQueue = [];
// Use the fix-ed jQuery.Event rather than the (read-only) native event
args[0] = event;
event.delegateTarget = this;
// Call the preDispatch hook for the mapped type, and let it bail if desired
if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
return;
}
// Determine handlers that should run if there are delegated events
// Avoid non-left-click bubbling in Firefox (#3861)
if ( delegateCount && !(event.button && event.type === "click") ) {
for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
// Don't process clicks (ONLY) on disabled elements (#6911, #8165, #11382, #11764)
if ( cur.disabled !== true || event.type !== "click" ) {
selMatch = {};
matches = [];
for ( i = 0; i < delegateCount; i++ ) {
handleObj = handlers[ i ];
sel = handleObj.selector;
if ( selMatch[ sel ] === undefined ) {
selMatch[ sel ] = jQuery( sel, this ).index( cur ) >= 0;
}
if ( selMatch[ sel ] ) {
matches.push( handleObj );
}
}
if ( matches.length ) {
handlerQueue.push({ elem: cur, matches: matches });
}
}
}
}
// Add the remaining (directly-bound) handlers
if ( handlers.length > delegateCount ) {
handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) });
}
// Run delegates first; they may want to stop propagation beneath us
for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) {
matched = handlerQueue[ i ];
event.currentTarget = matched.elem;
for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) {
handleObj = matched.matches[ j ];
// Triggered event must either 1) be non-exclusive and have no namespace, or
// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) {
event.data = handleObj.data;
event.handleObj = handleObj;
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
.apply( matched.elem, args );
if ( ret !== undefined ) {
event.result = ret;
if ( ret === false ) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
}
// Call the postDispatch hook for the mapped type
if ( special.postDispatch ) {
special.postDispatch.call( this, event );
}
return event.result;
},
【问题讨论】:
-
我没有完全明白你的问题。您是在尝试改进您的代码还是 jquery js 代码?
-
@pratikwebdev,其中任何一个。如您所见,我的代码中唯一需要优化的地方是订阅选择器。仅通过一个类替换复合选择器将导致调用
getElementsByClassName而不是querySelectorAll- 我希望这会有所改进,但仍会被多次调用。我的代码改进的另一种方法是订阅文档并自己检查event.target——但这似乎不是一个好的解决方案。所以我想知道其他优化方法,包括更改 jQuery 代码。 -
我明白你的意思。我之前为 Ie8、9 和 11 做了一些性能改进。我也遇到了类似的问题,而且与 querySelectorAll 相关。根据我的个人经验,它通常与开发人员代码相关联。如果您可以用您的代码更新您的问题,以及您试图用它实现什么,这可能有助于更好地理解问题。另外,我建议您在进行性能分析时确保时间考虑仅针对您尝试改进的代码,而不是针对整个页面或执行的多个操作。我假设你已经这样做了。
-
@pratikwebdev,我刚刚在深度 14 处主动单击了 2 个输入,并且 Ie9 无响应超过 80%(53 秒)的 mitute 配置为
querySelectorAll。分析这些调用,我发现每次点击我都会得到 14 次搜索[id='sizcache041783330822363496'] .some-class .some-other-class并且与点击的元素不匹配,所以没有从这些地方调用我的代码。该页面包含数千个元素。还执行了一些其他代码,但它甚至没有显示在顶部的配置文件行中。 -
想要的功能是什么?我的意思是你有多个带有 h1 元素的部分,里面有一些类名称。当用户单击搜索链接/h1 元素时,您想搜索具有特定类的所有匹配元素吗?通常,如果代码要根据类名进行搜索,并且在您的情况下,如果有很多元素与类名匹配,则需要时间。您可以做的一件事是,如果您知道包含这些部分元素的元素将您的事件绑定到最接近的元素而不是正文/文档。
标签: javascript jquery dom optimization