Skip to content

Commit

Permalink
feat: MessageChannel-based scheduler
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenybai committed Aug 3, 2021
1 parent d4173e0 commit 480f9de
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 45 deletions.
69 changes: 25 additions & 44 deletions src/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@ import {
VDelta,
VDeltaOperationTypes,
VElement,
VFiber,
VFlags,
VNode,
VProps,
} from './structs';

const workQueue: VFiber[] = [];
const DEADLINE_THRESHOLD = 1000 / 60; // Minimum time buffer for 60 FPS
import { schedule } from './schedule';

/**
* Diffs two VNode props and modifies the DOM node based on the necessary changes
Expand All @@ -25,12 +22,12 @@ export const patchProps = (el: HTMLElement, oldProps: VProps, newProps: VProps):
for (const oldPropName of Object.keys(oldProps)) {
const newPropValue = newProps[oldPropName];
if (newPropValue) {
workQueue.push(() => {
schedule(() => {
el[oldPropName] = newPropValue;
});
skip.add(oldPropName);
} else {
workQueue.push(() => {
schedule(() => {
el.removeAttribute(oldPropName);
delete el[oldPropName];
});
Expand All @@ -39,7 +36,7 @@ export const patchProps = (el: HTMLElement, oldProps: VProps, newProps: VProps):

for (const newPropName of Object.keys(newProps)) {
if (!skip.has(newPropName)) {
workQueue.push(() => {
schedule(() => {
el[newPropName] = newProps[newPropName];
});
}
Expand All @@ -64,7 +61,7 @@ export const patchChildren = (
const [deltaType, deltaPosition] = delta[i];
switch (deltaType) {
case VDeltaOperationTypes.INSERT: {
workQueue.push(() => {
schedule(() => {
el.insertBefore(
createElement(newVNodeChildren[deltaPosition]),
el.childNodes[deltaPosition],
Expand All @@ -81,15 +78,15 @@ export const patchChildren = (
break;
}
case VDeltaOperationTypes.DELETE: {
workQueue.push(() => {
schedule(() => {
el.removeChild(el.childNodes[deltaPosition]);
});
break;
}
}
}
} else if (!newVNodeChildren) {
workQueue.push(() => {
schedule(() => {
el.textContent = '';
});
} else {
Expand All @@ -99,30 +96,15 @@ export const patchChildren = (
}
}
for (let i = oldVNodeChildren.length ?? 0; i < newVNodeChildren.length; ++i) {
workQueue.push(() => {
schedule(() => {
el.appendChild(createElement(newVNodeChildren[i], false));
});
}
}
};

const replaceElementWithVNode = (el: HTMLElement | Text, newVNode: VNode): void => {
workQueue.push(() => el.replaceWith(createElement(newVNode)));
};

const processWorkQueue = (): void => {
const deadline = performance.now() + DEADLINE_THRESHOLD;
const isInputPending =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navigator && (<any>navigator)?.scheduling?.isInputPending({ includeContinuous: true });
while (workQueue.length > 0) {
if (isInputPending || performance.now() >= deadline) {
setTimeout(processWorkQueue);
} else {
const fiber = workQueue.shift();
if (fiber) fiber();
}
}
schedule(() => el.replaceWith(createElement(newVNode)));
};

/**
Expand All @@ -134,7 +116,7 @@ const processWorkQueue = (): void => {
*/
export const patch = (el: HTMLElement | Text, newVNode: VNode, prevVNode?: VNode): void => {
if (!newVNode) {
workQueue.push(() => el.remove());
schedule(() => el.remove());
} else {
const oldVNode: VNode | undefined = prevVNode ?? el[OLD_VNODE_FIELD];
const hasString = typeof oldVNode === 'string' || typeof newVNode === 'string';
Expand All @@ -149,34 +131,36 @@ export const patch = (el: HTMLElement | Text, newVNode: VNode, prevVNode?: VNode
if ((<VElement>oldVNode)?.tag !== (<VElement>newVNode)?.tag || el instanceof Text) {
replaceElementWithVNode(el, newVNode);
} else {
patchProps(el, (<VElement>oldVNode)?.props || {}, (<VElement>newVNode).props || {});
schedule(() => {
patchProps(el, (<VElement>oldVNode)?.props || {}, (<VElement>newVNode).props || {});
});

// 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
switch (<VFlags>(<VElement>newVNode).flag) {
case VFlags.NO_CHILDREN: {
workQueue.push(() => {
schedule(() => {
el.textContent = '';
});
break;
}
case VFlags.ONLY_TEXT_CHILDREN: {
// Joining is faster than setting textContent to an array
workQueue.push(
() => (el.textContent = <string>(<VElement>newVNode).children!.join('')),
);
schedule(() => (el.textContent = <string>(<VElement>newVNode).children!.join('')));
break;
}
default: {
patchChildren(
el,
(<VElement>oldVNode)?.children || [],
(<VElement>newVNode).children!,
// We need to pass delta here because this function does not have
// a reference to the actual vnode.
(<VElement>newVNode).delta,
);
schedule(() => {
patchChildren(
el,
(<VElement>oldVNode)?.children || [],
(<VElement>newVNode).children!,
// We need to pass delta here because this function does not have
// a reference to the actual vnode.
(<VElement>newVNode).delta,
);
});
break;
}
}
Expand All @@ -186,7 +170,4 @@ export const patch = (el: HTMLElement | Text, newVNode: VNode, prevVNode?: VNode
}

if (!prevVNode) el[OLD_VNODE_FIELD] = newVNode;

// Batch all modfications into a scheduler (diffing segregated from DOM manipulation)
processWorkQueue();
};
38 changes: 38 additions & 0 deletions src/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Adapted from https://github.com/yisar/fre/blob/master/src/schedule.ts

import { VTask } from './structs';

const queue: VTask[] = [];
const transitions: VTask[] = [];
const DEADLINE_THRESHOLD = 1000 / 100; // Minimum time buffer for 60 FPS (~6/16ms allocated for repaint)
let deadline = 0;

const { port1, port2 } = new MessageChannel();
port1.onmessage = () => {
if (transitions.length > 0) transitions.shift()!();
};
export const postMessage = (): void => port2.postMessage(null);

export const schedule = (callback: VTask): void => {
queue.push(callback);
startTransition(flush);
};

export const startTransition = (task: VTask): void => {
transitions.push(task) && postMessage();
};

export const shouldYield = (): boolean =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>navigator)?.scheduling?.isInputPending({ includeContinuous: true }) ||
performance.now() >= deadline;

export const flush = (): void => {
deadline = performance.now() + DEADLINE_THRESHOLD;
let task: VTask | undefined = queue.shift();
while (task && !shouldYield()) {
task();
task = queue.shift();
}
if (task) startTransition(flush);
};
2 changes: 1 addition & 1 deletion src/structs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type VProps = Record<string, string | boolean | (() => void)>;
export type VNode = VElement | string;
export type VDeltaOperation = [VDeltaOperationTypes, number];
export type VDelta = VDeltaOperation[];
export type VFiber = () => void;
export type VTask = () => void;

export interface VElement {
tag: string;
Expand Down

0 comments on commit 480f9de

Please sign in to comment.