Improved reconciliation

pull/1/head
paldepind 10 years ago
parent fa3b5b39cf
commit e3efdcb60d

@ -1,3 +1,4 @@
// jshint newcap: false
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory); // AMD. Register as an anonymous module.
@ -8,25 +9,33 @@
}
}(this, function () {
'use strict';
var isArr = Array.isArray;
function isString(s) { return typeof s === 'string'; }
function isPrimitive(s) { return typeof s === 'string' || typeof s === 'number'; }
function isUndef(s) { return s === undefined; }
function VNode(tag, props, children, text) {
return {tag: tag, props: props, children: children, text: text, elm: undefined};
function VNode(tag, props, children, text, elm) {
var key = !isUndef(props) ? props.key : undefined;
return {tag: tag, props: props, children: children,
text: text, elm: elm, key: key};
}
var emptyNodeAt = VNode.bind(null, {style: {}, class: {}}, []);
function emptyNodeAt(elm) {
return VNode(elm.tagName, {style: {}, class: {}}, [], undefined, elm);
}
var frag = document.createDocumentFragment();
var emptyNode = VNode(undefined, {style: {}, class: {}}, [], undefined);
function h(selector, b, c) {
var props = {}, children, tag, i;
if (arguments.length === 3) {
props = b; children = isString(c) ? [c] : c;
props = b; children = isPrimitive(c) ? [c] : c;
} else if (arguments.length === 2) {
if (isArr(b)) { children = b; }
else if (isString(b)) { children = [b]; }
else if (isPrimitive(b)) { children = [b]; }
else { props = b; }
}
// Parse selector
@ -40,19 +49,12 @@ function h(selector, b, c) {
if (isArr(children)) {
for (i = 0; i < children.length; ++i) {
if (isString(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
}
}
return VNode(tag, props, children, undefined, undefined);
}
function setStyles(elm, styles) {
for (var key in styles) {
elm.style[key] = styles[key];
}
}
function updateProps(elm, oldProps, props) {
var key, val, name, on;
for (key in props) {
@ -71,7 +73,7 @@ function updateProps(elm, oldProps, props) {
elm.classList[on ? 'add' : 'remove'](name);
}
}
} else {
} else if (key !== 'key') {
elm[key] = val;
}
}
@ -95,68 +97,119 @@ function createElm(vnode) {
return elm;
}
function sameElm(vnode1, vnode2) {
var key1 = isUndef(vnode1.props) ? undefined : vnode1.props.key;
var key2 = isUndef(vnode2.props) ? undefined : vnode2.props.key;
return key1 === key2 && vnode1.tag === vnode2.tag;
function sameVnode(vnode1, vnode2) {
return vnode1.key === vnode2.key && vnode1.tag === vnode2.tag;
}
function createKeyToOldIdx(children, beginIdx, endIdx) {
var i, map = {};
for (i = beginIdx; i <= endIdx; ++i) {
var ch = children[i];
if (!isUndef(ch.props) && !isUndef(ch.props.key)) {
map[ch.props.key] = i;
}
}
return map;
}
function updateChildren(parentElm, oldCh, newCh) {
if (isUndef(oldCh) && isUndef(newCh)) {
return; // Neither new nor old element has any children
var oldStartIdx = 0, oldEndIdx, oldStartVnode, oldEndVnode;
if (isUndef(oldCh)) {
oldEndIdx = -1;
} else {
oldEndIdx = oldCh.length - 1;
oldStartVnode = oldCh[0];
oldEndVnode = oldCh[oldEndIdx];
}
var oldStartPtr = 0, oldEndPtr = oldCh.length - 1;
var newStartPtr = 0, newEndPtr = newCh.length - 1;
var oldStartElm = oldCh[0], oldEndElm = oldCh[oldEndPtr];
var newStartElm = newCh[0], newEndElm = newCh[newEndPtr];
var success = true;
var succes = true;
while (success && oldStartPtr <= oldEndPtr && newStartPtr <= newEndPtr) {
success = false;
if (sameElm(oldStartElm, newStartElm)) {
oldStartElm = oldCh[++oldStartPtr];
newStartElm = newCh[++newStartPtr];
success = true;
}
if (sameElm(oldEndElm, newEndElm)) {
oldEndElm = oldCh[--oldEndPtr];
newEndElm = newCh[--newEndPtr];
success = true;
}
if (!isUndef(oldStartElm) && !isUndef(newEndElm) &&
sameElm(oldStartElm, newEndElm)) { // Elm moved forward
var beforeElm = oldEndElm.elm.nextSibling;
parentElm.insertBefore(oldStartElm.elm, beforeElm);
oldStartElm = oldCh[++oldStartPtr];
newEndElm = newCh[--newEndPtr];
success = true;
var newStartIdx = 0, newEndIdx, newStartVnode, newEndVnode;
if (isUndef(newCh)) {
newEndIdx = -1;
} else {
newEndIdx = newCh.length - 1;
newStartVnode = newCh[0];
newEndVnode = newCh[newEndIdx];
}
var oldKeyToIdx;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx &&
!isUndef(oldStartVnode) && sameVnode(oldStartVnode, newStartVnode)) {
patchElm(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchElm(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (!isUndef(oldStartVnode) && !isUndef(newEndVnode) &&
sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchElm(oldStartVnode, newEndVnode);
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (!isUndef(oldEndVnode) && !isUndef(newStartVnode) &&
sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchElm(oldEndVnode, newStartVnode);
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
var idxInOld = oldKeyToIdx[newStartVnode.key];
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode);
parentElm.insertBefore(newStartVnode.elm, oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
} else {
var elmToMove = oldCh[idxInOld];
patchElm(elmToMove, newStartVnode);
oldCh[idxInOld] = undefined;
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
newStartVnode = newCh[++newStartIdx];
}
}
}
if (oldStartPtr > oldEndPtr) { // Done with old elements
for (; newStartPtr <= newEndPtr; ++newStartPtr) {
frag.appendChild(createElm(newCh[newStartPtr]));
if (oldStartIdx > oldEndIdx) { // Done with old elements
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
frag.appendChild(createElm(newCh[newStartIdx]));
}
if (isUndef(oldStartElm)) {
if (isUndef(oldStartVnode)) {
parentElm.appendChild(frag);
} else {
parentElm.insertBefore(frag, oldStartElm.elm);
parentElm.insertBefore(frag, oldStartVnode.elm);
}
} else if (newStartPtr > newEndPtr) { // Done with new elements
for (; oldStartPtr <= oldEndPtr; ++oldStartPtr) {
parentElm.removeChild(oldCh[oldStartPtr].elm);
oldCh[oldStartPtr].elm = undefined;
} else if (newStartIdx > newEndIdx) { // Done with new elements
for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
var ch = oldCh[oldStartIdx];
if (!isUndef(ch)) {
parentElm.removeChild(oldCh[oldStartIdx].elm);
oldCh[oldStartIdx].elm = undefined;
}
}
}
}
function patchElm(oldVnode, newVnode) {
var elm = oldVnode.elm;
updateProps(elm, oldVnode.props, newVnode.props);
updateChildren(elm, oldVnode.children, newVnode.children);
var elm = newVnode.elm = oldVnode.elm;
if (isUndef(newVnode.text)) {
updateProps(elm, oldVnode.props, newVnode.props);
updateChildren(elm, oldVnode.children, newVnode.children);
} else {
if (oldVnode.text !== newVnode.text) {
elm.textContent = newVnode.text;
}
}
return newVnode;
}
return {h: h, createElm: createElm, patchElm: patchElm};
return {h: h, createElm: createElm, patchElm: patchElm, patch: patchElm, emptyNodeAt: emptyNodeAt, emptyNode: emptyNode};
}));

@ -1,10 +1,27 @@
var assert = require('assert');
var shuffle = require('knuth-shuffle').knuthShuffle;
var snabbdom = require('../snabbdom');
var createElm = snabbdom.createElm;
var patchElm = snabbdom.patchElm;
var h = snabbdom.h;
function prop(name) {
return function(obj) {
return obj[name];
};
}
function map(fn, list) {
var ret = [];
for (var i = 0; i < list.length; ++i) {
ret[i] = fn(list[i]);
}
return ret;
}
var inner = prop('innerHTML');
describe('snabbdom', function() {
describe('hyperscript', function() {
it('can create vnode with proper tag', function() {
@ -48,6 +65,12 @@ describe('snabbdom', function() {
var vnode = h('a', {}, 'I am a string');
assert.equal(vnode.children[0].text, 'I am a string');
});
it('can create empty vnode at element', function() {
var elm = document.createElement('div');
var vnode = snabbdom.emptyNodeAt(elm);
console.log(vnode);
assert.equal(vnode.elm, elm);
});
});
describe('created element', function() {
it('has tag', function() {
@ -78,7 +101,7 @@ describe('snabbdom', function() {
});
it('can create elements with text content', function() {
var elm = createElm(h('a', ['I am a string']));
//console.log(elm.innerHTML);
assert.equal(elm.innerHTML, 'I am a string');
});
});
describe('pathing an element', function() {
@ -103,12 +126,16 @@ describe('snabbdom', function() {
it('updates styles', function() {
var vnode1 = h('i', {style: {fontSize: '14px', display: 'inline'}});
var vnode2 = h('i', {style: {fontSize: '12px', display: 'block'}});
var vnode3 = h('i', {style: {fontSize: '10px', display: 'block'}});
var elm = createElm(vnode1);
assert.equal(elm.style.fontSize, '14px');
assert.equal(elm.style.display, 'inline');
patchElm(vnode1, vnode2);
assert.equal(elm.style.fontSize, '12px');
assert.equal(elm.style.display, 'block');
patchElm(vnode2, vnode3);
assert.equal(elm.style.fontSize, '10px');
assert.equal(elm.style.display, 'block');
});
describe('updating children with keys', function() {
function spanNum(n) { return h('span', {key: n}, n.toString()); }
@ -124,15 +151,12 @@ describe('snabbdom', function() {
assert.equal(elm.children[2].innerHTML, '3');
});
it('prepends elements', function() {
var vnode1 = h('span', [3].map(spanNum));
var vnode2 = h('span', [1, 2, 3].map(spanNum));
var vnode1 = h('span', [4, 5].map(spanNum));
var vnode2 = h('span', [1, 2, 3, 4, 5].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 1);
assert.equal(elm.children.length, 2);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 3);
console.log(elm.children);
assert.equal(elm.children[1].innerHTML, '2');
assert.equal(elm.children[2].innerHTML, '3');
assert.deepEqual(map(inner, elm.children), ['1', '2', '3', '4', '5']);
});
it('add elements in the middle', function() {
var vnode1 = h('span', [1, 2, 4, 5].map(spanNum));
@ -140,12 +164,31 @@ describe('snabbdom', function() {
var elm = createElm(vnode1);
assert.equal(elm.children.length, 4);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 5);
assert.equal(elm.children[0].innerHTML, '1');
assert.equal(elm.children[1].innerHTML, '2');
assert.equal(elm.children[2].innerHTML, '3');
assert.equal(elm.children[3].innerHTML, '4');
assert.equal(elm.children[4].innerHTML, '5');
assert.deepEqual(map(inner, elm.children), ['1', '2', '3', '4', '5']);
});
it('add elements at begin and end', function() {
var vnode1 = h('span', [2, 3, 4].map(spanNum));
var vnode2 = h('span', [1, 2, 3, 4, 5].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 3);
patchElm(vnode1, vnode2);
assert.deepEqual(map(inner, elm.children), ['1', '2', '3', '4', '5']);
});
it('adds children to parent with no children', function() {
var vnode1 = h('span', {key: 'span'});
var vnode2 = h('span', {key: 'span'}, [1, 2, 3].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 0);
patchElm(vnode1, vnode2);
assert.deepEqual(map(inner, elm.children), ['1', '2', '3']);
});
it('removes children from parent', function() {
var vnode1 = h('span', {key: 'span'}, [1, 2, 3].map(spanNum));
var vnode2 = h('span', {key: 'span'});
var elm = createElm(vnode1);
assert.deepEqual(map(inner, elm.children), ['1', '2', '3']);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 0);
});
});
describe('removal of elements', function() {
@ -155,10 +198,7 @@ describe('snabbdom', function() {
var elm = createElm(vnode1);
assert.equal(elm.children.length, 5);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 3);
assert.equal(elm.children[0].innerHTML, '3');
assert.equal(elm.children[1].innerHTML, '4');
assert.equal(elm.children[2].innerHTML, '5');
assert.deepEqual(map(inner, elm.children), ['3', '4', '5']);
});
it('removes elements from the end', function() {
var vnode1 = h('span', [1, 2, 3, 4, 5].map(spanNum));
@ -178,6 +218,7 @@ describe('snabbdom', function() {
assert.equal(elm.children.length, 5);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 4);
assert.deepEqual(elm.children[0].innerHTML, '1');
assert.equal(elm.children[0].innerHTML, '1');
assert.equal(elm.children[1].innerHTML, '2');
assert.equal(elm.children[2].innerHTML, '4');
@ -208,15 +249,149 @@ describe('snabbdom', function() {
assert.equal(elm.children[1].innerHTML, '3');
assert.equal(elm.children[2].innerHTML, '1');
});
it('moves element backwards', function() {
var vnode1 = h('span', [1, 2, 3, 4].map(spanNum));
var vnode2 = h('span', [1, 4, 2, 3].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 4);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 4);
assert.equal(elm.children[0].innerHTML, '1');
assert.equal(elm.children[1].innerHTML, '4');
assert.equal(elm.children[2].innerHTML, '2');
assert.equal(elm.children[3].innerHTML, '3');
});
it('swaps first and last', function() {
var vnode1 = h('span', [1, 2, 3, 4].map(spanNum));
var vnode2 = h('span', [4, 2, 3, 1].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 4);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 4);
assert.equal(elm.children[0].innerHTML, '4');
assert.equal(elm.children[1].innerHTML, '2');
assert.equal(elm.children[2].innerHTML, '3');
assert.equal(elm.children[3].innerHTML, '1');
});
});
describe('combinations of additions, removals and reorderings', function() {
it('move to left and replace', function() {
var vnode1 = h('span', [1, 2, 3, 4, 5].map(spanNum));
var vnode2 = h('span', [4, 1, 2, 3, 6].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 5);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 5);
assert.equal(elm.children[0].innerHTML, '4');
assert.equal(elm.children[1].innerHTML, '1');
assert.equal(elm.children[2].innerHTML, '2');
assert.equal(elm.children[3].innerHTML, '3');
assert.equal(elm.children[4].innerHTML, '6');
});
it('move to left and leave hole', function() {
var vnode1 = h('span', [1, 4, 5].map(spanNum));
var vnode2 = h('span', [4, 6].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 3);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 2);
assert.equal(elm.children[0].innerHTML, '4');
assert.equal(elm.children[1].innerHTML, '6');
});
it('handles moved and set to undefined element ending at the end', function() {
var vnode1 = h('span', [2, 4, 5].map(spanNum));
var vnode2 = h('span', [4, 5, 3].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 3);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 3);
assert.equal(elm.children[0].innerHTML, '4');
assert.equal(elm.children[1].innerHTML, '5');
assert.equal(elm.children[2].innerHTML, '3');
});
});
it('reverses elements', function() {
var vnode1 = h('span', [1, 2, 3, 4].map(spanNum));
var vnode2 = h('span', [4, 3, 2, 1].map(spanNum));
var elm = createElm(vnode1);
assert.equal(elm.children.length, 4);
patchElm(vnode1, vnode2);
assert.equal(elm.children.length, 4);
assert.equal(elm.children[0].innerHTML, '4');
assert.equal(elm.children[1].innerHTML, '3');
assert.equal(elm.children[2].innerHTML, '2');
assert.equal(elm.children[3].innerHTML, '1');
});
it('handles random shuffles', function() {
var n, i, arr = [], opacities = [], elms = 15, samples = 5;
function spanNumWithOpacity(n, o) {
return h('span', {key: n, style: {opacity: o}}, n.toString());
}
for (n = 0; n < elms; ++n) { arr[n] = n; }
for (n = 0; n < samples; ++n) {
var vnode1 = h('span', arr.map(function(n) {
return spanNumWithOpacity(n, '1');
}));
var shufArr = shuffle(arr.slice(0));
var elm = createElm(vnode1);
for (i = 0; i < elms; ++i) {
assert.equal(elm.children[i].innerHTML, i.toString());
opacities[i] = Math.random().toFixed(5).toString();
}
var vnode2 = h('span', arr.map(function(n) {
return spanNumWithOpacity(shufArr[n], opacities[n]);
}));
patchElm(vnode1, vnode2);
for (i = 0; i < elms; ++i) {
assert.equal(elm.children[i].innerHTML, shufArr[i].toString());
assert.equal(opacities[i].indexOf(elm.children[i].style.opacity), 0);
}
}
});
it('reverses elements');
});
describe('updating children without keys', function() {
it('appends elements');
it('prepends elements');
it('removes elements');
it('reorders elements');
it('reverses elements');
it('appends elements', function() {
var vnode1 = h('div', [h('span', 'Hello')]);
var vnode2 = h('div', [h('span', 'Hello'), h('span', 'World')]);
var elm = createElm(vnode1);
assert.deepEqual(map(inner, elm.children), ['Hello']);
patchElm(vnode1, vnode2);
assert.deepEqual(map(inner, elm.children), ['Hello', 'World']);
});
it('prepends element', function() {
var vnode1 = h('div', [h('span', 'World')]);
var vnode2 = h('div', [h('span', 'Hello'), h('span', 'World')]);
var elm = createElm(vnode1);
assert.deepEqual(map(inner, elm.children), ['World']);
patchElm(vnode1, vnode2);
assert.deepEqual(map(inner, elm.children), ['Hello', 'World']);
});
it('prepends element of different tag type', function() {
var vnode1 = h('div', [h('span', 'World')]);
var vnode2 = h('div', [h('div', 'Hello'), h('span', 'World')]);
var elm = createElm(vnode1);
assert.deepEqual(map(inner, elm.children), ['World']);
patchElm(vnode1, vnode2);
assert.deepEqual(map(prop('tagName'), elm.children), ['DIV', 'SPAN']);
assert.deepEqual(map(inner, elm.children), ['Hello', 'World']);
});
it('removes elements', function() {
var vnode1 = h('div', [h('span', 'One'), h('span', 'Two'), h('span', 'Three')]);
var vnode2 = h('div', [h('span', 'One'), h('span', 'Three')]);
var elm = createElm(vnode1);
assert.deepEqual(map(inner, elm.children), ['One', 'Two', 'Three']);
patchElm(vnode1, vnode2);
assert.deepEqual(map(inner, elm.children), ['One', 'Three']);
});
it('reorders elements', function() {
var vnode1 = h('div', [h('span', 'One'), h('div', 'Two'), h('b', 'Three')]);
var vnode2 = h('div', [h('b', 'Three'), h('span', 'One'), h('div', 'Two')]);
var elm = createElm(vnode1);
assert.deepEqual(map(inner, elm.children), ['One', 'Two', 'Three']);
patchElm(vnode1, vnode2);
assert.deepEqual(map(prop('tagName'), elm.children), ['B', 'SPAN', 'DIV']);
assert.deepEqual(map(inner, elm.children), ['Three', 'One', 'Two']);
});
});
});
});

Loading…
Cancel
Save