feat: new queuing/dirty checking approach

This commit is contained in:
Michael Rawlings 2021-05-06 16:04:02 -07:00
parent 95284be78c
commit 331d58dc94
No known key found for this signature in database
GPG Key ID: B9088328804D407C
24 changed files with 159 additions and 104 deletions

View File

@ -6,9 +6,9 @@
{
"name": "*",
"individual": {
"min": 8522,
"gzip": 3629,
"brotli": 3330
"min": 8729,
"gzip": 3767,
"brotli": 3466
}
}
]

View File

@ -1,8 +1,7 @@
import { Context, setContext } from "../common/context";
import { reconcile } from "./reconcile";
import { Renderer, initRenderer } from "./renderer";
import { getQueuedScope } from "./queue";
import { Scope, createScope, getEmptyScope } from "./scope";
import { Scope, createScope, getEmptyScope, set } from "./scope";
import { NodeType } from "./dom";
export type Conditional = {
@ -199,9 +198,8 @@ export function setLoopOf(loop: Loop, newArray: unknown[]) {
loop.___parentOffset
);
} else {
const queuedScope = getQueuedScope(childScope);
queuedScope[0] = item;
queuedScope[1] = index;
set(childScope, 0, item);
set(childScope, 1, index);
}
newScopes.set(key, childScope);
}

View File

@ -31,14 +31,8 @@ export { init, register } from "./hydrate";
export { pushContext, popContext, getInContext } from "../common/context";
export {
queue,
getQueuedScope,
run,
checkDirty,
checkDirtyNotEqual
} from "./queue";
export { queue, setQueued, run } from "./queue";
export { Scope } from "./scope";
export { Scope, set, checkDirty } from "./scope";
export { createRenderer, createRenderFn } from "./renderer";

View File

