addEventListener和`this`表现不如预期

4
我是一名经验丰富的程序员,但对于Web编程还比较新手。我正在尝试通过编写一个使用JavaScript玩井字游戏的HTML页面来学习JavaScript、HTML5和SVG。在VS2010中,我成功地将每个九宫格创建为SVG的<rect...>元素,但是我在为每个九宫格创建点击事件处理程序方面遇到了问题。
以下是基本的SVG元素,它们存在于HTML文件中:
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
    id="svgTTT" width="150" height="150" viewBox="0 0 300 300"  >
    <rect width="3" height="300" x="99" fill="#008d46" />
    <rect width="3" height="300" x="199" fill="#000000" />
    <rect width="300" height="3" y="99" fill="#008d46" />
    <rect width="300" height="3" y="199" fill="#d2232c" />
  </svg>

这些静态的<rect>元素绘制出TicTacToe棋盘的交叉线。九个棋盘格子是在一个名为从窗口加载事件调用的javascript函数中创建的(下面是代码)。

以下是嵌入在HTML正文中<script>元素内的javascript代码:

<script type="text/javascript">
  function getEl(s) { return document.getElementById(s); }

  var svg;  // get the TTT svg

  // execute after HTML has rendered
  window.onload = function () {
    svg = getEl("svgTTT");
    tttSetupSquares(svg);
    alert("click-squares are setup.");
  }

  var cells = new Array(3);
  // setup the click squares
  function tttSetupSquares(brd) {
    for (var i = 0; i < 3; i++) {
      cells[i] = new Array(3);
      for (var j = 0; j < 3; j++) {
        var x = 3 + (i * 100);
        var y = 3 + (j * 100);
        var newId = "svgTTTsqr" + i.toString() + j.toString();
        // add in new rect with html
        brd.innerHTML += "<rect id='"+newId+"' width='93' height='93' "
                        + "fill='#00DD00' x='" + x.toString() + "' y ='" + y.toString() + "'"
                        + " />";
        //find it using the newId
        var rect = document.getElementById(newId);
        //make a cell object to contain all of this
        var cell = {
          col: i,
          row: j,
          pSvg: svg,
          rect: rect,

          handleClick: function (event) {
            try {
              // this line fails because `this` is the target, not the cell
              var svgNS = this.pSvg.namespaceURI;

              var line = document.createElementNS(svgNS, 'line');
              this.pSvg.appendChild(line);
            }
            catch (err) {
              alert("handlClick err: " + err);
            }
          }
        }

        //add a click handler for the rect/cell
        cell.rect.addEventListener("click", cell.handleClick, false);
        //(only seems to work the last time it is executed)

        //save the cell object for later use
        cells[i][j] = cell;
      }
    }
  }
</script>

问题有两个方面:

  1. 只有最后一个addEventListener似乎起作用。点击其他所有方格都没有反应。点击最后一个方格(svgTTTsqr22)确实运行了cell.handleClick,但会遇到问题2(下文)。 Chrome开发者工具(F12)显示除最后一个以外的所有<rect>元素都没有事件侦听器。

  2. cell.handleClick 运行时,它在第一行失败(var svgNS = this.pSvg.namespaceURI;),出现类似"未定义的对象没有名为"namespaceURI"的属性"的错误。 在Devleoper工具中检查发现,它失败是因为this没有设置为cell对象,而是被点击的SVG<rect>元素所代替。

所以我的问题是:

A. 我在这里做错了什么,以及

B. 我应该如何做?

3个回答

4

1. 丢失的事件处理程序

使用innerHTML来更改元素的内部结构将导致所有子元素被删除,并且元素的DOM子树将通过重新解析HTML内容重新构建。通过删除子元素,所有先前注册的事件监听器将会丢失,并且在从HTML重建DOM时不会自动恢复。为了规避这种行为,最好尽可能避免使用innerHTML,而是使用直接的DOM操作。您可以使用以下内容来插入您的<rect>

// Use DOM manipulation instead of innerHTML
var rect = document.createElementNS(svg.namespaceURI, 'rect');
rect.setAttributeNS(null, "id", newId);
rect.setAttributeNS(null, "fill", "#00DD00");
rect.setAttributeNS(null, "width", "93");
rect.setAttributeNS(null, "height", "93");
rect.setAttributeNS(null, "x", x);
rect.setAttributeNS(null, "y", y);
svg.appendChild(rect);

2. 事件处理程序中的this上下文

每当事件监听器被调用时,this将绑定到触发事件的元素。然而,在您的代码中,您不需要this,因为所有信息都可以通过传递给函数.tttSetupSquares()的参数brd获得。

handleClick: function (event) {
    try {
        var svgNS = brd.namespaceURI;
        var line = document.createElementNS(svgNS, 'line');
        brd.appendChild(line);
    }
    catch (err) {
        alert("handlClick err: " + err);
    }
}

请看以下代码片段,这是一个可行的示例:

