addEventListener和`this`没有按预期工作

时间:2015-09-26 18:49:26

标签: javascript html html5 svg dom-events

我是一名经验丰富的程序员,但对网络编程有点新鲜。我正在尝试使用VS2010学习Javascript,HTML5和SVG,编写一个使用Javascript播放Tic-Tac-Toe的HTML页面。

我已成功创建九个方块中的每一个作为SVG <rect...>元素,但是我遇到了每个方块的点击事件处理程序的问题。

这里是HTML文件中存在的基本SVG元素:

  <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板的交叉哈希线。九个棋盘方块是在一个从windows load事件调用的javascript函数中创建的(下面)。

这里是javascript(在HTML正文的<script>元素中内嵌):

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

(我可以提供完整的页面源代码,但它只是包含这些内容的HTML元素。)

问题是双重的:

  1. 只有最后一个addEventListener似乎有效。点击所有其他方块什么都不做。单击最后一个方块(svgTTTsqr22)会运行cell.handleClick,但会导致问题2(下面)。 Chrome开发者工具(F12)显示除了最后一个没有事件监听器的所有<rect>个元素。

  2. cell.handleClick确实运行时,它在第一行(var svgNS = this.pSvg.namespaceURI;)失败并出现类似&#34的错误; 未定义的对象没有名为&#的属性34;的namespaceURI &#34; Devleoper Tools中的检查显示它失败,因为this未设置为cell对象,而是设置为单击的SVG <rect>元素。

  3. 所以我的问题是:

    一个。我在这做错了什么,

    B中。我该怎么做/应该这样做?

3 个答案:

答案 0 :(得分:4)

<强> 1。缺少事件处理程序

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

// 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,因为参数brd可以获取所有信息,这些信息会传递给函数.tttSetupSquares()

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

有关工作示例,请参阅以下代码段:

&#13;
&#13;
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;
      }
    }
  }
&#13;
line {
  stroke: red;
}
&#13;
<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>
&#13;
&#13;
&#13;

答案 1 :(得分:3)

如上所述,&#34;这个&#34;是它被绑定到一个上下文,这并不总是你想要的上下文。这类问题有很多解决方案。从臭名昭着的self=this技巧到.bind()。对此也有很多答案,一般来说可能是重复的。一个好的答案和一些后续阅读可以在这里找到:
var self = this?
或者在这里:How to change the context of a function in javascript
或者在这里:http://ryanmorr.com/understanding-scope-and-context-in-javascript/
或者在这里:https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Operators/this

虽然对您的问题的真正答案非常具体。在事件处理程序的情况下,有一个&#34;这个&#34; -problem的解决方案。您只需实现EventListener接口。这听起来比现在复杂得多。事实上它很容易。您的对象只需要实现一个函数:.handleEvent。将对象传递给addEventListener()函数时,会自动调用此函数。关于这个的好处是,使用这种方法,&#34;这个&#34;会自动成功。无需黑客或变通方法。知道一般情况的解决方法肯定是好的,但对于这个特定情况,.handleEvent是解决方案

这是一个完整的工作示例:

&#13;
&#13;
  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;
      }
    }
  }
&#13;
  <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>
&#13;
&#13;
&#13;

在EventHandlers的情况下,这是正确的解决方案,其他一切都是黑客!

答案 2 :(得分:2)

一些建议:

您可以考虑使用事件委派。如果你使用像jQuery或Angular或React这样的框架或库,它会自动为你做事件委托。在单独的DOM元素上安装许多事件处理程序会损害性能。你可以做的是做一个&#34;点击&#34;包装元素上的处理程序,并使用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)明显的原始功能有他们的&#39;这个&#39;绑定到他们被调用的任何范围。调用者也可以明确地设置这个&#39;这个&#39;一定会。解决方案是创建一个新的功能,这个功能被包装,以便这个&#39;被迫成为你想要的任何东西。使用内置的function () {}.bind(cell)将返回一个包装原始函数的新函数,而原始this将始终设置为cell,而不管{{1}返回的函数的上下文调用。