为什么在重用片段时事件侦听器会消失?

Why do event listeners disappear when reusing a fragment?

我正在尝试像这样重用片段,但事件侦听器只工作一次,当片段被重用时不再存在:

https://codepen.io/dvtan/pen/dyyGVKm

let $home = $(`
<div>
  <h1>Home page</h1>
  <button class="btn btn-primary">Go to another page</button>
</div>`);

let $another = $(`
<div>
  <h1>Another page</h1>
  <button class="btn btn-primary">Go back</button>
</div>`);

$("body").html($home);

$home.find('.btn').on('click', () => {
  $("body").html($another);
});

$another.find('.btn').on('click', () => {
  $("body").html($home);
});

我认为凭直觉,事件侦听器应该跟随片段,所以它们就这样消失是没有意义的。为什么会这样,有什么解决办法?

这是您在原版中尝试做的事情 javascript:

var body = document.body;
var pages = {};
['home', 'other'].forEach(function(page){
  var p = pages[page] = document.getElementById(page);
  body.removeChild(p);
});

go = function(where){
  body.innerHTML = '';
  body.appendChild(pages[where]);
}

go('home');
<body>
<div id='home'>
  <h1>Home Page</h1>
  <button onclick="go('other')">Go to another Page</button>
</div>
<div id='other'>
  <h1>Another Page</h1>
  <button onclick="go('home')">Go Home</button>
</div>
</body>

我认为您希望调用 jQuery 方法 .html() 与上面普通代码中的 go() 方法具有相同的效果。扔掉旧的 HTML,并放入新内容 - 实际上不是 HTML(=string),而是一个 dom 树,其中已经有处理程序。这种期望一点也不不合理 - 但 jQuery 并没有实现它,我将解释为什么会这样。

问题是 NOT 新东西有处理程序(jQuery 没问题);问题是 OLD 东西被扔掉了。 .html(),当把东西扔掉时,不只是把它从 Dom 树中移除然后忘记它;它假设(与 .detach() 不同,见下文)旧东西不是您稍后要重新插入的东西,而是垃圾,必须是 'incinerated' 以免导致内存泄漏。

来自文档:

When .html() is used to set an element's content, any content that was in that element is completely replaced by the new content. Additionally, jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.

旧dom节点导致内存泄漏的方式是这样的:通过jQuery方法.data()与dom节点关联的数据未存储在节点本身,但在中央 jQuery-interal 'cache' - 节点仅具有对缓存的引用,因此节点可以找到其数据。如果您以原始方式(parent.removeChild())删除节点,旧垃圾节点的数据将保留在 jQuery 数据缓存中并在那里浪费 space。我不确定事件处理程序如何出现与上述类似的问题,但我确定它们的删除有类似的原因。进一步讨论内存泄漏问题 here and in the upvoted comment to this question

现在,在您的代码中,事件处理程序未设置为属性 (onclick),而是设置为 .on,后者被转换为原版 addEventListener。当 .html() 删除旧内容时,那些(参见 jQuery 文档中的引用)被焚化。

确实,如果 jQuery 省略上面的焚烧,在某些情况下编码会更容易一些,但是,由此产生的内存泄漏问题会更糟(因为相比之下它们更难找到到你这里的小问题),所以如果 jQuery 不辜负你的期望,那么坏处就会超过好处。 (旁注:如果我没记错的话,旧的 jQuery 版本是这样的,但不要引用我的话。)

解决方法有多种。最接近原始代码的版本(以及高于原始版本,但使用 jQuery)是通过使用 .detach() 来规避焚烧,这是从 dom 树 没有 焚烧它。然后,确实,'event handlers follow the fragment around'.

像这样:

var pages = {}, showing = null;
['home', 'other'].forEach(function(page){
  pages[page] = $('#' + page).detach();
});
go = function(where){
  if (showing){ pages[showing].detach(); }
  $('body').append(pages[where]);
  showing = where;
}

go('home')
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<body>
<div id='home'>
  <h1>Home Page</h1>
  <button onclick="go('other')">Go to another Page</button>
</div>
<div id='other'>
  <h1>Another Page</h1>
  <button onclick="go('home')">Go Home</button>
</div>
</body>

我不确定我是否同意,但大多数人不使用 .detach(),而是让焚化发生,然后重新创建处理程序,就像这样(以及这里的事件处理程序 不要 'follow around'):

let homeHTML = `
<div>
  <h1>Home page</h1>
  <button class="btn btn-primary">Go to another page</button>
</div>`;

let anotherHTML = `
<div>
  <h1>Another page</h1>
  <button class="btn btn-primary">Go back</button>
</div>`;

function setHome(){
  $("body").html(homeHTML);
  $("body").find('.btn').on('click', setAnother);
}
function setAnother(){
  $("body").html(anotherHTML);
  $("body").find('.btn').on('click', setHome);
}
setHome();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

jQuery团队做出了上述焚化的正确决定。但它确实有一个缺点,你的问题就指向了它。

以上两种变通办法都有一些不尽如人意的地方(或者 'ugly',如果你像我一样是极端的完美主义者)。 detach-append是两步,.html-.on也是两步,但整体应该是一步 .单页应用程序中的内容更改应该是类似于变量赋值的一行。例如(在伪代码中)library.set(body, 'otherPage')body <- otherPage 左右。 jQuery .html() 命令不是单行的。不是,不是BUG,也不是选美冠军。

这是将动态 HTML 注入文档时的预期行为,而不是 jQuery 中的 'bug'。发生这种情况是因为当您注入动态 HTML ,您必须重新绑定事件和事件处理程序。完成此操作的最简单方法是使用:

// This is the important part ||
//                            \/ can be an #id or .class
$(document).on('click', "#some-id", function(event) {
  someHandler(); // event handler goes here
});

You can read more here

let homeHTML = `
<div>
  <h1>Home page</h1>
  <button id="nav-button" class="btn btn-primary">Go to another page</button>
</div>`;

let anotherHTML = `
<div>
  <h1>Another page</h1>
  <button id="nav-button" class="btn btn-primary">Go back</button>
</div>`;

function navigatePage(element, html, handler) {
  $("body").html(html);
  $(document).on('click', element, handler);
}

function setHome() {
  navigatePage("#nav-button", homeHTML, setAnother);
}

function setAnother() {
  navigatePage("#nav-button", anotherHTML, setHome);
}

setHome();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>