Improved diffs

This commit is contained in:
Eduard Urbach 2018-03-14 03:08:50 +01:00
parent a6390e5eaf
commit 1931d681f7
2 changed files with 79 additions and 25 deletions

View File

@ -1,3 +1,5 @@
import { MutationQueue, CustomMutationQueue } from "./MutationQueue"
export class Diff { export class Diff {
static persistentClasses = new Set<string>() static persistentClasses = new Set<string>()
static persistentAttributes = new Set<string>() static persistentAttributes = new Set<string>()
@ -5,6 +7,7 @@ export class Diff {
// Reuse container for diffs to avoid memory allocation // Reuse container for diffs to avoid memory allocation
static container: HTMLElement static container: HTMLElement
static rootContainer: HTMLElement static rootContainer: HTMLElement
static mutations: CustomMutationQueue = new CustomMutationQueue()
// innerHTML will diff the element with the given HTML string and apply DOM mutations. // innerHTML will diff the element with the given HTML string and apply DOM mutations.
static innerHTML(aRoot: HTMLElement, html: string): Promise<void> { static innerHTML(aRoot: HTMLElement, html: string): Promise<void> {
@ -15,10 +18,8 @@ export class Diff {
Diff.container.innerHTML = html Diff.container.innerHTML = html
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
window.requestAnimationFrame(() => { Diff.childNodes(aRoot, Diff.container)
Diff.childNodes(aRoot, Diff.container) this.mutations.wait(resolve)
resolve()
})
}) })
} }
@ -31,10 +32,8 @@ export class Diff {
Diff.rootContainer.innerHTML = html.replace("<!DOCTYPE html>", "") Diff.rootContainer.innerHTML = html.replace("<!DOCTYPE html>", "")
window.requestAnimationFrame(() => { Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.rootContainer.getElementsByTagName("body")[0])
Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.rootContainer.getElementsByTagName("body")[0]) this.mutations.wait(resolve)
resolve()
})
}) })
} }
@ -49,7 +48,7 @@ export class Diff {
// Remove nodes at the end of a that do not exist in b // Remove nodes at the end of a that do not exist in b
if(i >= bChild.length) { if(i >= bChild.length) {
aRoot.removeChild(a) this.mutations.queue(() => aRoot.removeChild(a))
continue continue
} }
@ -57,13 +56,13 @@ export class Diff {
// If a doesn't have that many nodes, simply append at the end of a // If a doesn't have that many nodes, simply append at the end of a
if(i >= aChild.length) { if(i >= aChild.length) {
aRoot.appendChild(b) this.mutations.queue(() => aRoot.appendChild(b))
continue continue
} }
// If it's a completely different HTML tag or node type, replace it // If it's a completely different HTML tag or node type, replace it
if(a.nodeName !== b.nodeName || a.nodeType !== b.nodeType) { if(a.nodeName !== b.nodeName || a.nodeType !== b.nodeType) {
aRoot.replaceChild(b, a) this.mutations.queue(() => aRoot.replaceChild(b, a))
continue continue
} }
@ -71,7 +70,7 @@ export class Diff {
// We don't need to check for b to be a text node as well because // We don't need to check for b to be a text node as well because
// we eliminated different node types in the previous condition. // we eliminated different node types in the previous condition.
if(a.nodeType === Node.TEXT_NODE) { if(a.nodeType === Node.TEXT_NODE) {
a.textContent = b.textContent this.mutations.queue(() => a.textContent = b.textContent)
continue continue
} }
@ -92,9 +91,11 @@ export class Diff {
} }
} }
for(let attr of removeAttributes) { this.mutations.queue(() => {
elemA.removeAttributeNode(attr) for(let attr of removeAttributes) {
} elemA.removeAttributeNode(attr)
}
})
for(let x = 0; x < elemB.attributes.length; x++) { for(let x = 0; x < elemB.attributes.length; x++) {
let attrib = elemB.attributes[x] let attrib = elemB.attributes[x]
@ -119,25 +120,29 @@ export class Diff {
} }
} }
for(let className of removeClasses) { this.mutations.queue(() => {
classesA.remove(className) for(let className of removeClasses) {
} classesA.remove(className)
for(let className of classesB) {
if(!classesA.contains(className)) {
classesA.add(className)
} }
}
for(let className of classesB) {
if(!classesA.contains(className)) {
classesA.add(className)
}
}
})
continue continue
} }
elemA.setAttribute(attrib.name, attrib.value) this.mutations.queue(() => elemA.setAttribute(attrib.name, attrib.value))
} }
// Special case: Apply state of input elements // Special case: Apply state of input elements
if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) { if(elemA !== document.activeElement && elemA instanceof HTMLInputElement && elemB instanceof HTMLInputElement) {
elemA.value = elemB.value this.mutations.queue(() => {
(elemA as HTMLInputElement).value = (elemB as HTMLInputElement).value
})
} }
} }

View File

@ -27,3 +27,52 @@ export class MutationQueue {
this.elements.length = 0 this.elements.length = 0
} }
} }
export class CustomMutationQueue {
mutations: Array<() => void>
onClearCallBack: () => void
timeCapacity = 6.5
constructor() {
this.mutations = []
}
queue(mutation: () => void) {
this.mutations.push(mutation)
if(this.mutations.length === 1) {
window.requestAnimationFrame(() => this.mutateAll())
}
}
mutateAll() {
let start = performance.now()
for(let i = 0; i < this.mutations.length; i++) {
if(performance.now() - start > this.timeCapacity) {
let end = performance.now()
// console.log(i, "mutations in", performance.now() - start, "ms")
this.mutations = this.mutations.slice(i)
window.requestAnimationFrame(() => this.mutateAll())
return
}
this.mutations[i]()
}
this.clear()
}
clear() {
this.mutations.length = 0
if(this.onClearCallBack) {
this.onClearCallBack()
this.onClearCallBack = null
}
}
wait(callBack: () => void) {
this.onClearCallBack = callBack
}
}