Walking the DOM comes in two flavors. Depth-first dives down one branch to the bottom before backtracking. Breadth-first (BFS) fans out level by level — the root, then all its children, then all the grandchildren — which is what you want for "find the nearest matching element" or "process the tree tier by tier." BFS is powered by a queue: you take a node off the front and push its children on the back, so nodes come out in the order they were discovered.
Implement traverseDomBfs(root). Return the elements in breadth-first order — root first, then each level left to right. Visit element nodes only; a null root yields [].
function traverseDomBfs(root) {
// returns an array of elements in level-by-level order
}
// <div id=a><div id=b><div id=d/></div><div id=c/></div>
traverseDomBfs(a); // [a, b, c, d] (BFS)
// depth-first would be: a, b, d, c
node.children, skipping text/comment nodes.traverseDomBfs(null) returns [].You'll run a queue-based breadth-first search: dequeue a node, record it, enqueue its element children, and repeat until the queue is empty.
Breadth-first means "visit everything one level away before anything two levels away." The data structure that produces that order is a queue (first-in, first-out): you seed it with the root, then repeatedly take the node at the front, visit it, and add its children to the back. Because children go to the back, they're only visited after everything already in the queue — i.e. after the rest of their level's predecessors — which is exactly level-by-level order. Swap the queue for a stack (add/remove at the same end) and you'd get depth-first instead.
Picture a line at a ticket counter. The root joins the line first. You serve (visit) whoever's at the front, and as you serve each node you send its children to the end of the line. Since a node's children always join behind everyone currently waiting, an entire level is served before the next level starts. The traversal ends when the line empties.
The tempting version uses a stack (or recursion), which gives depth-first:
function traverseDomBfsNaive(root) {
const result = [];
const stack = [root];
while (stack.length) {
const node = stack.pop(); // LIFO -> depth-first
result.push(node);
for (const child of node.children) stack.push(child);
}
return result;
}
pop() takes from the end, so the last child pushed is visited next — you dive down a branch before its siblings. That's depth-first, not BFS, and it also reverses sibling order. To get breadth-first you must remove from the front (shift) while adding to the back (push) — a FIFO queue.
function traverseDomBfs(root) {
if (!root) return [];
const result = [];
const queue = [root];
while (queue.length > 0) {
const node = queue.shift(); // take from the FRONT
result.push(node);
for (const child of node.children) { // element children only
queue.push(child); // add to the BACK
}
}
return result;
}
module.exports = { traverseDomBfs };
Seed the queue with the root and loop until it's empty. Each iteration shifts the front node (FIFO), records it, and pushes its element children to the back. node.children is elements only, so text/comment nodes never enter the queue. Because children always land behind the current contents, a whole level is dequeued before the next level's nodes — producing root, level 1, level 2, …. Every element is enqueued and dequeued once, so it's O(n).
<div id=a><div id=b><div id=d/></div><div id=c><div id=e/></div></div>:
[a] — dequeue a, record [a], enqueue its children → queue [b, c].[b, c] — dequeue b, record [a, b], enqueue b's child → queue [c, d].[c, d] — dequeue c, record [a, b, c], enqueue c's child → queue [d, e].[d, e] — dequeue d (leaf) → [a, b, c, d]; dequeue e (leaf) → [a, b, c, d, e].[a, b, c, d, e] — level 0 (a), level 1 (b, c), level 2 (d, e).A depth-first walk would have produced a, b, d, c, e instead.
pop — gives depth-first, and reverses siblings. Use a queue: shift from the front, push to the back.childNodes instead of children — enqueues text/comment nodes. Use children for elements.Array.shift cost — shift is O(n) on a JS array; for huge trees a real queue (two-pointer or a linked list) keeps it O(1) per op. Fine at DOM scale.null.children throws; return [] for a missing root.shadowRoot and same-origin iframe documents, which children alone won't cross.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Walking the DOM comes in two flavors. Depth-first dives down one branch to the bottom before backtracking. Breadth-first (BFS) fans out level by level — the root, then all its children, then all the grandchildren — which is what you want for "find the nearest matching element" or "process the tree tier by tier." BFS is powered by a queue: you take a node off the front and push its children on the back, so nodes come out in the order they were discovered.
Implement traverseDomBfs(root). Return the elements in breadth-first order — root first, then each level left to right. Visit element nodes only; a null root yields [].
function traverseDomBfs(root) {
// returns an array of elements in level-by-level order
}
// <div id=a><div id=b><div id=d/></div><div id=c/></div>
traverseDomBfs(a); // [a, b, c, d] (BFS)
// depth-first would be: a, b, d, c
node.children, skipping text/comment nodes.traverseDomBfs(null) returns [].You'll run a queue-based breadth-first search: dequeue a node, record it, enqueue its element children, and repeat until the queue is empty.
Breadth-first means "visit everything one level away before anything two levels away." The data structure that produces that order is a queue (first-in, first-out): you seed it with the root, then repeatedly take the node at the front, visit it, and add its children to the back. Because children go to the back, they're only visited after everything already in the queue — i.e. after the rest of their level's predecessors — which is exactly level-by-level order. Swap the queue for a stack (add/remove at the same end) and you'd get depth-first instead.
Picture a line at a ticket counter. The root joins the line first. You serve (visit) whoever's at the front, and as you serve each node you send its children to the end of the line. Since a node's children always join behind everyone currently waiting, an entire level is served before the next level starts. The traversal ends when the line empties.
The tempting version uses a stack (or recursion), which gives depth-first:
function traverseDomBfsNaive(root) {
const result = [];
const stack = [root];
while (stack.length) {
const node = stack.pop(); // LIFO -> depth-first
result.push(node);
for (const child of node.children) stack.push(child);
}
return result;
}
pop() takes from the end, so the last child pushed is visited next — you dive down a branch before its siblings. That's depth-first, not BFS, and it also reverses sibling order. To get breadth-first you must remove from the front (shift) while adding to the back (push) — a FIFO queue.
function traverseDomBfs(root) {
if (!root) return [];
const result = [];
const queue = [root];
while (queue.length > 0) {
const node = queue.shift(); // take from the FRONT
result.push(node);
for (const child of node.children) { // element children only
queue.push(child); // add to the BACK
}
}
return result;
}
module.exports = { traverseDomBfs };
Seed the queue with the root and loop until it's empty. Each iteration shifts the front node (FIFO), records it, and pushes its element children to the back. node.children is elements only, so text/comment nodes never enter the queue. Because children always land behind the current contents, a whole level is dequeued before the next level's nodes — producing root, level 1, level 2, …. Every element is enqueued and dequeued once, so it's O(n).
<div id=a><div id=b><div id=d/></div><div id=c><div id=e/></div></div>:
[a] — dequeue a, record [a], enqueue its children → queue [b, c].[b, c] — dequeue b, record [a, b], enqueue b's child → queue [c, d].[c, d] — dequeue c, record [a, b, c], enqueue c's child → queue [d, e].[d, e] — dequeue d (leaf) → [a, b, c, d]; dequeue e (leaf) → [a, b, c, d, e].[a, b, c, d, e] — level 0 (a), level 1 (b, c), level 2 (d, e).A depth-first walk would have produced a, b, d, c, e instead.
pop — gives depth-first, and reverses siblings. Use a queue: shift from the front, push to the back.childNodes instead of children — enqueues text/comment nodes. Use children for elements.Array.shift cost — shift is O(n) on a JS array; for huge trees a real queue (two-pointer or a linked list) keeps it O(1) per op. Fine at DOM scale.null.children throws; return [] for a missing root.shadowRoot and same-origin iframe documents, which children alone won't cross.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.