From 1931d681f75bfb605cd87db4a85932b61b4866e8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 14 Mar 2018 03:08:50 +0100 Subject: [PATCH] Improved diffs --- scripts/Diff.ts | 55 ++++++++++++++++++++++------------------ scripts/MutationQueue.ts | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 25 deletions(-) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 8f0bd397..ff36ae05 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -1,3 +1,5 @@ +import { MutationQueue, CustomMutationQueue } from "./MutationQueue" + export class Diff { static persistentClasses = new Set() static persistentAttributes = new Set() @@ -5,6 +7,7 @@ export class Diff { // Reuse container for diffs to avoid memory allocation static container: HTMLElement static rootContainer: HTMLElement + static mutations: CustomMutationQueue = new CustomMutationQueue() // innerHTML will diff the element with the given HTML string and apply DOM mutations. static innerHTML(aRoot: HTMLElement, html: string): Promise { @@ -15,10 +18,8 @@ export class Diff { Diff.container.innerHTML = html return new Promise((resolve, reject) => { - window.requestAnimationFrame(() => { - Diff.childNodes(aRoot, Diff.container) - resolve() - }) + Diff.childNodes(aRoot, Diff.container) + this.mutations.wait(resolve) }) } @@ -31,10 +32,8 @@ export class Diff { Diff.rootContainer.innerHTML = html.replace("", "") - window.requestAnimationFrame(() => { - Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.rootContainer.getElementsByTagName("body")[0]) - resolve() - }) + Diff.childNodes(aRoot.getElementsByTagName("body")[0], Diff.rootContainer.getElementsByTagName("body")[0]) + this.mutations.wait(resolve) }) } @@ -49,7 +48,7 @@ export class Diff { // Remove nodes at the end of a that do not exist in b if(i >= bChild.length) { - aRoot.removeChild(a) + this.mutations.queue(() => aRoot.removeChild(a)) continue } @@ -57,13 +56,13 @@ export class Diff { // If a doesn't have that many nodes, simply append at the end of a if(i >= aChild.length) { - aRoot.appendChild(b) + this.mutations.queue(() => aRoot.appendChild(b)) continue } // If it's a completely different HTML tag or node type, replace it if(a.nodeName !== b.nodeName || a.nodeType !== b.nodeType) { - aRoot.replaceChild(b, a) + this.mutations.queue(() => aRoot.replaceChild(b, a)) 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 eliminated different node types in the previous condition. if(a.nodeType === Node.TEXT_NODE) { - a.textContent = b.textContent + this.mutations.queue(() => a.textContent = b.textContent) continue } @@ -92,9 +91,11 @@ export class Diff { } } - for(let attr of removeAttributes) { - elemA.removeAttributeNode(attr) - } + this.mutations.queue(() => { + for(let attr of removeAttributes) { + elemA.removeAttributeNode(attr) + } + }) for(let x = 0; x < elemB.attributes.length; x++) { let attrib = elemB.attributes[x] @@ -119,25 +120,29 @@ export class Diff { } } - for(let className of removeClasses) { - classesA.remove(className) - } - - for(let className of classesB) { - if(!classesA.contains(className)) { - classesA.add(className) + this.mutations.queue(() => { + for(let className of removeClasses) { + classesA.remove(className) } - } + + for(let className of classesB) { + if(!classesA.contains(className)) { + classesA.add(className) + } + } + }) continue } - elemA.setAttribute(attrib.name, attrib.value) + this.mutations.queue(() => elemA.setAttribute(attrib.name, attrib.value)) } // Special case: Apply state of input elements 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 + }) } } diff --git a/scripts/MutationQueue.ts b/scripts/MutationQueue.ts index b353ab59..bb0fd707 100644 --- a/scripts/MutationQueue.ts +++ b/scripts/MutationQueue.ts @@ -26,4 +26,53 @@ export class MutationQueue { clear() { 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 + } } \ No newline at end of file