Skip to content

Commit 6ac9782

Browse files
ekwokacalebporzio
andauthored
🐛 Cleans up template directives memory (#4300)
* 🧪 Adds Failing Tests for Memory Cleanup * ✅ Cleans up x-if eagerly * 🧪 corrects x-for test failed for the wrong reasons * ✅ Cleans x-for tree * ♻️ Moves Effect dequeuing to Element Cleanup * formatting * formatting --------- Co-authored-by: Caleb Porzio <[email protected]>
1 parent f8eac72 commit 6ac9782

File tree

6 files changed

+99
-24
lines changed

6 files changed

+99
-24
lines changed

packages/alpinejs/src/directives/x-for.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { addScopeToNode } from '../scope'
22
import { evaluateLater } from '../evaluator'
33
import { directive } from '../directives'
44
import { reactive } from '../reactivity'
5-
import { initTree } from '../lifecycle'
5+
import { initTree, destroyTree } from '../lifecycle'
66
import { mutateDom } from '../mutation'
77
import { warn } from '../utils/warn'
8-
import { dequeueJob } from '../scheduler'
98
import { skipDuringClone } from '../clone'
109

1110
directive('for', (el, { expression }, { effect, cleanup }) => {
@@ -23,7 +22,13 @@ directive('for', (el, { expression }, { effect, cleanup }) => {
2322
effect(() => loop(el, iteratorNames, evaluateItems, evaluateKey))
2423

2524
cleanup(() => {
26-
Object.values(el._x_lookup).forEach(el => el.remove())
25+
Object.values(el._x_lookup).forEach(el =>
26+
mutateDom(() => {
27+
destroyTree(el)
28+
29+
el.remove()
30+
}
31+
))
2732

2833
delete el._x_prevKeys
2934
delete el._x_lookup
@@ -139,19 +144,18 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
139144
// for browser performance.
140145

141146
// We'll remove all the nodes that need to be removed,
142-
// letting the mutation observer pick them up and
143-
// clean up any side effects they had.
147+
// and clean up any side effects they had.
144148
for (let i = 0; i < removes.length; i++) {
145149
let key = removes[i]
146150

147-
// Remove any queued effects that might run after the DOM node has been removed.
148-
if (!! lookup[key]._x_effects) {
149-
lookup[key]._x_effects.forEach(dequeueJob)
150-
}
151+
if (! (key in lookup)) continue
151152

152-
lookup[key].remove()
153+
mutateDom(() => {
154+
destroyTree(lookup[key])
155+
156+
lookup[key].remove()
157+
})
153158

154-
lookup[key] = null
155159
delete lookup[key]
156160
}
157161

packages/alpinejs/src/directives/x-if.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { evaluateLater } from '../evaluator'
22
import { addScopeToNode } from '../scope'
33
import { directive } from '../directives'
4-
import { initTree } from '../lifecycle'
4+
import { initTree, destroyTree } from '../lifecycle'
55
import { mutateDom } from '../mutation'
6-
import { walk } from "../utils/walk"
7-
import { dequeueJob } from '../scheduler'
86
import { warn } from "../utils/warn"
97
import { skipDuringClone } from '../clone'
108

@@ -30,13 +28,11 @@ directive('if', (el, { expression }, { effect, cleanup }) => {
3028
el._x_currentIfEl = clone
3129

3230
el._x_undoIf = () => {
33-
walk(clone, (node) => {
34-
if (!!node._x_effects) {
35-
node._x_effects.forEach(dequeueJob)
36-
}
37-
})
31+
mutateDom(() => {
32+
destroyTree(clone)
3833

39-
clone.remove();
34+
clone.remove()
35+
})
4036

4137
delete el._x_currentIfEl
4238
}

packages/alpinejs/src/lifecycle.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ export function initTree(el, walker = walk, intercept = () => {}) {
9898

9999
export function destroyTree(root, walker = walk) {
100100
walker(root, el => {
101-
cleanupAttributes(el)
102101
cleanupElement(el)
102+
cleanupAttributes(el)
103103
})
104104
}
105105

packages/alpinejs/src/mutation.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { dequeueJob } from "./scheduler";
12
let onAttributeAddeds = []
23
let onElRemoveds = []
34
let onElAddeds = []
@@ -40,9 +41,9 @@ export function cleanupAttributes(el, names) {
4041
}
4142

4243
export function cleanupElement(el) {
43-
if (el._x_cleanups) {
44-
while (el._x_cleanups.length) el._x_cleanups.pop()()
45-
}
44+
el._x_effects?.forEach(dequeueJob)
45+
46+
while (el._x_cleanups?.length) el._x_cleanups.pop()()
4647
}
4748

4849
let observer = new MutationObserver(onMutate)

tests/cypress/integration/directives/x-for.spec.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,40 @@ test('x-for throws descriptive error when key is undefined',
605605
({ get }) => {},
606606
true
607607
)
608+
609+
// If x-for removes a child, all cleanups in the tree should be handled.
610+
test('x-for eagerly cleans tree',
611+
html`
612+
<div x-data="{ show: 0, counts: [0,0,0], items: [0,1,2] }">
613+
<button
614+
id="toggle"
615+
@click="show^=true"
616+
x-text="counts.reduce((a,b)=>a+b)">
617+
Toggle
618+
</button>
619+
<button id="remove" @click="items.pop()">Remove</button>
620+
<template x-for="num in items" :key="num">
621+
<div>
622+
<template x-for="n in show">
623+
<p x-effect="if (show) counts[num]++">hello</p>
624+
</template>
625+
</div>
626+
</template>
627+
</div>
628+
`,
629+
({ get }) => {
630+
get('#toggle').should(haveText('0'))
631+
get('#toggle').click()
632+
get('#toggle').should(haveText('3'))
633+
get('#toggle').click()
634+
get('#toggle').should(haveText('3'))
635+
get('#toggle').click()
636+
get('#toggle').should(haveText('6'))
637+
get('#remove').click()
638+
get('#toggle').should(haveText('6'))
639+
get('#toggle').click()
640+
get('#toggle').should(haveText('6'))
641+
get('#toggle').click()
642+
get('#toggle').should(haveText('8'))
643+
}
644+
)

tests/cypress/integration/directives/x-if.spec.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,40 @@ test('x-if removed dom does not attempt skipping already-processed reactive effe
110110
get('div#div-also-not-editing').should(exist())
111111
}
112112
)
113+
114+
// If x-if evaluates to false, all cleanups in the tree should be handled.
115+
test('x-if eagerly cleans tree',
116+
html`
117+
<div x-data="{ show: false, count: 0 }">
118+
<button @click="show^=true" x-text="count">Toggle</button>
119+
<template x-if="show">
120+
<div>
121+
<template x-if="true">
122+
<p x-effect="if (show) count++">
123+
hello
124+
</p>
125+
</template>
126+
</div>
127+
</template>
128+
</div>
129+
`,
130+
({ get }) => {
131+
get('button').should(haveText('0'))
132+
get('button').click()
133+
get('button').should(haveText('1'))
134+
get('button').click()
135+
get('button').should(haveText('1'))
136+
get('button').click()
137+
get('button').should(haveText('2'))
138+
get('button').click()
139+
get('button').should(haveText('2'))
140+
get('button').click()
141+
get('button').should(haveText('3'))
142+
get('button').click()
143+
get('button').should(haveText('3'))
144+
get('button').click()
145+
get('button').should(haveText('4'))
146+
get('button').click()
147+
get('button').should(haveText('4'))
148+
}
149+
)

0 commit comments

Comments
 (0)