sinuous
Version:
🧬 Small, fast, reactive render engine
302 lines (270 loc) • 7.74 kB
JavaScript
/* Adapted from Stage0 - The MIT License - Pavel Martynov */
/* Adapted from DOM Expressions - The MIT License - Ryan Carniato */
import { api } from 'sinuous';
import { FORWARD, BACKWARD } from './constants.js';
import {
addNode,
longestPositiveIncreasingSubsequence,
insertNodes,
removeNodes,
step
} from './utils.js';
let groupCounter = 0;
export default function map(items, expr) {
const { subscribe, root, sample, cleanup } = api;
const disposers = new Map();
let parent = document.createDocumentFragment();
const beforeNode = parent.appendChild(document.createTextNode(''));
const afterNode = parent.appendChild(document.createTextNode(''));
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 root(disposeFn => {
const node = addNode(
parent,
expr(item, i, data),
afterNode,
++groupCounter
);
disposers.set(node, disposeFn);
return node;
});
}
const unsubscribe = subscribe(renderedValues => {
renderedValues = renderedValues || [];
const data = items() || [];
return sample(() =>
reconcile(
parent,
renderedValues,
data,
beforeNode,
afterNode,
createFn,
disposeAll,
dispose
)
);
});
cleanup(unsubscribe);
cleanup(disposeAll);
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(
parent,
renderedValues,
data,
beforeNode,
afterNode,
createFn,
onClear,
onRemove
) {
const length = data.length;
// When parent was a DocumentFragment, then items got appended to the DOM.
parent = afterNode.parentNode;
// Fast path for clear
if (length === 0) {
if (!beforeNode.previousSibling && !afterNode.nextSibling) {
parent.textContent = '';
parent.appendChild(beforeNode);
parent.appendChild(afterNode);
} else {
removeNodes(parent, beforeNode.nextSibling, afterNode);
}
onClear && onClear();
return [];
}
// Fast path for create
if (renderedValues.length === 0) {
for (let i = 0; i < length; i++) {
createFn(parent, data[i], i, data, afterNode);
}
return data.slice();
}
let prevStart = 0;
let newStart = 0;
let loop = true;
let prevEnd = renderedValues.length - 1;
let newEnd = length - 1;
let a;
let b;
let prevStartNode = beforeNode.nextSibling;
let newStartNode = prevStartNode;
let prevEndNode = afterNode.previousSibling;
let newAfterNode = afterNode;
fixes: while (loop) {
loop = false;
let _node;
// Skip prefix
a = renderedValues[prevStart];
b = data[newStart];
while (a === b) {
prevStart++;
newStart++;
newStartNode = prevStartNode = step(prevStartNode, FORWARD);
if (prevEnd < prevStart || newEnd < newStart) break fixes;
a = renderedValues[prevStart];
b = data[newStart];
}
// Skip suffix
a = renderedValues[prevEnd];
b = data[newEnd];
while (a === b) {
prevEnd--;
newEnd--;
newAfterNode = step(prevEndNode, BACKWARD, true);
prevEndNode = newAfterNode.previousSibling;
if (prevEnd < prevStart || newEnd < newStart) break fixes;
a = renderedValues[prevEnd];
b = data[newEnd];
}
// Fast path to swap backward
a = renderedValues[prevEnd];
b = data[newStart];
while (a === b) {
loop = true;
let mark = step(prevEndNode, BACKWARD, true);
_node = mark.previousSibling;
if (newStartNode !== mark) {
insertNodes(parent, mark, prevEndNode.nextSibling, newStartNode);
prevEndNode = _node;
}
newStart++;
prevEnd--;
if (prevEnd < prevStart || newEnd < newStart) break fixes;
a = renderedValues[prevEnd];
b = data[newStart];
}
// Fast path to swap forward
a = renderedValues[prevStart];
b = data[newEnd];
while (a === b) {
loop = true;
_node = step(prevStartNode, FORWARD);
if (prevStartNode !== newAfterNode) {
let mark = _node.previousSibling;
insertNodes(parent, prevStartNode, _node, newAfterNode);
newAfterNode = mark;
prevStartNode = _node;
}
prevStart++;
newEnd--;
if (prevEnd < prevStart || newEnd < newStart) break fixes;
a = renderedValues[prevStart];
b = data[newEnd];
}
}
// Fast path for shrink
if (newEnd < newStart) {
if (prevStart <= prevEnd) {
let next;
let node;
while (prevStart <= prevEnd) {
node = step(prevEndNode, BACKWARD, true);
next = node.previousSibling;
removeNodes(parent, node, prevEndNode.nextSibling);
onRemove && onRemove(node);
prevEndNode = next;
prevEnd--;
}
}
return data.slice();
}
// Fast path for add
if (prevEnd < prevStart) {
if (newStart <= newEnd) {
while (newStart <= newEnd) {
createFn(parent, data[newStart], newStart, data, newAfterNode);
newStart++;
}
}
return data.slice();
}
// Positions for reusing nodes from current DOM state
const P = new Array(newEnd + 1 - newStart);
for (let i = newStart; i <= newEnd; i++) {
P[i] = -1;
}
// Index to resolve position from current to new
const I = new Map();
for (let i = newStart; i <= newEnd; i++) I.set(data[i], i);
let reusingNodes = 0;
let toRemove = [];
for (let i = prevStart; i <= prevEnd; i++) {
if (I.has(renderedValues[i])) {
P[I.get(renderedValues[i])] = i;
reusingNodes++;
} else {
toRemove.push(i);
}
}
// Fast path for full replace
if (reusingNodes === 0) {
return reconcile(
parent,
reconcile(
parent,
renderedValues,
[],
beforeNode,
afterNode,
createFn,
onClear
),
data,
beforeNode,
afterNode,
createFn
);
}
// What else?
const longestSeq = longestPositiveIncreasingSubsequence(P, newStart);
// Collect nodes to work with them
const nodes = [];
let tmpC = prevStartNode;
for (let i = prevStart; i <= prevEnd; i++) {
nodes[i] = tmpC;
tmpC = step(tmpC, FORWARD);
}
for (let i = 0; i < toRemove.length; i++) {
let index = toRemove[i];
let node = nodes[index];
removeNodes(parent, node, step(node, FORWARD));
onRemove && onRemove(node);
}
let lisIdx = longestSeq.length - 1;
let tmpD;
for (let i = newEnd; i >= newStart; i--) {
if (longestSeq[lisIdx] === i) {
newAfterNode = nodes[P[longestSeq[lisIdx]]];
lisIdx--;
} else {
if (P[i] === -1) {
tmpD = createFn(parent, data[i], i, data, newAfterNode);
} else {
tmpD = nodes[P[i]];
insertNodes(parent, tmpD, step(tmpD, FORWARD), newAfterNode);
}
newAfterNode = tmpD;
}
}
return data.slice();
}