在 JavaScript 中异步创建的动态元素,jQuery 为何无法正常获取元素?

1,248 阅读6分钟

问题综述

在讲解这一问题之前,我们首先需要了解一下异步创建动态元素和同步创建动态元素的区别。在 JavaScript 中,动态创建元素通常被分为同步创建和异步创建两种方式。同步创建元素是指在代码的执行过程中直接创建元素节点,并将其添加到 DOM 树中。例如,可以使用如下代码创建一个<div>元素并将其添加到文档之中。

let div = document.createElement('div');
document.body.appendChild(div);

异步创建元素则是指在代码的执行过程中,通过异步操作(例如 AJAX 请求)获取元素的 HTML 代码,然后再将其添加到 DOM 树中。例如,可以使用如下代码通过 AJAX 请求获取一个<div>元素的 HTML 代码,并将其添加到文档中。

let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/some-div.html', true);
xhr.onload = function() {
  let div = document.createElement('div');
  div.innerHTML = xhr.response;
  document.body.appendChild(div);
};
xhr.send();

异步创建元素通常用于通过网络获取元素内容,因为网络请求需要一定的时间来完成,因此不能立即获取元素内容并将其添加到 DOM 树中。而同步创建元素通常用于在代码执行过程中直接创建和添加元素,因为它们可以立即执行,所以不需要等待异步操作完成。

在 JavaScript 中,当我们使用异步的方式创建动态元素时,即在代码执行过程中使用 DOM API 通过 JavaScript 动态地添加新的元素节点或修改已有的元素节点,这些新的节点并不会立即被浏览器所渲染,而是需要等待浏览器将这些节点添加到 DOM 树中后才能被正确地访问和操作。在这种情况下,如果我们使用 jQuery 或原生的 querySelectorquerySelectorAll 方法来获取这些动态创建的元素节点,通常会发现这些方法无法正常获取到这些元素节点,而使用 getElementsByNamegetElementByIdgetElementsByTagNamegetElementsByClassName 方法则可以正常获取元素。

这个问题的根本原因在于这些方法所使用的查询引擎不同。jQuery 和 querySelectorquerySelectorAll 方法都是使用 CSS 选择器引擎进行查询的,而这些引擎在查询时会依赖于当前页面的渲染结果。也就是说,如果一个元素节点还没有被渲染到页面上,那么它在查询时就无法被找到。

相比之下,getElementsByNamegetElementByIdgetElementsByTagNamegetElementsByClassName 方法则是直接查询 DOM 树的节点,不依赖于页面的渲染结果。因此,它们可以正常地获取到动态创建的元素节点。

具体来说,getElementsByNamegetElementByIdgetElementsByTagNamegetElementsByClassName 方法是通过遍历 DOM 树中的节点来查找匹配的元素节点。这些方法在遍历 DOM 树时,会将新增的元素节点添加到遍历列表中,因此可以正常地获取到这些元素节点。

而 jQuery 和 querySelectorquerySelectorAll 方法则不同,它们使用的是基于当前页面渲染结果的查询引擎。这些引擎在查询时,会先根据 CSS 选择器查询到所有匹配的元素节点,然后再根据它们在页面上的位置进行排序。由于动态创建的元素节点还未被渲染到页面上,因此它们无法被正确地查询到。

解决方案

  1. 使用原生的 JavaScript 方法创建和添加元素:例如使用document.createElementelement.appendChild等原生 JavaScript 方法来创建和添加元素,确保它们已经被添加到了 DOM 树中。这样,无论是使用 jQuery 还是原生 JavaScript 方法都可以正常获取这些元素;
// 创建新的元素节点
let dynamicElement = document.createElement("div");
dynamicElement.id = "dynamic-element";

// 将元素节点添加到 DOM 树中
document.body.appendChild(dynamicElement);

// 使用 jQuery 查询
let $foundElement = $("#dynamic-element");
console.log($foundElement); // 输出 <div id="dynamic-element"></div>

// 使用原生的 JavaScript 方法查询
let foundElement = document.getElementById("dynamic-element");
console.log(foundElement); // 输出 <div id="dynamic-element"></div>

在这个示例中,我们首先使用 document.createElement 方法创建了一个新的元素节点,并将其添加到页面中。由于我们使用原生的 JavaScript 方法来添加元素节点,因此可以确保该节点已经被添加到 DOM 树中。然后,我们使用 jQuery 和原生的 JavaScript 方法来查询新创建的元素节点,并输出其结果。

