Skip to content

Commit

Permalink
perf(hdom): update diffTree(), inline node type checks
Browse files Browse the repository at this point in the history
- add child tracking index table template for diffTree()
- bail out early if edit distance = 2 and only attribs changed
- update checks in normalizeTree(), normalizeElement(), createDOM(),
  hydrateDOM(), setAttrib()
- rename releaseDeep() => releaseTree()
-
  • Loading branch information
postspectacular committed Sep 16, 2018
1 parent 224a537 commit 382c45c
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 59 deletions.
78 changes: 46 additions & 32 deletions packages/hdom/src/diff.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
import { IObjectOf, SEMAPHORE } from "@thi.ng/api/api";
import * as isa from "@thi.ng/checks/is-array";
import * as iss from "@thi.ng/checks/is-string";
import { DiffLogEntry } from "@thi.ng/diff/api";
import { diffArray } from "@thi.ng/diff/array";
import { diffObject } from "@thi.ng/diff/object";
import { equivArrayLike, equivObject, equivMap, equivSet } from "@thi.ng/equiv";
import { HDOMImplementation, HDOMOpts } from "./api";

const isArray = isa.isArray;
const isString = iss.isString;

const max = Math.max;

// child index tracking template buffer
const INDEX = (() => {
const res = new Array(2048);
for (let i = 2, n = res.length; i < n; i++) {
res[i] = i - 2;
}
return res;
})();

const buildIndex = (n: number) => {
if (n <= INDEX.length) {
return INDEX.slice(0, n);
}
const res = new Array(n);
while (--n >= 2) {
res[n] = n - 2;
}
return res;
};

/**
* Takes a DOM root element and two normalized hiccup trees, `prev` and
* `curr`. Recursively computes diff between both trees and applies any
Expand Down Expand Up @@ -46,7 +63,7 @@ export const diffTree = <T>(
const attribs = curr[1];
// always replace element if __diff = false
if (attribs.__diff === false) {
releaseDeep(prev);
releaseTree(prev);
impl.replaceChild(opts, parent, child, curr);
return;
}
Expand All @@ -70,32 +87,30 @@ export const diffTree = <T>(
let val: any;
if (edits[0][0] !== 0 || prev[1].key !== attribs.key) {
// DEBUG && console.log("replace:", prev, curr);
releaseDeep(prev);
releaseTree(prev);
impl.replaceChild(opts, parent, child, curr);
return;
}
if ((val = (<any>prev).__release) && val !== (<any>curr).__release) {
releaseDeep(prev);
releaseTree(prev);
}
if (edits[1][0] !== 0) {
diffAttributes(impl, el, prev[1], curr[1]);
// if attribs changed & distance == 2 then we're done here...
if (delta.distance === 2) {
return;
}
}
const equivKeys = extractEquivElements(edits);
const numEdits = edits.length;
const prevLength = prev.length - 1;
const offsets = new Array(prevLength + 1);
for (i = prevLength; i >= 2; i--) {
offsets[i] = i - 2;
}
const equivKeys = extractEquivElements(edits);
const offsets = buildIndex(prevLength + 1);
for (i = 2; i < numEdits; i++) {
e = edits[i];
status = e[0];
val = e[2];

// DEBUG && console.log(`edit: o:[${offsets.toString()}] i:${idx} s:${status}`, val);

// element removed?
if (status === -1) {
// element removed / edited?
val = e[2];
if (isArray(val)) {
k = val[1].key;
if (k !== undefined && equivKeys[k][2] !== undefined) {
Expand All @@ -106,19 +121,19 @@ export const diffTree = <T>(
} else {
idx = e[1];
// DEBUG && console.log("remove @", offsets[idx], val);
releaseDeep(val);
releaseTree(val);
impl.removeChild(el, offsets[idx]);
for (j = prevLength; j >= idx; j--) {
offsets[j] = max(offsets[j] - 1, 0);
}
}
} else if (isString(val)) {
} else if (typeof val === "string") {
impl.setContent(el, "");
}

// element added/inserted?
} else if (status === 1) {
if (isString(val)) {
// element added/inserted?
val = e[2];
if (typeof val === "string") {
impl.setContent(el, val);
} else if (isArray(val)) {
k = val[1].key;
Expand All @@ -135,16 +150,15 @@ export const diffTree = <T>(
}
// call __init after all children have been added/updated
if ((val = (<any>curr).__init) && val != (<any>prev).__init) {
// DEBUG && console.log("call __init", curr);
val.apply(curr, [el, ...((<any>curr).__args)]);
}
};

export const diffAttributes = <T>(impl: HDOMImplementation<T>, el: T, prev: any, curr: any) => {
let i, e, edits;
const delta = diffObject(prev, curr, equiv);
impl.removeAttribs(el, delta.dels, prev);
let value = SEMAPHORE;
let val = SEMAPHORE;
let i, e, edits;
for (edits = delta.edits, i = edits.length; --i >= 0;) {
e = edits[i];
const a = e[0];
Expand All @@ -154,23 +168,23 @@ export const diffAttributes = <T>(impl: HDOMImplementation<T>, el: T, prev: any,
if (a !== "value") {
impl.setAttrib(el, a, e[1], curr);
} else {
value = e[1];
val = e[1];
}
}
for (edits = delta.adds, i = edits.length; --i >= 0;) {
e = edits[i];
if (e !== "value") {
impl.setAttrib(el, e, curr[e], curr);
} else {
value = curr[e];
val = curr[e];
}
}
if (value !== SEMAPHORE) {
impl.setAttrib(el, "value", value, curr);
if (val !== SEMAPHORE) {
impl.setAttrib(el, "value", val, curr);
}
};

export const releaseDeep = (tag: any) => {
export const releaseTree = (tag: any) => {
if (isArray(tag)) {
if (tag[1] && tag[1].__release === false) {
return;
Expand All @@ -181,7 +195,7 @@ export const releaseDeep = (tag: any) => {
delete (<any>tag).__release;
}
for (let i = tag.length; --i >= 2;) {
releaseDeep(tag[i]);
releaseTree(tag[i]);
}
}
};
Expand Down Expand Up @@ -216,7 +230,7 @@ const OBJP = Object.getPrototypeOf({});
* @param b
*/
export const equiv = (a: any, b: any): boolean => {
let proto;
let proto: any;
if (a === b) {
return true;
}
Expand Down Expand Up @@ -260,4 +274,4 @@ export const equiv = (a: any, b: any): boolean => {
}
// NaN
return (a !== a && b !== b);
};
};
20 changes: 8 additions & 12 deletions packages/hdom/src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import * as isa from "@thi.ng/checks/is-array";
import * as isf from "@thi.ng/checks/is-function";
import * as isi from "@thi.ng/checks/is-not-string-iterable";
import * as iss from "@thi.ng/checks/is-string";
import { SVG_NS, SVG_TAGS } from "@thi.ng/hiccup/api";
import { css } from "@thi.ng/hiccup/css";
import { HDOMImplementation, HDOMOpts } from "./api";

