如何使用覆盖层高效地突出显示鼠标光标下的元素?

12

我仍在努力回答这个问题,我认为我终于找到了一个解决方案,但它运行得太慢了。

var $div = $('<div>')
    .css({ 'border': '1px solid red', 'position': 'absolute', 'z-index': '65535' })
    .appendTo('body');

$('body *').live('mousemove', function(e) {
    var topElement = null;
    $('body *').each(function() {
        if(this == $div[0]) return true;
        var $elem = $(this);
        var pos = $elem.offset();
        var width = $elem.width();
        var height = $elem.height();
        if(e.pageX > pos.left && e.pageY > pos.top
            && e.pageX < (pos.left + width) && e.pageY < (pos.top + height)) {
            var zIndex = document.defaultView.getComputedStyle(this, null).getPropertyValue('z-index');
            if(zIndex == 'auto') zIndex = $elem.parents().length;
            if(topElement == null || zIndex > topElement.zIndex) {
                topElement = {
                    'node': $elem,
                    'zIndex': zIndex
                };
            }

        }
    });
    if(topElement != null ) {
        var $elem = topElement.node;
        $div.offset($elem.offset()).width($elem.width()).height($elem.height());
    }
});

这段代码基本上循环遍历页面上的所有元素,并查找鼠标下面最靠近顶部的元素。

也许有什么方法可以使用四叉树之类的东西来对页面进行分割,以便循环运行得更快吗?


2
дҪ дёҚиғҪзӣҙжҺҘдҪҝз”Ёe.currentTargetиҖҢдёҚз”ЁеҫӘзҺҜжҹҘжүҫе…ғзҙ еҗ—пјҹ - Guffa
@Guffa:是的,currentTarget将始终是$div,因为我正在将其移动到要突出显示的元素上方,但然后它会夺取所有鼠标事件。 - mpen
1
最大的瓶颈不在于循环部分,而在于计算偏移和尺寸。把你的注意力放在那里。一个小的优化是,如果偏移量大于当前鼠标位置,则不必计算宽度和高度。 - David Tang
@meo: 呃...那个页面上没有太多的描述,但看起来完全不相关。 - mpen
可能是重复的问题:jQuery:高亮鼠标光标下的元素? - kapa
显示剩余4条评论
2个回答

18

也许你可以使用四叉树之类的方式,将页面分割以便加快循环速度?

退一步,认识到问题非常小,努力越大,使用的答案就会越复杂。

现在你需要做的是为高亮创建4个元素。它们将形成一个空方块,因此鼠标事件可以自由触发。这类似于我制作的overlay example

不同之处在于,你只需要四个元素(没有调整大小标记),而且4个框的大小和位置略有不同(模仿红色边框)。然后你可以在事件处理程序中使用event.target,因为默认情况下它获取真正的最顶层元素。

另一种方法是隐藏额外元素,获取elementFromPoint,计算然后放回去。

它们比光还快,我可以告诉你。甚至爱因斯坦也会同意的 :)

1.) elementFromPoint overlay/borders - [Demo1] FF 需要 v3.0+

var box = $("<div class='outer' />").css({
  display: "none", position: "absolute", 
  zIndex: 65000, background:"rgba(255, 0, 0, .3)"
}).appendTo("body");

var mouseX, mouseY, target, lastTarget;

// in case you need to support older browsers use a requestAnimationFrame polyfill
// e.g: https://gist.github.com/paulirish/1579671
window.requestAnimationFrame(function frame() {
  window.requestAnimationFrame(frame);
    if (target && target.className === "outer") {
        box.hide();
        target = document.elementFromPoint(mouseX, mouseY);
    }
    box.show();   

    if (target === lastTarget) return;

    lastTarget = target;
    var $target = $(target);
    var offset = $target.offset();
    box.css({
        width:  $target.outerWidth()  - 1, 
        height: $target.outerHeight() - 1, 
        left:   offset.left, 
        top:    offset.top 
    });
});

$("body").mousemove(function (e) {
    mouseX = e.clientX;
    mouseY = e.clientY;
    target = e.target;
});

2.) mouseover边框 - [演示2]

var box = new Overlay();

$("body").mouseover(function(e){
  var el = $(e.target);
  var offset = el.offset();
  box.render(el.outerWidth(), el.outerHeight(), offset.left, offset.top);
});​

/**
 * This object encapsulates the elements and actions of the overlay.
 */