需要注意的是,使用原生的 JavaScript 方法创建和添加元素节点可以确保节点已经被添加到 DOM 树中,但是也需要注意时机的选择。如果我们在页面加载完成之前或者在页面渲染完成之前执行添加元素的操作,可能会导致元素节点的添加失败或者无法被查询到。因此,在使用原生的 JavaScript 方法时,需要确保时机的选择和执行的顺序。

  1. 使用 jQuery 的事件委托机制:使用 jQuery 的时间委托机制来处理动态创建的元素。事件委托机制允许你在父元素上监听子元素的时间,这样即使子元素是动态创建的,也可以正常处理它们的时间。例如,可以使用$(document).on("click", ".dynamic-element", function() {})来处理动态创建的元素的点击事件;
// 创建新的元素节点
let dynamicElement = $("<div>", {
  id: "dynamic-element",
  class: "dynamic-element",
});

// 将元素节点添加到页面中
$("body").append(dynamicElement);

// 使用事件委托机制绑定点击事件
$(document).on("click", ".dynamic-element", function() {
  console.log("点击了动态创建的元素");
});

在这个示例中,我们首先使用 $("<div>") 方法创建了一个新的元素节点,并将其添加到页面中。然后,我们使用 jQuery 的事件委托机制来绑定点击事件,将事件监听器绑定到 document 对象上。这样就可以在动态创建的元素被点击时,触发事件监听器,并执行相应的回调函数。

需要注意的是,使用事件委托机制时,我们需要确保选择器的正确性和性能的优化。选择器应该尽可能地精确,以避免对不必要的元素进行事件监听。在实际的开发中,我们还可以考虑使用事件冒泡和事件捕获机制来优化事件的监听和处理。

  1. 使用 jQuery 的回调函数:如果你必须使用异步操作来创建动态元素,你可以使用 jQuery 的回调函数来确保元素已经被添加到 DOM 树中。例如,可以在一步操作完成后使用回调函数来处理元素的添加和选择。例如,可以使用$.ajax()方法的success毁掉函数来确保元素已经被添加到 DOM 树中后再使用 jQuery 选择器来选择它们;
// 使用异步操作创建动态元素
$.ajax({
  url: "/some/url",
  success: function(data) {
    // 创建新的元素节点
    let dynamicElement = $("<div>", {
      id: "dynamic-element",
      class: "dynamic-element",
    });
  
    // 将元素节点添加到页面中
    $("body").append(dynamicElement);

    // 使用 jQuery 选择器选择新创建的元素节点
    let foundElement = $("#dynamic-element");
    console.log(foundElement); // 输出 <div id="dynamic-element" class="dynamic-element"></div>
  },
});

在这个示例中,我们使用 $.ajax() 方法模拟了一个异步操作,该操作返回一个数据对象。在异步操作的成功回调函数中,我们使用 $("<div>") 方法创建了一个新的元素节点,并将其添加到页面中。在元素添加后,我们使用 jQuery 的选择器来选择新创建的元素节点,并输出其结果。

需要注意的是,在使用回调函数时,我们需要确保回调函数的执行时机和执行顺序。如果回调函数在元素添加之前执行,那么将无法找到新创建的元素节点。因此,在使用回调函数时,我们需要仔细考虑时机的选择和执行的顺序,以确保元素能够被正确地添加和选择。

  1. 使用原生的getElementsByNamegetElementByIdgetElementsByTagNamegetElementsByClassName方法来获取动态创建的元素。因为此类方法是基于文档的,它会在整个文档中搜索具有指定名称的元素,并返回一个元素列表。因此,即使元素是通过异步操作创建的并且还没有被添加到 DOM 树中,此类方法仍然可以找到它们。
// 创建新的元素节点
let dynamicElement = document.createElement("div");
dynamicElement.id = "dynamic-element";
document.body.appendChild(dynamicElement);

// 使用原生方法获取新创建的元素节点
let foundById = document.getElementById("dynamic-element");
console.log(foundById); // 输出 <div id="dynamic-element"></div>

let foundByName = document.getElementsByName("dynamic-element");
console.log(foundByName); // 输出 HTMLCollection [div#dynamic-element]

let foundByTagName = document.getElementsByTagName("div");
console.log(foundByTagName); // 输出 HTMLCollection [div#dynamic-element]

let foundByClassName = document.getElementsByClassName("dynamic-element");
console.log(foundByClassName); // 输出 HTMLCollection [div#dynamic-element]

在这个示例中,我们首先使用 document.createElement 方法创建了一个新的元素节点,并将其添加到页面中。然后,我们使用原生的 getElementByIdgetElementsByNamegetElementsByTagNamegetElementsByClassName 方法来获取新创建的元素节点,并输出其结果。

需要注意的是,虽然这些方法可以找到动态创建的元素,但是它们返回的是一个元素列表。如果我们只需要查询一个元素,可以使用 getElementById 方法,该方法会返回一个元素对象。同时,由于这些方法的搜索范围是整个文档,因此在文档较大时,性能可能会受到影响。因此,在实际的开发中,我们还需要考虑时机的选择和性能的优化。