Skip to content

Commit 7db9bed

Browse files
committed
Fix(Runtime): Runtime class name counter overflow caused styles to leak memory
1 parent a0ece13 commit 7db9bed

File tree

3 files changed

+32
-93
lines changed

3 files changed

+32
-93
lines changed

Diff for: packages/runtime/playground/index.html

+6-27
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,18 @@
11
<!DOCTYPE html>
2-
<html lang="en">
2+
<html lang="en" hidden>
33

44
<head>
55
<meta charset="UTF-8" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<style id="master">
8-
@layer base, theme, preset, components, general;
9-
10-
@layer general {
11-
.flex {
12-
display: flex
13-
}
14-
15-
.\@fade\|1s {
16-
animation: fade 1s
17-
}
18-
}
19-
20-
@keyframes fade {
21-
0% {
22-
opacity: 0
23-
}
24-
25-
to {
26-
opacity: 1
27-
}
28-
}
29-
</style>
307
<script type="module" src="src/main.ts"></script>
318
</head>
329

3310
<body>
34-
<button class="flex @fade|1s">button</button>
35-
<button class="flex @fade|1s">button</button>
36-
<button class="flex @fade|1s">button</button>
11+
<ul class="a">a
12+
<li class="b">b
13+
<ul class="c">c</ul>
14+
</li>
15+
</ul>
3716
</body>
3817

3918
</html>

Diff for: packages/runtime/playground/src/main.ts

+7-28
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,11 @@
11
import { initCSSRuntime } from '../../src'
22

3-
initCSSRuntime({
4-
selectors: {
5-
'::both': ['::before', '::after'],
6-
},
7-
components: {
8-
btn: 'bg:black block::both'
9-
}
10-
})
3+
initCSSRuntime()
114

12-
// const p1 = document.getElementById('p1') as HTMLHeadElement
5+
const a = document.getElementsByClassName('a')[0]
6+
const b = document.getElementsByClassName('b')[0]
137

14-
// const create = (name: string) => {
15-
// const el = document.createElement('div')
16-
// el.id = name
17-
// el.className = name
18-
// return el
19-
// }
20-
21-
// const p1c1 = create('p1c1')
22-
// const p1c2 = document.getElementById('p1c2') as HTMLHeadElement
23-
// const p1c3 = document.getElementById('p1c3') as HTMLHeadElement
24-
25-
// p1.remove()
26-
// p1.appendChild(p1c1)
27-
// p1c2.remove()
28-
// p1.appendChild(p1c2)
29-
// p1c2.classList.add('p1c2-1')
30-
// p1c3.remove()
31-
// p1c3.classList.add('p1c3-1')
32-
// document.body.appendChild(p1)
8+
a.remove()
9+
document.body.appendChild(a)
10+
b.remove()
11+
a.appendChild(b)

Diff for: packages/runtime/src/core.ts