function Overlay(width, height, left, top) {

    this.width = this.height = this.left = this.top = 0;

    // outer parent
    var outer = $("<div class='outer' />").appendTo("body");

    // red lines (boxes)
    var topbox    = $("<div />").css("height", 1).appendTo(outer);
    var bottombox = $("<div />").css("height", 1).appendTo(outer);  
    var leftbox   = $("<div />").css("width",  1).appendTo(outer);
    var rightbox  = $("<div />").css("width",  1).appendTo(outer);

    // don't count it as a real element
    outer.mouseover(function(){ 
        outer.hide(); 
    });    

    /**
     * Public interface
     */

    this.resize = function resize(width, height, left, top) {
      if (width != null)
        this.width = width;
      if (height != null)
        this.height = height;
      if (left != null)
        this.left = left;
      if (top != null)
        this.top = top;      
    };

    this.show = function show() {
       outer.show();
    };

    this.hide = function hide() {
       outer.hide();
    };     

    this.render = function render(width, height, left, top) {

        this.resize(width, height, left, top);

        topbox.css({
          top:   this.top,
          left:  this.left,
          width: this.width
        });
        bottombox.css({
          top:   this.top + this.height - 1,
          left:  this.left,
          width: this.width
        });
        leftbox.css({
          top:    this.top, 
          left:   this.left, 
          height: this.height
        });
        rightbox.css({
          top:    this.top, 
          left:   this.left + this.width - 1, 
          height: this.height  
        });

        this.show();
    };      

    // initial rendering [optional]
    // this.render(width, height, left, top);
}

不知道有一个 elementFromPoint 函数... 这使得事情变得更容易了。看起来这个函数工作得很好,谢谢 :) - mpen
1
那么new前面的加号是做什么的呢?我以前从未见过这种用法。 - mpen
2
@Mark,我也很好奇:在'return +new date'中加号的作用是什么? - John Leehey
哈哈,很抱歉让你等了这么久。你可以在我关于JS常见速记符号的另一个答案中找到答案:https://dev59.com/e2865IYBdhLWcg3wR8ah#3899587 - gblazex
1
一般来说,前缀运算符 + 可以将其转换为数字。检查 alert(typeof "52")alert(typeof +"52") - gblazex
显示剩余5条评论

1
首先,我认为执行$('body *').live不是一个很好的想法,它似乎非常昂贵(考虑一下每次移动鼠标时浏览器必须进行的计算类型)。
话虽如此,这里有一个优化版本,忽略了这个方面。
var $div = $('<div>')
    .css({ 'border': '1px solid red', 'position': 'absolute', 'z-index': '65535' })
    .appendTo('body');

$('body *').live('mousemove', function(e) {
    var topElement = null;
    var $bodyStar = $('body *');
    for(var i=0,elem;elem=$bodyStar[i];i++) {
        if(elem == $div[0]) continue;
        var $elem = $(elem);
        var pos = $elem.offset();
        var width = $elem.width();
        var height = $elem.height();
        if(e.pageX > pos.left && e.pageY > pos.top && e.pageX < (pos.left + width) && e.pageY < (pos.top + height)) {
            var zIndex = document.defaultView.getComputedStyle(this, null).getPropertyValue('z-index');
            if(zIndex == 'auto') zIndex = $elem.parents().length;
            if(topElement == null || zIndex > topElement.zIndex) {
                topElement = {
                    'node': $elem,
                    'zIndex': zIndex
                };
            }

        }
    }
    if(topElement != null) {
        var $elem = topElement.node;
        $div.offset($elem.offset()).width($elem.width()).height($elem.height());
    }
});

以后参考,如果您需要性能,永远不要使用jQuery的循环机制。它们都是基于每次迭代的函数调用构建的,与普通循环相比非常慢,因为当您进行函数调用时发生的调用堆栈初始化对于大多数迭代操作来说是巨大的开销。

代码已更新以修复错误,并允许动态插入元素。


首先,最好使用 addClass() 而不是 .css()。其次,我可以问一下你的 for 循环什么时候结束循环吗? - Reigel Gallarde
2
当 $bodystar[i] 返回 undefined 时它会结束,而 i 到达数组的长度时也会返回 undefined。这是循环遍历一个不包含会被解释为 false 的值的数组中最有效的方法。 - Martin Jespersen
1
此外,我不认为像那样提取 bodyStar 会有所帮助。循环条件 可能.each 更好(如果正确实现),但是...最终我们仍然在页面上循环遍历每个元素,我认为这是致命的。也许缓存一些计算属性会有所帮助.... - mpen
1
循环条件是一个巨大的改进,每次运行一些测试,你就会发现 :)。each/filter/map等可能是jQuery中最慢的单个方面。在这个问题上进行一些测试,你就会发现,我在IE7和更高版本的浏览器中做了大量的性能测试,包括Chrome、Safari、Opera和Firefox,使用常规循环和基于函数的循环之间的差异非常大...但是嘿,你总是可以忽略这个建议,毕竟它是免费的 :)。 - Martin Jespersen
@galambalaz:你可能是对的,我从来没有想过我的解决方案会使整个实现工作。当我阅读问题时,它是关于优化循环的,所以我这样做了,无论它是否足够高效始终存在疑问-在我看来,全页面委托mousemove永远不会起作用,这就是我在回答的第一句话中试图表达的意思。 - Martin Jespersen
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接