tangled
alpha
login
or
join now
yippee.fun
/
morphlex
0
fork
atom
Precise DOM morphing
morphing
typescript
dom
0
fork
atom
overview
issues
pulls
pipelines
Use specific operations for better performance
joel.drapper.me
4 months ago
36ccc84e
7f3af345
+71
-28
2 changed files
expand all
collapse all
unified
split
benchmark
index.html
src
morphlex.ts
+9
-1
benchmark/index.html
···
383
383
import morphdom from "https://unpkg.com/morphdom@2.7.7/dist/morphdom-esm.js"
384
384
// Try loading nanomorph from jsdelivr with ESM
385
385
import nanomorph from "https://cdn.jsdelivr.net/npm/nanomorph@5.4.3/+esm"
386
386
-
// Alpine morph requires Alpine.js to be loaded, so we'll skip it
386
386
+
// Load Alpine.js and Alpine morph
387
387
+
import Alpine from "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/module.esm.js"
388
388
+
import Morph from "https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/module.esm.js"
389
389
+
390
390
+
// Initialize Alpine with morph plugin
391
391
+
Alpine.plugin(Morph)
392
392
+
window.Alpine = Alpine
393
393
+
Alpine.start()
387
394
388
395
const sandbox = document.getElementById("sandbox")
389
396
let isRunning = false
···
558
565
{ name: "idiomorph", fn: (from, to) => Idiomorph.morph(from, to) },
559
566
{ name: "morphdom", fn: (from, to) => morphdom(from, to.cloneNode(true)) },
560
567
{ name: "nanomorph", fn: (from, to) => nanomorph(from, to.cloneNode(true)) },
568
568
+
{ name: "alpine-morph", fn: (from, to) => Alpine.morph(from, to) },
561
569
]
562
570
563
571
for (const lib of libraries) {
+62
-27
src/morphlex.ts
···
3
3
const TEXT_NODE_TYPE = 3
4
4
const PARENT_NODE_TYPES = [false, true, false, false, false, false, false, false, false, true, false, true]
5
5
6
6
+
const Operation = {
7
7
+
EqualNode: 0,
8
8
+
SameElement: 1,
9
9
+
SameNode: 2,
10
10
+
} as const
11
11
+
12
12
+
type Operation = (typeof Operation)[keyof typeof Operation]
13
13
+
6
14
const candidateNodes: Set<number> = new Set()
7
15
const candidateElements: Set<number> = new Set()
8
16
const unmatchedNodes: Set<number> = new Set()
···
322
330
if (from === to) return
323
331
if (from.isEqualNode?.(to)) return
324
332
325
325
-
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
326
326
-
327
333
if (from.nodeType === ELEMENT_NODE_TYPE && to.nodeType === ELEMENT_NODE_TYPE) {
328
334
if ((from as Element).localName === (to as Element).localName) {
329
335
this.#morphMatchingElements(from as Element, to as Element)
···
333
339
} else {
334
340
this.#morphOtherNode(from, to)
335
341
}
336
336
-
337
337
-
this.#options.afterNodeVisited?.(from, to)
338
342
}
339
343
340
344
#morphMatchingElements(from: Element, to: Element): void {
345
345
+
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
346
346
+
341
347
if (from.hasAttributes() || to.hasAttributes()) {
342
348
this.#visitAttributes(from, to)
343
349
}
···
347
353
} else if (from.hasChildNodes() || to.hasChildNodes()) {
348
354
this.visitChildNodes(from, to)
349
355
}
356
356
+
357
357
+
this.#options.afterNodeVisited?.(from, to)
350
358
}
351
359
352
360
#morphNonMatchingElements(from: Element, to: Element): void {
361
361
+
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
362
362
+
353
363
this.#replaceNode(from, to)
364
364
+
365
365
+
this.#options.afterNodeVisited?.(from, to)
354
366
}
355
367
356
368
#morphOtherNode(from: ChildNode, to: ChildNode): void {
369
369
+
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
370
370
+
357
371
if (from.nodeType === to.nodeType && from.nodeValue !== null && to.nodeValue !== null) {
358
372
from.nodeValue = to.nodeValue
359
373
} else {
360
374
this.#replaceNode(from, to)
361
375
}
376
376
+
377
377
+
this.#options.afterNodeVisited?.(from, to)
362
378
}
363
379
364
380
#visitAttributes(from: Element, to: Element): void {
···
454
470
unmatchedElements.clear()
455
471
whitespaceNodes.clear()
456
472
457
457
-
const seq: Array<number | undefined> = []
458
458
-
const matches: Array<number | undefined> = []
473
473
+
const seq: Array<number> = []
474
474
+
const matches: Array<number> = []
475
475
+
const op: Array<Operation> = []
459
476
460
477
for (let i = 0; i < fromChildNodes.length; i++) {
461
478
const candidate = fromChildNodes[i]!
···
492
509
493
510
if (candidate.isEqualNode(element)) {
494
511
matches[unmatchedIndex] = candidateIndex
512
512
+
op[unmatchedIndex] = Operation.EqualNode
495
513
seq[candidateIndex] = unmatchedIndex
496
514
candidateElements.delete(candidateIndex)
497
515
unmatchedElements.delete(unmatchedIndex)
···
512
530
candidateLoop: for (const candidateIndex of candidateElements) {
513
531
const candidate = fromChildNodes[candidateIndex] as Element
514
532
515
515
-
// Match by exact id
516
516
-
if (id !== "" && element.localName === candidate.localName && id === candidate.id) {
517
517
-
matches[unmatchedIndex] = candidateIndex
518
518
-
seq[candidateIndex] = unmatchedIndex
519
519
-
candidateElements.delete(candidateIndex)
520
520
-
unmatchedElements.delete(unmatchedIndex)
521
521
-
break candidateLoop
522
522
-
}
533
533
+
if (element.localName === candidate.localName) {
534
534
+
// Match by exact id
535
535
+
if (id !== "" && id === candidate.id) {
536
536
+
matches[unmatchedIndex] = candidateIndex
537
537
+
op[unmatchedIndex] = Operation.SameElement
538
538
+
seq[candidateIndex] = unmatchedIndex
539
539
+
candidateElements.delete(candidateIndex)
540
540
+
unmatchedElements.delete(unmatchedIndex)
541
541
+
break candidateLoop
542
542
+
}
523
543
524
524
-
// Match by idArray (to) against idSet (from)
525
525
-
if (idArray) {
526
526
-
const candidateIdSet = this.#idSetMap.get(candidate)
527
527
-
if (candidateIdSet) {
528
528
-
for (let i = 0; i < idArray.length; i++) {
529
529
-
const arrayId = idArray[i]!
530
530
-
if (candidateIdSet.has(arrayId)) {
531
531
-
matches[unmatchedIndex] = candidateIndex
532
532
-
seq[candidateIndex] = unmatchedIndex
533
533
-
candidateElements.delete(candidateIndex)
534
534
-
unmatchedElements.delete(unmatchedIndex)
535
535
-
break candidateLoop
544
544
+
// Match by idArray (to) against idSet (from)
545
545
+
if (idArray) {
546
546
+
const candidateIdSet = this.#idSetMap.get(candidate)
547
547
+
if (candidateIdSet) {
548
548
+
for (let i = 0; i < idArray.length; i++) {
549
549
+
const arrayId = idArray[i]!
550
550
+
if (candidateIdSet.has(arrayId)) {
551
551
+
matches[unmatchedIndex] = candidateIndex
552
552
+
op[unmatchedIndex] = Operation.SameElement
553
553
+
seq[candidateIndex] = unmatchedIndex
554
554
+
candidateElements.delete(candidateIndex)
555
555
+
unmatchedElements.delete(unmatchedIndex)
556
556
+
break candidateLoop
557
557
+
}
536
558
}
537
559
}
538
560
}
···
558
580
) {
559
581
matches[unmatchedIndex] = candidateIndex
560
582
seq[candidateIndex] = unmatchedIndex
583
583
+
op[unmatchedIndex] = Operation.SameElement
561
584
candidateElements.delete(candidateIndex)
562
585
unmatchedElements.delete(unmatchedIndex)
563
586
break
···
580
603
}
581
604
matches[unmatchedIndex] = candidateIndex
582
605
seq[candidateIndex] = unmatchedIndex
606
606
+
op[unmatchedIndex] = Operation.SameElement
583
607
candidateElements.delete(candidateIndex)
584
608
unmatchedElements.delete(unmatchedIndex)
585
609
break
···
595
619
const candidate = fromChildNodes[candidateIndex]!
596
620
if (candidate.isEqualNode(node)) {
597
621
matches[unmatchedIndex] = candidateIndex
622
622
+
op[unmatchedIndex] = Operation.EqualNode
598
623
seq[candidateIndex] = unmatchedIndex
599
624
candidateNodes.delete(candidateIndex)
600
625
unmatchedNodes.delete(unmatchedIndex)
···
613
638
const candidate = fromChildNodes[candidateIndex]!
614
639
if (nodeType === candidate.nodeType) {
615
640
matches[unmatchedIndex] = candidateIndex
641
641
+
op[unmatchedIndex] = Operation.SameNode
616
642
seq[candidateIndex] = unmatchedIndex
617
643
candidateNodes.delete(candidateIndex)
618
644
unmatchedNodes.delete(unmatchedIndex)
···
641
667
const matchInd = matches[i]
642
668
if (matchInd !== undefined) {
643
669
const match = fromChildNodes[matchInd]!
670
670
+
const operation = op[matchInd]!
644
671
645
672
if (!shouldNotMove[matchInd]) {
646
673
moveBefore(parent, match, insertionPoint)
647
674
}
648
648
-
this.#morphOneToOne(match, node)
675
675
+
676
676
+
if (operation === Operation.EqualNode) {
677
677
+
} else if (operation === Operation.SameElement) {
678
678
+
// this.#morphMatchingElements(match as Element, node as Element)
679
679
+
this.#morphMatchingElements(match as Element, node as Element)
680
680
+
} else {
681
681
+
this.#morphOneToOne(match, node)
682
682
+
}
683
683
+
649
684
insertionPoint = match.nextSibling
650
685
} else {
651
686
if (this.#options.beforeNodeAdded?.(parent, node, insertionPoint) ?? true) {