+19-38
Original file line numberDiff line numberDiff line change
@@ -104,22 +104,14 @@ export default class CSSRuntime extends MasterCSS {
104104

105105
// @ts-expect-error readonly
106106
this.observer = new MutationObserver((mutationRecords) => {
107-
// console.clear()
108-
// const test = ''
109-
// if (test) {
110-
// console.log('')
111-
// console.log(`${test}: ${this.classCounts.get(test)}`)
112-
// }
113107
const eachClassCounts = new Map()
114108
const targetFirstAttrMutationRecord = new Map<Element, MutationRecord>()
115-
116109
const updateClassCount = (classes: Set<string> | string[] | DOMTokenList, isAdding = false) => {
117110
const usage = isAdding ? 1 : -1
118111
classes.forEach((className) => {
119112
eachClassCounts.set(className, (eachClassCounts.get(className) || 0) + usage)
120113
})
121114
}
122-
123115
const connectedStatusMap = new Map<Element, { change: number, mutationRecord: MutationRecord }>()
124116
const disconnectedStatusMap = new Map<Element, { change: number, mutationRecord: MutationRecord }>()
125117
const recordStatus = (target: Element, mutationRecord: MutationRecord, map: Map<Element, { change: number, mutationRecord: MutationRecord }>, adding: boolean) => {
@@ -130,8 +122,12 @@ export default class CSSRuntime extends MasterCSS {
130122
} else {
131123
map.set(target, { change: adding ? 1 : -1, mutationRecord })
132124
}
125+
for (const child of target.children) {
126+
if ('classList' in child) {
127+
recordStatus(child as Element, mutationRecord, map, adding)
128+
}
129+
}
133130
}
134-
135131
mutationRecords.forEach((mutationRecord) => {
136132
const target = mutationRecord.target as Element
137133
switch (mutationRecord.type) {
@@ -151,40 +147,27 @@ export default class CSSRuntime extends MasterCSS {
151147
break
152148
}
153149
})
154-
155-
const updatedTargetChangeMap = new Map<Element, number>()
156-
157150
const updateTarget = (target: Element, adding: boolean) => {
158-
const change = updatedTargetChangeMap.get(target) || 0
159-
const newChange = change + (adding ? 1 : -1)
160-
if (newChange >= -1 && newChange <= 1) {
161-
updatedTargetChangeMap.set(target, newChange)
162-
const firstAttrMutationRecord = targetFirstAttrMutationRecord.get(target)
151+
const firstAttrMutationRecord = targetFirstAttrMutationRecord.get(target)
152+
if (firstAttrMutationRecord) {
153+
targetFirstAttrMutationRecord.delete(target)
154+
}
155+
if (adding) {
156+
updateClassCount(target.classList, adding)
157+
} else {
163158
if (firstAttrMutationRecord) {
164-
targetFirstAttrMutationRecord.delete(target)
165-
}
166-
if (adding) {
167-
updateClassCount(target.classList, adding)
159+
updateClassCount(firstAttrMutationRecord.oldValue ? firstAttrMutationRecord.oldValue.split(/\s+/) : [], adding)
168160
} else {
169-
if (firstAttrMutationRecord) {
170-
updateClassCount(firstAttrMutationRecord.oldValue ? firstAttrMutationRecord.oldValue.split(/\s+/) : [], adding)
171-
} else {
172-
updateClassCount(target.classList, adding)
173-
}
174-
disconnectedStatusMap.forEach((disconnectedTargetStatus, disconnectedTarget) => {
175-
if (disconnectedTargetStatus.mutationRecord.target === target && disconnectedTargetStatus.change !== 0) {
176-
updateTarget(disconnectedTarget, disconnectedTargetStatus.change > 0)
177-
}
178-
})
179-
}
180-
for (const child of target.children) {
181-
updateTarget(child as Element, adding)
161+
updateClassCount(target.classList, adding)
182162
}
163+
disconnectedStatusMap.forEach((disconnectedTargetStatus, disconnectedTarget) => {
164+
if (disconnectedTargetStatus.mutationRecord.target === target && disconnectedTargetStatus.change !== 0) {
165+
updateTarget(disconnectedTarget, disconnectedTargetStatus.change > 0)
166+
}
167+
})
183168
}
184169
}
185-
186170
connectedStatusMap.forEach(({ change }, target) => change !== 0 && updateTarget(target, change > 0))
187-
188171
targetFirstAttrMutationRecord.forEach((mutation, target) => {
189172
if (!target.isConnected) return
190173
const oldClassList = mutation.oldValue ? mutation.oldValue.split(/\s+/) : []
@@ -195,11 +178,9 @@ export default class CSSRuntime extends MasterCSS {
195178
})
196179
const removedClasses = oldClassList.filter(c => !newClassList.contains(c))
197180
if (addedClasses.length) {
198-
// console.log('[attribute]', '[add] ', addedClasses, target)
199181
updateClassCount(addedClasses, true)
200182
}
201183
if (removedClasses.length) {
202-
// console.log('[attribute]', '[remove]', removedClasses, target)
203184
updateClassCount(removedClasses, false)
204185
}
205186
})

0 commit comments

Comments
 (0)