14

I am working a lot with nodes before they are attached to the main page's DOM; and I need to perform some work depending on whether or not a given node is contained within the main document or not.

My current method is to walk up the parents, via:

if this.el$.closest("body").length > 0

Is there a more appropriate way to do this? (Preferrably one that doesn't have to walk all of the node's ancestors?)

Nevir
  • 7,951
  • 4
  • 41
  • 50
  • Well it will vary by how deep down I am in the tree, I don't know what I have in store for this project I could be doing some crazy shit and be super far down, results will vary :) – Jonathan Stanton Aug 13 '12 at 23:07
  • It definitely would be interesting to see if there's a more appropriate way of testing this, rather than having to walk the node's ancestry every time – Nevir Aug 13 '12 at 23:11
  • what about just `.parent()`? I just did a quick test and an element in memory had no parent. Or even quicker would be javascript's `.parentNode` – MrOBrian Aug 13 '12 at 23:20
  • @MrOBrian I'm fairly certain that when you call `appendChild` on an in-memory node it will set the `parent` of the appended node normally. If the OP is building trees in memory, that won't be enough. – apsillers Aug 13 '12 at 23:23
  • See [this very similar question](http://stackoverflow.com/questions/5629684/javascript-check-if-element-exists-in-the-dom), I think the solution there would be faster – MrOBrian Aug 13 '12 at 23:32
  • @MrOBrian: You are indeed correct, the method added in the linked solution is slightly faster and I added it to my tests for completeness. Thanks for the info and the link. – Nope Aug 13 '12 at 23:50

3 Answers3

11

The modern, vanilla answer is to use Node.isConnected:

let test = document.createElement('p');
console.log(test.isConnected); // Returns false
document.body.appendChild(test);
console.log(test.isConnected); // Returns true

(example taken directly from the MDN docs)

Alec
  • 2,432
  • 2
  • 18
  • 28
7

You have several options which execute at several different speeds.

var $document = $(document);
var $element = $("#jq-footer");
var exists;

// Test if the element is within a body
exists = $element.closest("body").length;

// Test if the document contains an element
// wrong syntax, use below instead --> exists = $.contains($document, $element);
exists = $.contains(document.documentElement, $element[0]);

// Test if the element is within a body
$($element).parents().is("body");

// Manually loop trough the elements
exists = elementExists($element[0]);

// Used for manual loop
function elementExists(element) {
    while (element) {
        if (element == document) {
            return true;
        }
        element = element.parentNode;
    }
    return false;
}​

See Performance Test

For this test I copied a huge amount of html to traverse over, I copied the source of one of the jQuery pages into a fiddle, stripping out all the script tags leaving only the body and the html in between.

Feel free you use document instead of "body" and vice-versa or add more tests but it should give you the general idea.

Edit

I updated the performance test to use the correct syntax for contains as the previous one was incorrect and always returned true even if the element did not exist. the blow now returns true if the element exists but if you specify a selector which doesn't exist it will return false.

exists = $.contains(document.documentElement, $element[0]);

I also added the suggested alternative mentioned by MrOBrian in the comments of the question which is again slightly faster than contains.

Nice one MrOBrian.

Edit

Here is the jsPerf performance test with all the nice charts.

Thanks Felix Kling for spotting the issue and helping me fix the jsPerf tests.

Added more test results from the comments, this one is really good:
jsPerf performance test: dom-tree-test-exists

Nope
  • 22,147
  • 7
  • 47
  • 72
  • Awesome! `$.contains` looks like the way to go – Nevir Aug 13 '12 at 23:34
  • You might be interested in this jsPerf comparison: http://jsperf.com/dom-tree-test – Felix Kling Aug 13 '12 at 23:53
  • @FelixKling: I tried setting up jsPerf but got `TypeError: Object [object Object] has no method 'compareDocumentPosition'.` when trying to execute the contains code. Here is the link to the failing test: `http://jsperf.com/closest-vs-contains` Not sure how to fix it. So I resorted to time stamping in fiddle instead :) – Nope Aug 13 '12 at 23:55
  • 1
    The problem was that you passed to jQuery objects to `$.contains`, not DOM nodes... – Felix Kling Aug 14 '12 at 00:03
  • @FelixKling: Awesome, I fixed the tests up now and added them to the answer, the more info the better. Thanks and well spotted. – Nope Aug 14 '12 at 00:13
  • @Jonathan: According to [this test](http://jsperf.com/dom-tree-test-exists), ID lookup for attached elements is even faster than `$.contains` in Chrome. And in Firefox, using `document.body.contains` seems to be a lot faster than anything else. – Felix Kling Aug 14 '12 at 00:33
  • @FelixKling: Nice find, I will add this to the answer as well. Very very good information. – Nope Aug 14 '12 at 00:39
  • `document.body.contains` is crazy-fast (3-4x faster) on Safari 6 and iOS 6, as well – Nevir Aug 14 '12 at 02:31
4

You could assign an ID to the element and then search for it:

var id = element.id || generateRandomId(); // some function generating a random string
if(document.getElementById(id) !== null) {
    // element in tree
}

Here is a performance comparison including François' suggestions, comparing each method for an attached and detached element node. Here are the test cases for only existing nodes, to get a better idea for the speed difference.

Test results:

Obviously testing a detached node with the while is faster, since it terminates nearly instantly (at the second iteration). But if the node has a (potentially) detached ancestor, then assigning an ID to the node and look for it seems to be the fastest way in Chrome 21.

Interestingly though, in Firefox 14, the Node#contains [MDN] method seems to be much faster than anything else.

Since the speed improvements in Firefox from ID lookup to native .contains seems to be higher than the performance loss in Chrome, a fast function could look like this:

function in_tree(element) {
    if(!element || !element.parentNode) { // fail fast
        return false;
    }
    if(element.contains) {
        return document.body.contains(element);
    }
    var id = element.id || generateRandomId();
    element.id = id;
    return document.getElementById(id) !== null;
}

But in the end, there will always be differences between browsers, so you have to make a compromise.

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143