Skip to content

Commit edd3adf

Browse files
committed
Adjust mutation observer to be faster and still account for moving and wrapping nodes
1 parent 8cc3aac commit edd3adf

File tree

2 files changed

+37
-31
lines changed

2 files changed

+37
-31
lines changed

packages/alpinejs/src/lifecycle.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,22 @@ let initInterceptors = []
8282

8383
export function interceptInit(callback) { initInterceptors.push(callback) }
8484

85+
let markerDispenser = 1
86+
8587
export function initTree(el, walker = walk, intercept = () => {}) {
86-
// Don't init a tree within a parent that is being ignored.
88+
// Don't init a tree within a parent that is being ignored...
8789
if (findClosest(el, i => i._x_ignore)) return
8890

8991
deferHandlingDirectives(() => {
9092
walker(el, (el, skip) => {
93+
// If the element has a marker, it's already been initialized...
94+
if (el._x_marker) return
95+
96+
// Add a marker to the element so we can tell if it's been initialized...
97+
// This is important so that we can prevent double-initialization of
98+
// elements that are moved around on the page.
99+
el._x_marker = markerDispenser++
100+
91101
intercept(el, skip)
92102

93103
initInterceptors.forEach(i => i(el, skip))
@@ -103,6 +113,7 @@ export function destroyTree(root, walker = walk) {
103113
walker(root, el => {
104114
cleanupElement(el)
105115
cleanupAttributes(el)
116+
delete el._x_marker
106117
})
107118
}
108119

packages/alpinejs/src/mutation.js

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,28 @@ function onMutate(mutations) {
118118
return
119119
}
120120

121-
let addedNodes = new Set
122-
let removedNodes = new Set
121+
let addedNodes = []
122+
let removedNodes = []
123123
let addedAttributes = new Map
124124
let removedAttributes = new Map
125125

126126
for (let i = 0; i < mutations.length; i++) {
127127
if (mutations[i].target._x_ignoreMutationObserver) continue
128128

129129
if (mutations[i].type === 'childList') {
130-
mutations[i].addedNodes.forEach(node => node.nodeType === 1 && addedNodes.add(node))
131-
mutations[i].removedNodes.forEach(node => node.nodeType === 1 && removedNodes.add(node))
130+
mutations[i].removedNodes.forEach(node => {
131+
if (node.nodeType !== 1) return
132+
if (! node._x_marker) return
133+
134+
removedNodes.push(node)
135+
})
136+
137+
mutations[i].addedNodes.forEach(node => {
138+
if (node.nodeType !== 1) return
139+
if (node._x_marker) return
140+
141+
addedNodes.push(node)
142+
})
132143
}
133144

134145
if (mutations[i].type === 'attributes') {
@@ -170,42 +181,26 @@ function onMutate(mutations) {
170181
onAttributeAddeds.forEach(i => i(el, attrs))
171182
})
172183

184+
// There are two special scenarios we need to account for when using the mutation
185+
// observer to init and destroy elements. First, when a node is "moved" on the page,
186+
// it's registered as both an "add" and a "remove", so we want to skip those.
187+
// (This is handled above by the ._x_marker conditionals...)
188+
// Second, when a node is "wrapped", it gets registered as a "removal" and the wrapper
189+
// as an "addition". We don't want to remove, then re-initialize the node, so we look
190+
// and see if it's inside any added nodes (wrappers) and skip it.
191+
// (This is handled below by the .contains conditional...)
192+
173193
for (let node of removedNodes) {
174-
// If an element gets moved on a page, it's registered
175-
// as both an "add" and "remove", so we want to skip those.
176-
if (addedNodes.has(node)) continue
194+
if (addedNodes.some(i => i.contains(node))) continue
177195

178196
onElRemoveds.forEach(i => i(node))
179197
}
180198

181-
// Mutations are bundled together by the browser but sometimes
182-
// for complex cases, there may be javascript code adding a wrapper
183-
// and then an alpine component as a child of that wrapper in the same
184-
// function and the mutation observer will receive 2 different mutations.
185-
// when it comes time to run them, the dom contains both changes so the child
186-
// element would be processed twice as Alpine calls initTree on
187-
// both mutations. We mark all nodes as _x_ignored and only remove the flag
188-
// when processing the node to avoid those duplicates.
189-
addedNodes.forEach((node) => {
190-
node._x_ignoreSelf = true
191-
node._x_ignore = true
192-
})
193199
for (let node of addedNodes) {
194-
// If the node was eventually removed as part of one of his
195-
// parent mutations, skip it
196-
if (removedNodes.has(node)) continue
197200
if (! node.isConnected) continue
198201

199-
delete node._x_ignoreSelf
200-
delete node._x_ignore
201202
onElAddeds.forEach(i => i(node))
202-
node._x_ignore = true
203-
node._x_ignoreSelf = true
204203
}
205-
addedNodes.forEach((node) => {
206-
delete node._x_ignoreSelf
207-
delete node._x_ignore
208-
})
209204

210205
addedNodes = null
211206
removedNodes = null

0 commit comments

Comments
 (0)