@ -1,63 +1,73 @@
import { Scope } from "./scope";
import { set, cleanScopes, Scope } from "./scope";
type ExecFn = (scope: Scope, offset: number) => void;
const { port1, port2 } = new MessageChannel();
let queued: boolean;
port1.onmessage = () => {
queued = false;
run();
};
function flushAndWaitFrame() {
run();
requestAnimationFrame(triggerMacroTask);
}
function triggerMacroTask() {
port2.postMessage(0);
}
const fns: Set<ExecFn> = new Set();
let queuedFns: unknown[] = [];
export function queue(fn: ExecFn, scope: Scope, offset: number) {
// TODO: maybe don't do this and
// 1. required a queued scope to be passed in OR
// 2. get the queued scope when running the queue
const stagedScope = getQueuedScope(scope);
export function queue(fn: ExecFn, scope: Scope, offset: number) {
if (fns.has(fn))
for (let i = 0; i < queuedFns.length; i += 3)
if (
queuedFns[i] === fn &&
queuedFns[i + 1] === stagedScope &&
queuedFns[i + 1] === scope &&
queuedFns[i + 2] === offset
)
return;
else fns.add(fn);
queuedFns.push(fn, stagedScope, offset);
if (!queued) {
queued = true;
queueMicrotask(flushAndWaitFrame);
}
queuedFns.push(fn, scope, offset);
}
let queuedScopes: Map<Scope, Scope> = new Map();
export function getQueuedScope(scope: Scope) {
let queuedScope = queuedScopes.get(scope);
if (!queuedScope) {
queuedScopes.set(scope, (queuedScope = Object.create(scope)));
}
return queuedScope || scope;
let queuedValues: unknown[] = [];
export function setQueued(scope: Scope, index: number, value: unknown) {
// TODO: if the same index is set twice for a scope,
// the first one should be removed from the queue
queuedValues.push(scope, index, value);
}
export function run() {
const runningFns = queuedFns;
const runningScopes = queuedScopes;
queuedFns = [];
queuedScopes = new Map();
fns.clear();
for (let i = 0; i < runningFns.length; i += 3) {
(runningFns[i] as ExecFn)(
runningFns[i + 1] as Scope,
runningFns[i + 2] as number
);
}
for (const runningScope of runningScopes.values()) {
Object.assign(Object.getPrototypeOf(runningScope), runningScope);
if (queuedFns.length) {
const runningFns = queuedFns;
const runningValues = queuedValues;
queuedFns = [];
queuedValues = [];
fns.clear();
for (let i = 0; i < runningValues.length; i += 3) {
set(
runningValues[i] as Scope,
runningValues[i + 1] as number,
runningValues[i + 2] as unknown
);
}
for (let i = 0; i < runningFns.length; i += 3) {
(runningFns[i] as ExecFn)(
runningFns[i + 1] as Scope,
runningFns[i + 2] as number
);
}
cleanScopes();
}
}
export function getRunningScope() {}
export function checkDirty(scope: unknown[], index: number) {
return scope.hasOwnProperty(index);
}
export function checkDirtyNotEqual(scope: unknown[], index: number) {
return (
checkDirty(scope, index) &&
scope[index] !== Object.getPrototypeOf(scope)[index]
);
}

View File

@ -1,7 +1,6 @@
import { Conditional, Loop } from "./control-flow";
import { DOMMethods } from "./dom";
import { getQueuedScope, queue, run } from "./queue";
import { createScope, Scope } from "./scope";
import { createScope, Scope, cleanScopes } from "./scope";
import { WalkCodes, detachedWalk } from "./walker";
const enum NodeType {
@ -94,11 +93,11 @@ export function createRenderFn<I extends Input>(
const scope = createScope(size!, domMethods!);
const dom = initRenderer(renderer, scope, 0) as RenderResult;
dynamicInput && dynamicInput(input, scope, 0);
cleanScopes();
dom.update = (newInput: I) => {
const queuedScope = getQueuedScope(scope);
dynamicInput && dynamicInput(newInput, queuedScope, 0);
run();
dynamicInput && dynamicInput(newInput, scope, 0);
cleanScopes();
};
dom.destroy = () => {

View File

@ -1,16 +1,21 @@
import { Conditional, Loop } from "./control-flow";
import { DOMMethods, staticNodeMethods } from "./dom";
const dirtyScopes: Set<Scope> = new Set();
export type Scope = unknown[] &
DOMMethods & {
___startNode: Conditional | Loop | Node | undefined;
___endNode: Conditional | Loop | Node | undefined;
___dirty: Record<number, true> | true | undefined;
};
export function createScope(size: number, methods: DOMMethods): Scope {
const scope = new Array(size).fill(undefined) as Scope;
scope.___startNode = scope.___endNode = undefined;
return Object.assign(scope, methods);
scope.___dirty = true;
dirtyScopes.add(Object.assign(scope, methods));
return scope;
}
const emptyScope = createScope(0, staticNodeMethods);
@ -19,4 +24,28 @@ export function getEmptyScope(marker?: Comment) {
return emptyScope;
}
export function attachDOMToScope(scope: Scope, dom: Node) {}
export function set(scope: Scope, index: number, value: unknown) {
if (scope[index] !== value) {
if (!scope.___dirty) {
scope.___dirty = {};
dirtyScopes.add(scope);
}
scope[index] = value;
if (scope.___dirty !== true) {
scope.___dirty[index] = true;
}
}
}
export function checkDirty(scope: Scope, index: number) {
const dirty = scope.___dirty;
return dirty === true || (dirty && dirty[index]);
}
export function cleanScopes() {
for (const scope of dirtyScopes) {
scope.___dirty = undefined;
}
dirtyScopes.clear();
}

View File

@ -2,6 +2,7 @@ import {
walk,
data,
loop,
set,
setLoopOf,
Loop,
Scope,
@ -73,7 +74,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.children;
set(scope, offset, input.children);
execInputChildren(scope, offset);
};

View File

@ -6,7 +6,7 @@ import {
Scope,
on,
ensureDelegated,
getQueuedScope,
setQueued,
queue,
checkDirty
} from "../../../../src/dom/index";
@ -39,7 +39,7 @@ const execClickCount = (scope: Scope, offset: number) => {
"click",
scope[offset] <= 1
? () => {
(getQueuedScope(scope)[offset] as number)++;
setQueued(scope, offset, (scope[offset] as number) + 1);
queue(execClickCount, scope, offset);
}
: false

View File

@ -2,12 +2,14 @@ import {
walk,
data,
loop,
set,
setLoopOf,
Loop,
Scope,
createRenderer,
createRenderFn,
staticNodeMethods
staticNodeMethods,
checkDirty
} from "../../../../src/dom/index";
import { get, next, over } from "../../utils/walks";
@ -73,7 +75,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.children;
set(scope, offset, input.children);
execInputChildren(scope, offset);
};
@ -90,5 +92,10 @@ const iter0 = createRenderer(
);
const iter0_execItem = (scope: Scope) => {
data(scope[3] as Text, (scope[0] as Input["children"][number]).text);
if (checkDirty(scope, 0)) {
set(scope, 4, (scope[0] as Input["children"][number]).text);
if (checkDirty(scope, 4)) {
data(scope[3] as Text, scope[4]);
}
}
};

View File

@ -20,7 +20,7 @@ inserted div0
# Mutations
```
removed #text before div0/#text0
removed div0/#text2 before div0/#text0
inserted div0/#text2
```

View File

@ -2,6 +2,7 @@ import {
walk,
data,
loop,
set,
setLoopOf,
Loop,
Scope,
@ -73,7 +74,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.children;
set(scope, offset, input.children);
execInputChildren(scope, offset);
};

View File

@ -17,8 +17,8 @@ inserted #text0, #text1, #text2
# Mutations
```
inserted #comment0
removed #text before
removed #text before
removed #text before #text
removed #text before #text
removed #text before #comment0
```

View File

@ -2,9 +2,11 @@ import {
walk,
data,
loop,
set,
setLoopOf,
Loop,
Scope,
checkDirty,
createRenderer,
createRenderFn,
staticNodeMethods
@ -78,7 +80,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.children;
set(scope, offset, input.children);
execInputChildren(scope, offset);
};
@ -95,5 +97,10 @@ const iter0 = createRenderer(
);
const iter0_execItem = (scope: Scope) => {
data(scope[3] as Text, (scope[0] as Input["children"][number]).text);
if (checkDirty(scope, 0)) {
set(scope, 4, (scope[0] as Input["children"][number]).text);
if (checkDirty(scope, 4)) {
data(scope[3] as Text, scope[4]);
}
}
};

View File

@ -2,9 +2,11 @@ import {
walk,
data,
loop,
set,
setLoopOf,
Loop,
Scope,
checkDirty,
createRenderer,
createRenderFn,
staticNodeMethods
@ -74,7 +76,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.children;
set(scope, offset, input.children);
execInputChildren(scope, offset);
};
@ -91,5 +93,10 @@ const iter0 = createRenderer(
);
const iter0_execItem = (scope: Scope) => {
data(scope[3] as Text, (scope[0] as Input["children"][number]).text);
if (checkDirty(scope, 0)) {
set(scope, 4, (scope[0] as Input["children"][number]).text);
if (checkDirty(scope, 4)) {
data(scope[3] as Text, scope[4]);
}
}
};

View File

@ -20,10 +20,10 @@ inserted div0
# Mutations
```
removed #text after div0/#text1
removed div0/#text0 after div0/#text1
inserted div0/#text0
div0/#text1: "a" => "d"
div0/#text0: "b" => "c"
div0/#text1: "a" => "d"
```
@ -36,6 +36,6 @@ div0/#text0: "b" => "c"
# Mutations
```
removed #text after div0/#text1
removed div0/#text0 after div0/#text1
inserted div0/#text0
```

View File

@ -1,6 +1,7 @@
import {
walk,
data,
set,
conditional,
setConditionalRenderer,
Conditional,
@ -49,8 +50,8 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.value;
scope[offset + 1] = input.visible;
set(scope, offset, input.value);
set(scope, offset + 1, input.visible);
execInputValueVisible(scope, offset);
};

View File

@ -5,6 +5,7 @@ import {
setConditionalRenderer,
Conditional,
Scope,
set,
createRenderer,
createRenderFn,
staticNodeMethods
@ -46,7 +47,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.value;
set(scope, offset, input.value);
execInputValue(scope, offset);
};

View File

@ -5,6 +5,7 @@ import {
setConditionalRenderer,
Conditional,
Scope,
set,
createRenderer,
createRenderFn,
staticNodeMethods
@ -46,7 +47,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.value;
set(scope, offset, input.value);
execInputValue(scope, offset);
};

View File

@ -5,6 +5,7 @@ import {
setConditionalRenderer,
Conditional,
Scope,
set,
createRenderer,
createRenderFn,
staticNodeMethods
@ -46,7 +47,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.value;
set(scope, offset, input.value);
execInputValue(scope, offset);
};

View File

@ -5,12 +5,12 @@ import {
setConditionalRenderer,
Conditional,
Scope,
set,
createRenderer,
createRenderFn,
staticNodeMethods,
dynamicFragmentMethods,
checkDirty,
checkDirtyNotEqual
checkDirty
} from "../../../../src/dom/index";
import { next, over, get } from "../../utils/walks";
@ -59,15 +59,12 @@ export const hydrate = (scope: Scope, offset: number) => {
export const execInputShowValue1Value2 = (scope: Scope, offset: number) => {
const cond0 = scope[offset + 3] as Conditional;
if (checkDirtyNotEqual(scope, offset)) {
if (checkDirty(scope, offset)) {
setConditionalRenderer(cond0, scope[offset] ? branch0 : undefined);
}
if (cond0.renderer === branch0) {
const cond0_scope = cond0.scope;
if (
checkDirtyNotEqual(scope, offset) ||
checkDirtyNotEqual(scope, offset + 1)
) {
if (checkDirty(scope, offset) || checkDirty(scope, offset + 1)) {
const cond0_0 = cond0_scope[0] as Conditional;
setConditionalRenderer(
cond0_0,
@ -78,10 +75,7 @@ export const execInputShowValue1Value2 = (scope: Scope, offset: number) => {
data(cond0_0_scope[0] as Text, scope[offset + 1]);
}
}
if (
checkDirtyNotEqual(scope, offset) ||
checkDirtyNotEqual(scope, offset + 2)
) {
if (checkDirty(scope, offset) || checkDirty(scope, offset + 2)) {
const cond0_1 = cond0_scope[1] as Conditional;
setConditionalRenderer(
cond0_1,
@ -100,9 +94,9 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.show;
scope[offset + 1] = input.value1;
scope[offset + 2] = input.value2;
set(scope, offset, input.show);
set(scope, offset + 1, input.value1);
set(scope, offset + 2, input.value2);
execInputShowValue1Value2(scope, offset);
};

View File

@ -5,6 +5,7 @@ import {
setConditionalRendererOnlyChild,
Conditional,
Scope,
set,
createRenderer,
createRenderFn,
staticNodeMethods
@ -46,7 +47,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.value;
set(scope, offset, input.value);
execInputValue(scope, offset);
};

View File

@ -2,6 +2,7 @@ import {
attr,
walk,
register,
set,
createRenderFn,
Scope
} from "../../../../src/dom/index";
@ -44,7 +45,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.value;
set(scope, offset, input.value);
execInputValue(scope, offset);
};

View File

@ -1,6 +1,7 @@
import {
attrs,
walk,
set,
register,
createRenderFn,
Scope
@ -41,7 +42,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.value;
set(scope, offset, input.value);
execInputValue(scope, offset);
};

View File

@ -1,6 +1,7 @@
import {
data,
walk,
set,
enableExtendedWalk,
register,
createRenderFn,
@ -36,7 +37,7 @@ export const execDynamicInput = (
scope: Scope,
offset: number
) => {
scope[offset] = input.value;
set(scope, offset, input.value);
execInputValue(scope, offset);
};