Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new composable api #146

Merged
merged 3 commits into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"@rollup/plugin-eslint": "^8.0.1",
"@rollup/plugin-node-resolve": "13.0.5",
"@rollup/plugin-strip": "^2.1.0",
"@swc-node/jest": "^1.3.3",
"@swc-node/jest": "^1.3.5",
"@testing-library/dom": "^8.10.1",
"@types/glob": "^7.2.0",
"@types/jest": "^27.0.2",
Expand Down
1,202 changes: 671 additions & 531 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/usr/bin/env zx
import 'zx/globals';
import { info, success } from './helpers.mjs';
$.verbose = false;

await $`rm -rf dist/*`;
await $`rollup -c`;
Expand Down
12 changes: 7 additions & 5 deletions src/__test__/patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createElement } from '../createElement';
import { childrenDriver } from '../drivers/children';
import { propsDriver } from '../drivers/props';
import { DELETE, INSERT, m, UPDATE } from '../m';
import { compose, patch } from '../patch';
import { compose, flushWorkStack, patch } from '../patch';
import { VFlags } from '../types/base';

describe('.patch', () => {
Expand Down Expand Up @@ -197,11 +197,13 @@ describe('.patch', () => {

it('should compose a custom patch', () => {
const el1 = createElement(m('div'));
const customPatch = compose([propsDriver, childrenDriver]);
const el2 = customPatch(el1, m('div', { id: 'app' }));
const customPatch = compose([propsDriver(), childrenDriver()]);
const data = customPatch(el1, m('div', { id: 'app' }));

expect((<HTMLElement>el2).id).toEqual('app');
expect((<HTMLElement>el2).isEqualNode(el1)).toBeTruthy();
flushWorkStack(data.workStack);

expect((<HTMLElement>data.el).id).toEqual('app');
expect((<HTMLElement>data.el).isEqualNode(el1)).toBeTruthy();
});

it('should hard replace if different tag', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/createElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { propsDriver } from './drivers/props';
import { flushWorkStack } from './patch';
import { DOMNode, OLD_VNODE_FIELD, VNode } from './types/base';

const patchProps = propsDriver();

/**
* Creates an Element from a VNode
*/
Expand All @@ -12,7 +14,7 @@ export const createElement = (vnode: VNode, attachField = true): DOMNode => {
? <SVGElement>document.createElementNS(<string>vnode.props?.ns, vnode.tag)
: <HTMLElement>document.createElement(vnode.tag);

flushWorkStack(propsDriver(el, vnode));
flushWorkStack(patchProps(el, vnode).workStack);

if (vnode.children) {
for (let i = 0; i < vnode.children.length; ++i) {
Expand Down
261 changes: 140 additions & 121 deletions src/drivers/children.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DOMNode,
VDelta,
VDeltaOperationTypes,
VDriver,
VElement,
VFlags,
VNode,
Expand All @@ -13,145 +14,163 @@ import {
/**
* Diffs two VNode children and modifies the DOM node based on the necessary changes
*/
export const childrenDriver = (
el: HTMLElement | SVGElement,
newVNode: VElement,
oldVNode?: VElement,
workStack: VTask[] = [],
): VTask[] => {
const oldVNodeChildren: VNode[] = oldVNode?.children ?? [];
const newVNodeChildren: VNode[] | undefined = newVNode.children;
const delta: VDelta | undefined = newVNode.delta;
export const childrenDriver =
(): VDriver =>
// @ts-expect-error Subset of VDriver
(
el: HTMLElement | SVGElement,
newVNode: VElement,
oldVNode?: VElement,
workStack: VTask[] = [],
): ReturnType<VDriver> => {
const data = {
el,
newVNode,
oldVNode,
workStack,
};

// Deltas are a way for the compile-time to optimize runtime operations
// by providing a set of predefined operations. This is useful for cases
// where you are performing consistent, predictable operations at a high
// interval, low payload situation.
if (delta) {
for (let i = 0; i < delta.length; ++i) {
const [deltaType, deltaPosition] = delta[i];
const child = el.childNodes[deltaPosition];
switch (deltaType) {
case VDeltaOperationTypes.INSERT:
workStack.push(() =>
el.insertBefore(createElement(newVNodeChildren![deltaPosition]), child),
);
break;
case VDeltaOperationTypes.UPDATE:
patch(<DOMNode>child, newVNodeChildren![deltaPosition], oldVNodeChildren[deltaPosition]);
break;
case VDeltaOperationTypes.DELETE:
workStack.push(() => el.removeChild(child));
break;
}
}
return workStack;
}

// Flags allow for greater optimizability by reducing condition branches.
// Generally, you should use a compiler to generate these flags, but
// hand-writing them is also possible
if (!newVNodeChildren || newVNode.flag === VFlags.NO_CHILDREN) {
if (!oldVNodeChildren) return workStack;
const oldVNodeChildren: VNode[] = oldVNode?.children ?? [];
const newVNodeChildren: VNode[] | undefined = newVNode.children;
const delta: VDelta | undefined = newVNode.delta;

workStack.push(() => (el.textContent = ''));
return workStack;
}
if (newVNode.flag === undefined || newVNode.flag === VFlags.ANY_CHILDREN) {
if (oldVNodeChildren) {
// Interates backwards, so in case a childNode is destroyed, it will not shift the nodes
// and break accessing by index
for (let i = oldVNodeChildren.length - 1; i >= 0; --i) {
patch(<DOMNode>el.childNodes[i], newVNodeChildren[i], oldVNodeChildren[i]);
// Deltas are a way for the compile-time to optimize runtime operations
// by providing a set of predefined operations. This is useful for cases
// where you are performing consistent, predictable operations at a high
// interval, low payload situation.
if (delta) {
for (let i = 0; i < delta.length; ++i) {
const [deltaType, deltaPosition] = delta[i];
const child = el.childNodes[deltaPosition];
switch (deltaType) {
case VDeltaOperationTypes.INSERT:
workStack.push(() =>
el.insertBefore(createElement(newVNodeChildren![deltaPosition]), child),
);
break;
case VDeltaOperationTypes.UPDATE:
patch(
<DOMNode>child,
newVNodeChildren![deltaPosition],
oldVNodeChildren[deltaPosition],
);
break;
case VDeltaOperationTypes.DELETE:
workStack.push(() => el.removeChild(child));
break;
}
}
return data;
}

for (let i = oldVNodeChildren.length ?? 0; i < newVNodeChildren.length ?? 0; ++i) {
const node = createElement(newVNodeChildren[i], false);
workStack.push(() => el.appendChild(node));
}
return workStack;
}
if (newVNode.flag === VFlags.ONLY_TEXT_CHILDREN) {
workStack.push(() => (el.textContent = newVNode.children!.join('')));
return workStack;
}
if (newVNode.flag === VFlags.ONLY_KEYED_CHILDREN) {
let oldHead = 0;
let newHead = 0;
let oldTail = oldVNodeChildren.length - 1;
let newTail = newVNodeChildren.length - 1;
// Flags allow for greater optimizability by reducing condition branches.
// Generally, you should use a compiler to generate these flags, but
// hand-writing them is also possible
if (!newVNodeChildren || newVNode.flag === VFlags.NO_CHILDREN) {
if (!oldVNodeChildren) return data;

// Constrain tails to dirty vnodes: [X, A, B, C], [Y, A, B, C] -> [X], [Y]
while (oldHead <= oldTail && newHead <= newTail) {
if ((<VElement>oldVNodeChildren[oldTail]).key !== (<VElement>newVNodeChildren[newTail]).key) {
break;
}
oldTail--;
newTail--;
workStack.push(() => (el.textContent = ''));
return data;
}
if (newVNode.flag === undefined || newVNode.flag === VFlags.ANY_CHILDREN) {
if (oldVNodeChildren) {
// Interates backwards, so in case a childNode is destroyed, it will not shift the nodes
// and break accessing by index
for (let i = oldVNodeChildren.length - 1; i >= 0; --i) {
patch(<DOMNode>el.childNodes[i], newVNodeChildren[i], oldVNodeChildren[i]);
}
}

// Constrain heads to dirty vnodes: [A, B, C, X], [A, B, C, Y] -> [X], [Y]
while (oldHead <= oldTail && newHead <= newTail) {
if ((<VElement>oldVNodeChildren[oldHead]).key !== (<VElement>newVNodeChildren[newHead]).key) {
break;
for (let i = oldVNodeChildren.length ?? 0; i < newVNodeChildren.length ?? 0; ++i) {
const node = createElement(newVNodeChildren[i], false);
workStack.push(() => el.appendChild(node));
}
oldHead++;
newHead++;
return data;
}
if (newVNode.flag === VFlags.ONLY_TEXT_CHILDREN) {
workStack.push(() => (el.textContent = newVNode.children!.join('')));
return data;
}
if (newVNode.flag === VFlags.ONLY_KEYED_CHILDREN) {
let oldHead = 0;
let newHead = 0;
let oldTail = oldVNodeChildren.length - 1;
let newTail = newVNodeChildren.length - 1;

if (oldHead > oldTail) {
// There are no dirty old children: [], [X, Y, Z]
while (newHead <= newTail) {
const newHeadIndex = newHead++;
workStack.push(() =>
el.insertBefore(
createElement(newVNodeChildren[newHeadIndex], false),
el.childNodes[newHeadIndex],
),
);
}
} else if (newHead > newTail) {
// There are no dirty new children: [X, Y, Z], []
while (oldHead <= oldTail) {
const node = el.childNodes[oldHead++];
workStack.push(() => el.removeChild(node));
}
} else {
const oldKeyMap: Record<string, number> = {};
for (let i = oldTail; i >= oldHead; --i) {
oldKeyMap[(<VElement>oldVNodeChildren[i]).key!] = i;
// Constrain tails to dirty vnodes: [X, A, B, C], [Y, A, B, C] -> [X], [Y]
while (oldHead <= oldTail && newHead <= newTail) {
if (
(<VElement>oldVNodeChildren[oldTail]).key !== (<VElement>newVNodeChildren[newTail]).key
) {
break;
}
oldTail--;
newTail--;
}
while (newHead <= newTail) {
const newVNodeChild = <VElement>newVNodeChildren[newHead];
const oldVNodePosition = oldKeyMap[newVNodeChild.key!];
const node = el.childNodes[oldVNodePosition];
const newPosition = newHead++;

// Constrain heads to dirty vnodes: [A, B, C, X], [A, B, C, Y] -> [X], [Y]
while (oldHead <= oldTail && newHead <= newTail) {
if (
oldVNodePosition !== undefined &&
newVNodeChild.key === (<VElement>oldVNodeChildren[oldVNodePosition]).key
(<VElement>oldVNodeChildren[oldHead]).key !== (<VElement>newVNodeChildren[newHead]).key
) {
if (newPosition !== oldVNodePosition) {
// Determine move for child that moved: [X, A, B, C] -> [A, B, C, X]
workStack.push(() => el.insertBefore(node, el.childNodes[newPosition]));
}
delete oldKeyMap[newVNodeChild.key!];
} else {
// VNode doesn't exist yet: [] -> [X]
break;
}
oldHead++;
newHead++;
}

if (oldHead > oldTail) {
// There are no dirty old children: [], [X, Y, Z]
while (newHead <= newTail) {
const newHeadIndex = newHead++;
workStack.push(() =>
el.insertBefore(createElement(newVNodeChild, false), el.childNodes[newPosition]),
el.insertBefore(
createElement(newVNodeChildren[newHeadIndex], false),
el.childNodes[newHeadIndex],
),
);
}
} else if (newHead > newTail) {
// There are no dirty new children: [X, Y, Z], []
while (oldHead <= oldTail) {
const node = el.childNodes[oldHead++];
workStack.push(() => el.removeChild(node));
}
} else {
const oldKeyMap: Record<string, number> = {};
for (let i = oldTail; i >= oldHead; --i) {
oldKeyMap[(<VElement>oldVNodeChildren[i]).key!] = i;
}
while (newHead <= newTail) {
const newVNodeChild = <VElement>newVNodeChildren[newHead];
const oldVNodePosition = oldKeyMap[newVNodeChild.key!];
const node = el.childNodes[oldVNodePosition];
const newPosition = newHead++;

if (
oldVNodePosition !== undefined &&
newVNodeChild.key === (<VElement>oldVNodeChildren[oldVNodePosition]).key
) {
if (newPosition !== oldVNodePosition) {
// Determine move for child that moved: [X, A, B, C] -> [A, B, C, X]
workStack.push(() => el.insertBefore(node, el.childNodes[newPosition]));
}
delete oldKeyMap[newVNodeChild.key!];
} else {
// VNode doesn't exist yet: [] -> [X]
workStack.push(() =>
el.insertBefore(createElement(newVNodeChild, false), el.childNodes[newPosition]),
);
}
}
for (const oldVNodePosition of Object.values(oldKeyMap)) {
// VNode wasn't found in new vnodes, so it's cleaned up: [X] -> []
const node = el.childNodes[oldVNodePosition];
workStack.push(() => el.removeChild(node));
}
}
for (const oldVNodePosition of Object.values(oldKeyMap)) {
// VNode wasn't found in new vnodes, so it's cleaned up: [X] -> []
const node = el.childNodes[oldVNodePosition];
workStack.push(() => el.removeChild(node));
}
return data;
}
return workStack;
}

return workStack;
};
return data;
};
Loading