NodeIterator and TreeWalker are usually more powerful than you need. However, if you have to do something complex with a collection of DOM nodes, you will love what you can do with these iterator APIs.
Parameters
Both NodeIterator and TreeWalker take one required parameter, root
, and two optional parameters, whatToShow
and filter
.
whatToShow
and filter
use NodeFilter constants to build DOM node filters. The important thing to remember is that NodeIterator and TreeWalker iteration methods will only work on nodes included in the filtered collection.
root
root
expects a reference to a node. If you only pass root
to the constructor, your NodeIterator will contain that node and all the nodes in its subtree.
whatToShow
The first NodeFilter parameter, whatToShow
, allows you to limit the DOM nodes in your collection by passing a NodeFilter SHOW
constant. For example, passing NodeFilter.SHOW_ALL
will include every DOM node in the collection. NodeFilter.SHOW_ELEMENT
only includes elements in your collection. NodeFilter.SHOW_TEXT
would only include text nodes in your collection.
filter
The second NodeFilter parameter, filter
, is a function that is run on every node in the collection created by the first two parameters. It expects either an anonymous callback function or an object with an acceptNode
method. These functions should return at least one of three FILTER
constants.
NodeFilter.FILTER_ACCEPT
includes the current node in the collection. NodeFilter.FILTER_SKIP
does not include the node. NodeFilter.FILTER_REJECT
does not include the node or any of the nodes in its subtree.
A valid anonymous function to pass as a filter looks like:
(node) => {
if (node.matches("h1")) return NodeFilter.FILTER_REJECT;
if (node.matches("div")) return NodeFilter.FILTER_ACCEPT;
if (node.matches("ul")) return NodeFilter.FILTER_SKIP;
}
One of the ways to pass it as an object method looks like:
{
acceptNode: function (node) {
if (node.matches("h1")) return NodeFilter.FILTER_REJECT;
if (node.matches("div")) return NodeFilter.FILTER_ACCEPT;
if (node.matches("ul")) return NodeFilter.FILTER_SKIP;
}
}
NodeIterator
The NodeIterator puts your nodes in a list instead of maintaining the tree structure. To make one, you use document.createNodeIterator(root, whatToShow, filter)
.
NodeIterator has five read-only properties. root
, whatToShow
, and filter
will return objects matching what you passed into the constructor. referenceNode
will return a reference to the current node. pointerBeforeReferenceNode
returns a boolean. If true, a pointer is set on the node before the referenceNode
. This helps the iterator traverse your collection of nodes.
Both the root
and referenceNode
properties have a lot of properties with information about the nodes. For example, NodeIterator.referenceNode
has information about the current node's attributes, parent nodes, sibling nodes, and more.
NodeIterator has two methods - nextNode()
and previousNode()
. These methods will traverse the collection of nodes in the iterator. For example, nextNode()
will look for the next node in the collection. If it exists, it will update referenceNode
and return a reference to the new referenceNode
at the same time. If it doesn't exist, it will just return null
.
The following code would create a NodeIterator with a list of seven <li>
s.
<ul id="root">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>
5
<ul id="sub-list">
<li>6</li>
<li>7</li>
</ul>
</li>
</ul>
const NodeIterator =
document.createNodeIterator(document.getElementById("root"),
NodeFilter.SHOW_ELEMENT,
(node) =>
if (node.matches("li")) return NodeFilter.FILTER_ACCEPT
)
The referenceNode
will start as the root, so to get to <li>
1, I can use NodeIterator.nextNode()
. If I call it four more times, I'll be on <li>
5.
TreeWalker
The TreeWalker keeps your nodes in a tree structure. To make one, you use document.createTreeWalker(root, whatToShow, filter)
.
It has three read-only properties, root
, whatToShow
, and filter
. These properties and the first two methods, nextNode()
and previousNode()
, work the same way they do in NodeIterator.
Because TreeWalker maintains the tree structure, we also get methods that use nodes' relationships to one another: parentNode()
, firstChild()
, lastChild()
, previousSibling()
, and nextSibling()
. They also iterate to a matching node and return a reference to it or just return null
if a matching node is not found.
The following code would create a TreeWalker with a tree collection of seven <li>
s. The currentNode
will start as the root.
<ul id="root">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>
5
<ul id="sub-list">
<li>6</li>
<li>7</li>
</ul>
</li>
</ul>
const NodeIterator =
document.createTreeWalker(document.getElementById("root"),
NodeFilter.SHOW_ELEMENT,
(node) =>
if (node.matches("li")) return NodeFilter.FILTER_ACCEPT
)
Like NodeIterator, root
and currentNode
properties have a lot of properties with information about the nodes. Unlike NodeIterator, the TreeWalker's currentNode
property can be reassigned, as long as it is given a valid reference to a node. You can even set it to a node that wouldn't match the filter. Then, you can call nextNode()
, and the TreeWalker will still iterate to the next node in the DOM that matches your filter. Combined with the information TreeWalker stores about the DOM, you can hop all around the tree.
Say my use case for my collection of seven <li>
s also involves jumping from <li>
7 to it's parent, <li>
5. Because the sub-list <ul>
isn't in the collection, I can't use parentNode()
to get there. previousNode()
would work for <li>
6, but not <li>
7. Luckily, I can reassign currentNode
using currentNode
properties.
TreeWalker.currentNode = TreeWalker.currentNode.parentNode.parentNode;
When to Use Them
Instantiation of these iterators is slower than a normal loop. If you're planning on using it once or a few times, document.querySelectorAll()
with a for loop or NodeList methods should suit your needs.
Similarly, the more complex your filter, the slower these iterators will be. If you're looking to find a few nodes based on a complex series of CSS selectors, loops will probably be a lot faster.
Where NodeIterator and TreeWalker really shine is when you have a use case that requires referencing the collection of nodes repeatedly. They both store a lot of information about the DOM, so that's especially true when you need to know the relationships between nodes.
The main thing they offer over other ways to iterate through nodes is a reference to a node. A NodeList may let you access similar information, but you'll still have to find the node you want every time, usually by index.
You can use NodeIterator.referenceNode
and TreeWalker.currentNode
like you would ref.current
in React or document.getElementById("id")
in vanilla JS. For example, managing a roving tabIndex.
TreeWalker.currentNode.tabIndex = -1;
Because methods like nextNode()
also return a reference, you can iterate and focus the element all in one line.
TreeWalker.nextNode().focus()
TreeWalker.currentNode.tabIndex = 0;
Conclusion
Somewhere in all my research, I saw someone say that a web developer may use these iterators a maximum of five times in their entire career. Maybe I'm lucky a use case popped up at work. Either way, I had fun tree walking.
Top comments (5)
I am thinking of updating only the page items that have changed after a call to an endpoint and this endpoint returns the new HTML. Do you think it would be a good idea to use two TreeWalkers to compare the original DOM with the DOM of the HTML string I receive and update each of the items? Or would this not be possible?
So like a Virtual DOM but without the Virtual part
I feel like React / Vue / Solid’s approach of treating the DOM as throwaway state have trained a generation of devs to not understand how powerful the DOM is.
If you knew what these were before this article, you're the first person I know who did. 🤣
But yes, I'll keep talking about HTML/the DOM until people stop asking me why I'm not just talking about a JS framework.
I’m 1,000 years old in webdev years and nope, I had never heard of these methods, and I thought I knew some pretty obscure parts of the DOM. Thanks for the deep dive!