const isArray = isa.isArray;
const isFunction = isf.isFunction;
const isNotStringAndIterable = isi.isNotStringAndIterable
const isString = iss.isString;

/**
* Creates an actual DOM tree from given hiccup component and `parent`
Expand All @@ -27,7 +23,7 @@ const isString = iss.isString;
export const createDOM = (opts: Partial<HDOMOpts>, parent: Element, tree: any, insert?: number) => {
if (isArray(tree)) {
const tag = tree[0];
if (isFunction(tag)) {
if (typeof tag === "function") {
return createDOM(opts, parent, tag.apply(null, [opts.ctx, ...tree.slice(1)]), insert);
}
const attribs = tree[1];
Expand Down Expand Up @@ -75,7 +71,7 @@ export const createDOM = (opts: Partial<HDOMOpts>, parent: Element, tree: any, i
export const hydrateDOM = (opts: Partial<HDOMOpts>, parent: Element, tree: any, i = 0) => {
if (isArray(tree)) {
const el = parent.children[i];
if (isFunction(tree[0])) {
if (typeof tree[0] === "function") {
hydrateDOM(opts, parent, tree[0].apply(null, [opts.ctx, ...tree.slice(1)]), i);
}
const attribs = tree[1];
Expand Down Expand Up @@ -180,7 +176,7 @@ export const setAttribs = (el: Element, attribs: any) => {
export const setAttrib = (el: Element, id: string, val: any, attribs?: any) => {
if (id.startsWith("__")) return;
const isListener = id.indexOf("on") === 0;
if (!isListener && isFunction(val)) {
if (!isListener && typeof val === "function") {
val = val(attribs);
}
if (val !== undefined && val !== false) {
Expand Down Expand Up @@ -217,11 +213,11 @@ export const updateValueAttrib = (el: HTMLInputElement, v: any) => {
case "url":
case "tel":
case "search":
if (el.value !== undefined && isString(v)) {
const e = el as HTMLInputElement;
const off = v.length - (e.value.length - e.selectionStart);
e.value = v;
e.selectionStart = e.selectionEnd = off;
if (el.value !== undefined && typeof v === "string") {
const off = v.length - (el.value.length - el.selectionStart);
el.value = v;
el.selectionStart = el.selectionEnd = off;
break;
}
default:
el.value = v;
Expand Down
24 changes: 9 additions & 15 deletions packages/hdom/src/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import * as impf from "@thi.ng/checks/implements-function";
import * as isa from "@thi.ng/checks/is-array";
import * as isf from "@thi.ng/checks/is-function";
import * as isi from "@thi.ng/checks/is-iterable";
import * as insi from "@thi.ng/checks/is-not-string-iterable";
import * as iso from "@thi.ng/checks/is-plain-object";
import * as iss from "@thi.ng/checks/is-string";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
import { NO_SPANS, TAG_REGEXP } from "@thi.ng/hiccup/api";
import { HDOMOpts } from "./api";

const isArray = isa.isArray;
const isFunction = isf.isFunction;
const implementsFunction = impf.implementsFunction;
const isIterable = isi.isIterable
const isNotStringAndIterable = insi.isNotStringAndIterable;
const isPlainObject = iso.isPlainObject;
const isString = iss.isString;

/**
* Expands single hiccup element/component into its canonical form:
Expand Down Expand Up @@ -41,7 +35,7 @@ const isString = iss.isString;
*/
export const normalizeElement = (spec: any[], keys: boolean) => {
let tag = spec[0], hasAttribs = isPlainObject(spec[1]), match, id, clazz, attribs;
if (!isString(tag) || !(match = TAG_REGEXP.exec(tag))) {
if (typeof tag !== "string" || !(match = TAG_REGEXP.exec(tag))) {
illegalArgs(`${tag} is not a valid tag name`);
}
// return orig if already normalized and satisfies key requirement
Expand Down Expand Up @@ -122,12 +116,12 @@ const _normalizeTree = (tree: any, opts: Partial<HDOMOpts>, ctx: any, path: numb
const tag = tree[0];
// use result of function call
// pass ctx as first arg and remaining array elements as rest args
if (isFunction(tag)) {
if (typeof tag === "function") {
return _normalizeTree(tag.apply(null, [ctx, ...tree.slice(1)]), opts, ctx, path, keys, span);
}
// component object w/ life cycle methods
// (render() is the only required hook)
if (implementsFunction(tag, "render")) {
if (typeof tag.render === "function") {
const args = [ctx, ...tree.slice(1)];
norm = _normalizeTree(tag.render.apply(tag, args), opts, ctx, path, keys, span);
if (isArray(norm)) {
Expand All @@ -154,7 +148,7 @@ const _normalizeTree = (tree: any, opts: Partial<HDOMOpts>, ctx: any, path: numb
let el = norm[i];
if (el != null) {
const isarray = isArray(el);
if ((isarray && isArray(el[0])) || (!isarray && !isString(el) && isIterable(el))) {
if ((isarray && isArray(el[0])) || (!isarray && isNotStringAndIterable(el))) {
for (let c of el) {
c = _normalizeTree(c, opts, ctx, path.concat(k), keys, span);
if (c !== undefined) {
Expand All @@ -175,13 +169,13 @@ const _normalizeTree = (tree: any, opts: Partial<HDOMOpts>, ctx: any, path: numb
}
return norm;
}
if (isFunction(tree)) {
if (typeof tree === "function") {
return _normalizeTree(tree(ctx), opts, ctx, path, keys, span);
}
if (implementsFunction(tree, "toHiccup")) {
if (typeof tree.toHiccup === "function") {
return _normalizeTree(tree.toHiccup(opts.ctx), opts, ctx, path, keys, span);
}
if (implementsFunction(tree, "deref")) {
if (typeof tree.deref === "function") {
return _normalizeTree(tree.deref(), opts, ctx, path, keys, span);
}
return span ?
Expand Down

0 comments on commit 382c45c

Please sign in to comment.