sinuous
Version:
🧬 Small, fast, reactive render engine
348 lines (300 loc) • 8.53 kB
JavaScript
/* Adapted from Stage0 - The MIT License - Pavel Martynov */
/* Adapted from DOM Expressions - The MIT License - Ryan Carniato */
import { api } from './index.js';
export const GROUPING = '__g';
export const FORWARD = 'nextSibling';
export const BACKWARD = 'previousSibling';
/**
* Map over a list of items that create DOM nodes.
*
* @param {Function} items - Function or observable that creates a list.
* @param {Function} expr
* @param {boolean} [cleaning]
* @return {DocumentFragment}
*/
export function map(items, expr, cleaning) {
const { subscribe, root, sample, cleanup } = api;
// Disable cleaning for templates by default.
if (cleaning == null) cleaning = !expr.$t;
let parent = document.createDocumentFragment();
const beforeNode = add(parent, '');
const afterNode = add(parent, '');
const disposers = new Map();
const unsubscribe = subscribe((a) => {
const b = items();
return sample(() =>
reconcile(
a || [],
b || [],
beforeNode,
afterNode,
createFn,
cleaning && disposeAll,
cleaning && dispose
)
);
});
cleanup(unsubscribe);
cleanup(disposeAll);
function disposeAll() {
disposers.forEach((d) => d());
disposers.clear();
}
function dispose(node) {
let disposer = disposers.get(node);
disposer && disposer();
disposers.delete(node);
}
function createFn(parent, item, i, data, afterNode) {
// The root call makes it possible the child's computations outlive
// their parents' update cycle.
return cleaning
? root((disposeFn) => {
const node = add(parent, expr(item, i, data), afterNode);
disposers.set(node, disposeFn);
return node;
})
: add(parent, expr(item, i, data), afterNode);
}
return parent;
}
// This is almost straightforward implementation of reconcillation algorithm
// based on ivi documentation:
// https://github.com/localvoid/ivi/blob/2c81ead934b9128e092cc2a5ef2d3cabc73cb5dd/packages/ivi/src/vdom/implementation.ts#L1366
// With some fast paths from Surplus implementation:
// https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L86
// And working with data directly from Stage0:
// https://github.com/Freak613/stage0/blob/master/reconcile.js
// This implementation is tailored for fine grained change detection and adds support for fragments
export function reconcile(
a,
b,
beforeNode,
afterNode,
createFn,
onClear,
onRemove
) {
// When parent was a DocumentFragment, then items got appended to the DOM.
const parent = afterNode.parentNode;
let length = b.length;
let i;
// Fast path for clear
if (length === 0) {
let startMark = beforeNode.previousSibling;
if ((startMark && startMark.previousSibling) || afterNode.nextSibling) {
removeNodes(parent, beforeNode.nextSibling, afterNode);
} else {
parent.textContent = '';
if (startMark) {
parent.appendChild(startMark);
}
parent.appendChild(beforeNode);
parent.appendChild(afterNode);
}
onClear && onClear();
return [];
}
// Fast path for create
if (a.length === 0) {
for (i = 0; i < length; i++) {
createFn(parent, b[i], i, b, afterNode);
}
return b.slice();
}
let aStart = 0;
let bStart = 0;
let aEnd = a.length - 1;
let bEnd = length - 1;
let tmp;
let aStartNode = beforeNode.nextSibling;
let aEndNode = afterNode.previousSibling;
let bAfterNode = afterNode;
let mark;
fixes: while (true) {
// Skip prefix
while (a[aStart] === b[bStart]) {
bStart++;
aStartNode = step(aStartNode, FORWARD);
if (aEnd < ++aStart || bEnd < bStart) break fixes;
}
// Skip suffix
while (a[aEnd] === b[bEnd]) {
bEnd--;
bAfterNode = step(aEndNode, BACKWARD, true);
aEndNode = bAfterNode.previousSibling;
if (--aEnd < aStart || bEnd < bStart) break fixes;
}
break;
}
// Fast path for shrink
if (bEnd < bStart) {
while (aStart <= aEnd--) {
tmp = step(aEndNode, BACKWARD, true);
mark = tmp.previousSibling;
removeNodes(parent, tmp, aEndNode.nextSibling);
onRemove && onRemove(tmp);
aEndNode = mark;
}
return b.slice();
}
// Fast path for add
if (aEnd < aStart) {
while (bStart <= bEnd) {
createFn(parent, b[bStart++], bStart, b, bAfterNode);
}
return b.slice();
}
// Positions for reusing nodes from current DOM state
const P = new Array(bEnd + 1 - bStart);
// Index to resolve position from current to new
const I = new Map();
for (i = bStart; i <= bEnd; i++) {
P[i] = -1;
I.set(b[i], i);
}
// Re-using `length` variable for reusing nodes count.
length = 0;
let toRemove = [];
for (i = aStart; i <= aEnd; i++) {
tmp = I.get(a[i]);
if (tmp) {
P[tmp] = i;
length++;
} else {
toRemove.push(i);
}
}
// Fast path for full replace
if (length === 0) {
return reconcile(
reconcile(a, [], beforeNode, afterNode, createFn, onClear),
b,
beforeNode,
afterNode,
createFn
);
}
// Collect nodes to work with them
const nodes = [];
tmp = aStartNode;
for (i = aStart; i <= aEnd; i++) {
nodes[i] = tmp;
tmp = step(tmp, FORWARD);
}
for (i = 0; i < toRemove.length; i++) {
let index = toRemove[i];
tmp = nodes[index];
removeNodes(parent, tmp, step(tmp, FORWARD));
onRemove && onRemove(tmp);
}
const longestSeq = longestPositiveIncreasingSubsequence(P, bStart);
// Re-use `length` for longest sequence length.
length = longestSeq.length - 1;
for (i = bEnd; i >= bStart; i--) {
if (longestSeq[length] === i) {
bAfterNode = nodes[P[longestSeq[length]]];
length--;
} else {
if (P[i] === -1) {
tmp = createFn(parent, b[i], i, b, bAfterNode);
} else {
tmp = nodes[P[i]];
insertNodes(parent, tmp, step(tmp, FORWARD), bAfterNode);
}
bAfterNode = tmp;
}
}
return b.slice();
}
let groupCounter = 0;
function add(parent, value, endMark) {
let mark;
if (typeof value === 'string') {
value = document.createTextNode(value);
} else if (!(value instanceof Node)) {
// Passing an empty array creates a DocumentFragment.
value = api.h([], value);
}
if (
value.nodeType === 11 &&
(mark = value.firstChild) &&
mark !== value.lastChild
) {
mark[GROUPING] = value.lastChild[GROUPING] = ++groupCounter;
}
// If endMark is `null`, value will be added to the end of the list.
parent.insertBefore(value, endMark);
// Explicit undefined to store if frag.firstChild is null.
return mark || value;
}
function step(node, direction, inner) {
const key = node[GROUPING];
if (key) {
node = node[direction];
while (node && node[GROUPING] !== key) {
node = node[direction];
}
}
return inner ? node : node[direction];
}
function removeNodes(parent, node, end) {
let tmp;
while (node !== end) {
tmp = node.nextSibling;
parent.removeChild(node);
node = tmp;
}
}
function insertNodes(parent, node, end, target) {
let tmp;
while (node !== end) {
tmp = node.nextSibling;
parent.insertBefore(node, target);
node = tmp;
}
}
// Picked from
// https://github.com/adamhaile/surplus/blob/master/src/runtime/content.ts#L368
// return an array of the indices of ns that comprise the longest increasing subsequence within ns
function longestPositiveIncreasingSubsequence(ns, newStart) {
let seq = [];
let is = [];
let l = -1;
let pre = new Array(ns.length);
for (var i = newStart, len = ns.length; i < len; i++) {
var n = ns[i];
if (n < 0) continue;
var j = findGreatestIndexLEQ(seq, n);
if (j !== -1) pre[i] = is[j];
if (j === l) {
l++;
seq[l] = n;
is[l] = i;
} else if (n < seq[j + 1]) {
seq[j + 1] = n;
is[j + 1] = i;
}
}
for (i = is[l]; l >= 0; i = pre[i], l--) {
seq[l] = i;
}
return seq;
}
function findGreatestIndexLEQ(seq, n) {
// invariant: lo is guaranteed to be index of a value <= n, hi to be >
// therefore, they actually start out of range: (-1, last + 1)
let lo = -1;
let hi = seq.length;
// fast path for simple increasing sequences
if (hi > 0 && seq[hi - 1] <= n) return hi - 1;
while (hi - lo > 1) {
var mid = ((lo + hi) / 2) | 0;
if (seq[mid] > n) {
hi = mid;
} else {
lo = mid;
}
}
return lo;
}