function getEl(s) { return document.getElementById(s); }

  var svg;  // get the TTT svg
  var cells = new Array(3);

  // execute after HTML has rendered
  !(function () {
    svg = getEl("svgTTT");
    tttSetupSquares(svg);
    alert("click-squares are setup.");
  }());

  // setup the click squares
  function tttSetupSquares(brd) {
    for (var i = 0; i < 3; i++) {
      cells[i] = new Array(3);
      for (var j = 0; j < 3; j++) {
        var x = 3 + (i * 100);
        var y = 3 + (j * 100);
        var newId = "svgTTTsqr" + i.toString() + j.toString();
        
        // Use DOM manipulation instead of innerHTML
  var rect = document.createElementNS(svg.namespaceURI, 'rect');
        rect.setAttributeNS(null, "id", newId);
        rect.setAttributeNS(null, "fill", "#00DD00");
        rect.setAttributeNS(null, "width", "93");
        rect.setAttributeNS(null, "height", "93");
        rect.setAttributeNS(null, "x", x);
        rect.setAttributeNS(null, "y", y);
        svg.appendChild(rect);
        
        //make a cell object to contain all of this
        var cell = {
          col: i,
          row: j,
          pSvg: brd,
          rect: rect,

          handleClick: function (event) {
            try {
              //console.log(this);
              var svgNS = brd.namespaceURI;
              var line = document.createElementNS(svgNS, 'line');
              line.setAttributeNS(null, "x1", this.x.baseVal.value);
              line.setAttributeNS(null, "y1", this.y.baseVal.value);
              line.setAttributeNS(null, "x2", this.x.baseVal.value + this.width.baseVal.value);
              line.setAttributeNS(null, "y2", this.y.baseVal.value + this.height.baseVal.value);
              brd.appendChild(line);
            }
            catch (err) {
              alert("handlClick err: " + err);
            }
          }
        }

        //add a click handler for the rect/cell
        cell.rect.addEventListener("click", cell.handleClick, false);

        //save the cell object for later use
        cells[i][j] = cell;
      }
    }
  }
line {
  stroke: red;
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
    id="svgTTT" width="150" height="150" viewBox="0 0 300 300"  >
    <rect width="3" height="300" x="99" fill="#008d46" />
    <rect width="3" height="300" x="199" fill="#000000" />
    <rect width="300" height="3" y="99" fill="#008d46" />
    <rect width="300" height="3" y="199" fill="#d2232c" />
  </svg>


哦,我一直在想使用 innerHtml 会有什么不利之处。我要试一下这个... - RBarryYoung
为什么这是答案?Rect的目的不是绘制线条,而是可点击并在处理程序中具有正确的上下文。这个答案是如何解决问题的?这个片段甚至不能工作。 - Holger Will
1
@HolgerWill,我刚看到你的评论。我将此帖子标记为答案,因为它正确地识别了我做错的两件事情。首先,我使用了一个循环来设置.InnerHtml,然后添加了Rect事件,因此InnerHtml修改导致所有先前的Rect事件丢失。其次,this在事件处理程序中根本没有像我想象的那样工作。您的答案非常详细地解决了this问题(所以我点了赞),但是忽略了InnerHtml问题,因此我选择了这篇文章作为答案。至于片段不起作用,我没有尝试它,而是修复了自己的代码。 - RBarryYoung
@RBarryYoung,为了结束这个话题,我完成了我的代码片段,不仅修复了错误,而且还具有视觉吸引力;-) - altocumulus
@altocumulus,你事件处理程序中的this上下文仍然是矩形而不是抽象单元格对象。如果你想采用这种方法,所有需要的额外数据都必须存储在矩形本身上(例如,如果它被点击了,或者它在棋盘中的索引等)。这可以很容易地完成。推荐的做法是使用data-*属性。因此,至少要将单元格的索引存储在data-attributes中... cell.rect.setAttribute("data-col",i)cell.rect.setAttribute("data-row",j),所以在你的点击处理程序中,你可以这样做:this.getAttribute("data-index-i") - Holger Will
最终,您仍然需要在两个数据结构中拥有所有数据,一个是在对象上,另一个是在矩形上... 最好一开始就正确理解上下文;-) - Holger Will

3
如上所述,“this”的问题在于它会被绑定到一个上下文中,而这个上下文并不总是你想要的。针对这种类型的问题存在许多解决方案。从臭名昭著的self=this技巧到.bind()。此外还有许多答案,通常可能是重复的。在这里可以找到一个好的答案和一些后续阅读:
var self = this?
或者在这里:如何在Javascript中更改函数的上下文
或者在这里:http://ryanmorr.com/understanding-scope-and-context-in-javascript/
或者在这里:https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Operators/this
虽然你的问题的真正答案是非常具体的。在事件处理程序的情况下,有一个解决“this”问题的解决方案。你只需要实现一个EventListener接口就行了。这听起来比实际情况复杂得多。实际上它非常简单。你的对象只需要实现一个函数:.handleEvent。当你将一个对象传递给addEventListener()函数时,它会自动调用此函数。这种方法的好处在于,使用这种方法,“this”的上下文将自动正确。不需要任何技巧或解决方法。对于一般情况来说,了解解决方法肯定是有益的,但对于这个具体的问题,.handleEvent就是解决方案

here is a complete working example:

  function getEl(s) { return document.getElementById(s); }

  var svg;  // get the TTT svg

  // execute after HTML has rendered
  window.onload = function () {
    svg = getEl("svgTTT");
    tttSetupSquares(svg);
    //alert("click-squares are setup.");
  }

  var cells = new Array(3);
  // setup the click squares
  function tttSetupSquares(brd) {
    for (var i = 0; i < 3; i++) {
      cells[i] = new Array(3);
      for (var j = 0; j < 3; j++) {
        var x = 3 + (i * 100);
        var y = 3 + (j * 100);
        var rect= document.createElementNS("http://www.w3.org/2000/svg","rect")
rect.setAttribute("x",x);
rect.setAttribute("y",y);
rect.setAttribute("width",100);
rect.setAttribute("height",100);
rect.setAttribute("fill","grey")

        brd.appendChild(rect)
        var cell = {
          col: i,
          row: j,
          pSvg: svg,
          rect: rect,

          handleEvent: function (event) {
            try {
              // this line fails because `this` is the target, not the cell
              var svgNS = this.pSvg.namespaceURI;

              var line = document.createElementNS(svgNS, 'line');
              line.setAttribute("x1",this.rect.getAttribute("x"))
              line.setAttribute("y1",this.rect.getAttribute("y"))
              line.setAttribute("x2",this.rect.getAttribute("x")*1+this.rect.getAttribute("width")*1)
              line.setAttribute("y2",this.rect.getAttribute("y")*1+this.rect.getAttribute("height")*1)
              line.setAttribute("stroke","red")
              this.pSvg.appendChild(line);
              document.getElementById("out").innerHTML="rect("+this.col+","+this.row+") was clicked"
            }
            catch (err) {
              alert("handlClick err: " + err);
            }
          }
        }

        //add a click handler for the rect/cell
        cell.rect.addEventListener("click", cell, false);
        //(only seems to work the last time it is executed)

        //save the cell object for later use
        cells[i][j] = cell;
      }
    }
  }
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"
    id="svgTTT" width="150" height="150" viewBox="0 0 300 300"  >
    <rect width="3" height="300" x="99" fill="#008d46" />
    <rect width="3" height="300" x="199" fill="#000000" />
    <rect width="300" height="3" y="99" fill="#008d46" />
    <rect width="300" height="3" y="199" fill="#d2232c" />
  </svg>

<div id="out"></div>

对于事件处理程序而言,这是正确的解决方案,其他任何方法都是hack!

我可以问一下给我点踩的人,我的回答哪里有问题吗?我很乐意进行更正,使它成为更好的回答。 - Holger Will
注意:这个答案对解决我的问题非常有帮助。+1 - RBarryYoung

2

一些建议:

您可以考虑使用事件委托。如果您使用像jQuery、Angular或React这样的框架或库,它会自动为您执行事件委托。在许多单独的DOM元素上有许多事件处理程序会影响性能。相反,您可以在包装元素上使用“click”处理程序,并使用event.target属性查看实际点击的元素。

svg.addEventListener("click", function (e) {
    if (e.target.nodeName === "rect" && (/^svgTTTsqr/).test(e.target.id)) {
        // Use a regexp on  e.target.id to find your
        // cell object in `cells`
    }
});

正则表达式可能会有些复杂,因此您可能应该使用数据属性。

// Generating the HTML
brd.innerHTML += "<rect id='"+newId+"' data-i='" + i + "' data-j='" + j + "' "

// The event handler:
svg.addEventListener("click", function (e) {
    if (e.target.nodeName === "rect" && (/^svgTTTsqr/).test(e.target.id)) {
        var i = e.target.getAttribute("data-i");
        var j = e.target.getAttribute("data-j");
        var cell = cells[i][j];
        cell.handleClick();
    }
});

如果你这样做,你也可以很容易地进行另一个性能调整,即先生成整个HTML字符串,然后在单个操作中将其附加到DOM中,因为你不再需要在循环时将HTML插入DOM并添加事件监听器。

至于你的问题:

1)抱歉,无法帮助您:( 需要设置可执行示例并进行调试,阅读代码时无法想到任何事情。我仍会发表回答,希望以上解释的事件委托解决方案可以解决问题。

2)直接调用裸函数会将它们的“this”绑定到它们被调用的作用域。调用者还可以显式地设置“this”绑定到什么上面。解决方案是创建一个新函数,该函数被包装以强制“this”成为您想要的任何内容。使用内置的function(){}.bind(cell),将返回一个新函数,该函数包装了原始函数,并且在原始函数中this将始终设置为cell,无论调用bind返回的函数的上下文是什么。


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