Skip to content

Commit

Permalink
feat(core): better handle of fibers rencilliation
Browse files Browse the repository at this point in the history
  • Loading branch information
F0rsaken committed Oct 16, 2024
1 parent 3156155 commit f1c8b9b
Show file tree
Hide file tree
Showing 18 changed files with 159 additions and 103 deletions.
2 changes: 0 additions & 2 deletions packages/ovee/src/core/component/defineComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,6 @@ export function defineComponent<

node.setup(<any>instance);

console.log('-->', props);

return { type, node, props };
};

Expand Down
2 changes: 1 addition & 1 deletion packages/ovee/src/core/component/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ function validateProp(key: string, value: unknown, prop: NormalizedProp, isAbsen
let isValid = false;
const expectedTypes: string[] = [];

for (let i = 0; i++ && !isValid; i++) {
for (let i = 0; i < type.length && !isValid; i++) {
const { valid, expectedType } = assertType(value, type[i]);

expectedTypes.push(expectedType);
Expand Down
26 changes: 18 additions & 8 deletions packages/ovee/src/jsx/runtime/createFiber.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { isDefined } from '@/utils';
import { isDefined, isPrimitive } from '@/utils';

import {
Children,
ElementFiberProps,
Fiber,
FiberFactory,
Expand All @@ -13,8 +12,13 @@ import {
export const JSX_TEXT_FIBER = '__TEXT_FIBER';
export const JSX_FRAGMENT = '__FRAGMENT';

export function createFragment(_props: ElementFiberProps = {}): Fiber {
const props = _props;
export function createFragment(_props: Props = {}): Fiber {
const props = _props as ElementFiberProps;
const children = (_props as Props).children;

if (children) {
props.children = translateChildren(children);
}

return {
type: JSX_FRAGMENT,
Expand Down Expand Up @@ -42,6 +46,8 @@ export function createFiber(
};
}

// TODO: maybe function props also needs translation

const props = _props as FunctionFiberProps;

return {
Expand All @@ -51,9 +57,13 @@ export function createFiber(
};
}

function translateChildren(children: Children): Fiber[] {
function translateChildren(children: Props['children']): Fiber[] {
if (Array.isArray(children)) {
return children.map(c => translateChild(c)).filter(isDefined);
const out = children
.flatMap(c => (Array.isArray(c) ? c.map(translateChild) : translateChild(c)))
.filter(isDefined);

return out;
}

const child = translateChild(children);
Expand All @@ -62,14 +72,14 @@ function translateChildren(children: Children): Fiber[] {
}

function translateChild(child: JSXElement): Fiber | null | undefined {
if (typeof child === 'string' || typeof child === 'number') {
if (isPrimitive(child)) {
return createTextFiber(child);
}

return child;
}

function createTextFiber(text: string | number): Fiber {
function createTextFiber(text: string | number | boolean): Fiber {
return {
type: JSX_TEXT_FIBER,
props: {
Expand Down
120 changes: 78 additions & 42 deletions packages/ovee/src/jsx/runtime/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Logger } from '@/errors';
import { isNil } from '@/utils';

import { JSX_FRAGMENT, JSX_TEXT_FIBER } from './createFiber';
import { ElementFiber, Fiber, FunctionFiber } from './types';
Expand All @@ -10,7 +11,6 @@ export type Render = (fiber: Fiber, target?: Node | null) => Promise<void>;
export type RenderSync = () => void;

const logger = new Logger('JSX Renderer');
const TO_DELETE_KEY = Symbol.for('key-delete');

export class Renderer {
currentRoot: ElementFiber | null = null;
Expand Down Expand Up @@ -148,6 +148,8 @@ function updateFiberNode(fiber: Fiber) {

if (!fiber.node) {
fiber.node = createNode(fiber);
} else {
updateNode(fiber.node, fiber);
}
}

Expand All @@ -157,48 +159,78 @@ function createNode(fiber: ElementFiber): Node | undefined {
? document.createTextNode(fiber.props?.nodeValue ?? '')
: document.createElement(fiber.type);

if (!(node instanceof Text)) {
updateNode(node, fiber);
}
updateNode(node, fiber);

return node;
}

function reconcileChildren(toDelete: Fiber[], fiber: Fiber, children?: Fiber[]) {
if (!children) return;
const removeFiber = (f: Fiber) => {
f.effectTag = 'DELETION';
toDelete.push(f);
};
const updateFiber = (child: Fiber, alternate: Fiber) => {
child.node = alternate?.node;
child.parent = fiber;
child.alternate = alternate;
child.effectTag = 'UPDATE';
};
const removeOldFibers = (toRemove: Fiber[]) => {
for (let i = 0; i < toRemove.length - 1; i++) {
removeFiber(toRemove[i]);
}
};

let oldChild = fiber.alternate?.firstChild;
let prevSibling: Fiber;
const oldKeyChildren: Fiber[] = [];
const oldChildren: Fiber[] = [];
let curr = fiber.alternate?.firstChild;

// connect child fibers with it's parent and siblings
for (let i = 0; i < children.length; i++) {
if (oldChild?.parent?.effectTag === 'DELETION') {
oldChild.key = TO_DELETE_KEY;
oldChild.effectTag = 'DELETION';
}
while (curr) {
(curr.key ? oldKeyChildren : oldChildren).push(curr);
curr = curr.nextSibling;
}

// remove all old children
if (!children?.length) {
oldChildren.forEach(removeFiber);
oldKeyChildren.forEach(removeFiber);

return;
}

for (let i = 0; i < children.length && oldKeyChildren.length > 0; i++) {
const child = children[i];
const sameType = areFibersSame(child, oldChild);

if (sameType) {
child.node = oldChild?.node;
child.parent = fiber;
child.alternate = oldChild;
child.effectTag = 'UPDATE';
}
if (!child.key) continue;

if (child && !sameType) {
child.parent = fiber;
child.effectTag = 'PLACEMENT';
}
const idx = oldKeyChildren.findIndex(f => areFibersSame(f, child));
const alternate = oldKeyChildren[idx];

if (oldChild && !sameType) {
oldChild.effectTag = 'DELETION';
toDelete.push(oldChild);
}
if (!alternate) continue;

removeOldFibers(oldKeyChildren.splice(0, idx + 1));
updateFiber(child, alternate);
}

oldKeyChildren.forEach(removeFiber);

if (oldChild) {
oldChild = oldChild.nextSibling;
let prevSibling: Fiber;
// connect child fibers with it's parent and siblings
for (let i = 0; i < children.length; i++) {
const child = children[i];

// it's not a child with key and don't have an already connected alternate fiber
if (!child.alternate) {
const oldChildIdx = oldChildren.findIndex(f => areFibersSame(f, child));
const oldChild = oldChildren[oldChildIdx];

if (!oldChild) {
child.parent = fiber;
child.effectTag = 'PLACEMENT';
} else {
removeOldFibers(oldChildren.splice(0, oldChildIdx + 1));
updateFiber(child, oldChild);
}
}

if (i === 0) {
Expand All @@ -213,6 +245,8 @@ function reconcileChildren(toDelete: Fiber[], fiber: Fiber, children?: Fiber[])

prevSibling = child;
}

oldChildren.forEach(removeFiber);
}

function commitDeletion(fiber?: Fiber) {
Expand Down Expand Up @@ -242,13 +276,17 @@ function placeNode(fiber: Fiber, nodeParent: Node) {
}

function updateNode(_node: Node | undefined, fiber: Fiber) {
// NOTE: maybe handle
if (!_node) return;

const node = <Element>_node;
const nextProps = fiber.props;
const prevProps = fiber.alternate?.props ?? {};

if (node instanceof Text) {
node.nodeValue = nextProps.nodeValue;
return;
}

//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
Expand All @@ -262,25 +300,23 @@ function updateNode(_node: Node | undefined, fiber: Fiber) {
// Remove old properties
Object.keys(prevProps)
.filter(isAtrribute)
.filter(isGone(nextProps))
.filter(isGone(prevProps, nextProps))
.forEach(name => {
if (name === 'style' || name === 'class') {
node.removeAttribute(name);
} else {
(node as any)[name] = '';
}
node.removeAttribute(name);
});

// Set new or changed properties
Object.keys(nextProps)
.filter(isAtrribute)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
if (name === 'style' || name === 'class') {
node.setAttribute(name, nextProps[name]);
} else {
(node as any)[name] = nextProps[name];
}
// TODO: handle boolean attributes
let value = nextProps[name];

if (value === true) value = '';
if (isNil(value)) return;

node.setAttribute(name, value);
});

// Add event listeners
Expand Down
13 changes: 9 additions & 4 deletions packages/ovee/src/jsx/runtime/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export type JSXElement = undefined | null | string | number | Fiber;
import { AnyObject } from '@/utils';

export type JSXElement = undefined | null | boolean | string | number | Fiber;
export type Children = JSXElement | JSXElement[];
export type SlotProp = () => Children;
export type SlotProp = () => JSXElement;
export type SlotChildren = SlotProp | Record<string | 'default', SlotProp>;

export interface Props {
children?: Children;
children?: Children | Children[];
[k: string]: any;
}

Expand Down Expand Up @@ -46,6 +48,9 @@ export interface FiberShared<P> {
effectTag?: EffectTag;
}

export type FiberFactory = (props: FunctionFiberProps, fiber: Fiber) => Fiber;
export type FiberFactory<Props extends AnyObject = AnyObject> = (
props: FunctionFiberProps & Props,
fiber: Fiber
) => Fiber;

export type EffectTag = 'UPDATE' | 'PLACEMENT' | 'DELETION';
11 changes: 8 additions & 3 deletions packages/ovee/src/jsx/runtime/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ export const isEvent = (key: string) => key.startsWith('on') && isUpperCase(key[

export const isAtrribute = (key: string) => key !== 'children';

export const isNew = (prev: FiberProps, next: FiberProps) => (key: string) =>
prev[key] !== next[key];
export const isNew = (prev: FiberProps, next: FiberProps) => (key: string) => {
if (key in next && !(key in prev)) return true;

export const isGone = (next: FiberProps) => (key: string) => !(key in next);
return prev[key] !== next[key];
};

export const isGone = (prev: FiberProps, next: FiberProps) => (key: string) => {
return !(key in next) || (isNil(next[key]) && prev[key] !== next[key]);
};

export const isFunctionFiber = (fiber: Fiber): fiber is FunctionFiber =>
typeof fiber.type === 'function';
Expand Down
4 changes: 2 additions & 2 deletions packages/ovee/src/utils/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ export class Task<T = void> extends Promise<T> {
};
}

static get [Symbol.species]() {
static override get [Symbol.species]() {
return Promise;
}

get [Symbol.toStringTag]() {
override get [Symbol.toStringTag]() {
return 'Task';
}
}
2 changes: 1 addition & 1 deletion packages/ovee/src/utils/attributeMap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isNil } from './isNil';
import { isNil } from './typeChecking';

export type AttributeMapType = 'number' | 'boolean';
export type GetTypeFromMapType<T extends AttributeMapType> = T extends 'number' ? number : boolean;
Expand Down
2 changes: 1 addition & 1 deletion packages/ovee/src/utils/extractComponent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AnyComponent, App, Component, GetComponentInstance, HTMLOveeElement } from '../core';
import { extractComponentInternalInstance } from './extractComponentInternalInstance';
import { isString } from './isString';
import { isString } from './typeChecking';

export function extractComponent<C extends Component = AnyComponent>(
element: HTMLOveeElement,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
GetComponentInternalInstance,
HTMLOveeElement,
} from '../core';
import { isString } from './isString';
import { toKebabCase } from './toKebabCase';
import { isString } from './typeChecking';

export function extractComponentInternalInstance<C extends Component = AnyComponent>(
element: HTMLOveeElement,
Expand Down
4 changes: 4 additions & 0 deletions packages/ovee/src/utils/hasOwn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const hasOwnProperty = Object.prototype.hasOwnProperty;

export const hasOwn = (val: object, key: string | symbol): key is keyof typeof val =>
hasOwnProperty.call(val, key);
5 changes: 2 additions & 3 deletions packages/ovee/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ export * from './attributeMap';
export * from './EventBus';
export * from './extractComponent';
export * from './extractComponentInternalInstance';
export * from './hasOwn';
export * from './isComponentDefinition';
export * from './isDefined';
export * from './isModuleDefinition';
export * from './isNil';
export { isString } from './isString';
export * from './isValidNode';
export * from './messages';
export * from './MutationObserverManager';
Expand All @@ -17,4 +15,5 @@ export * from './registerCustomElement';
export * from './runThrowable';
export * from './Task';
export * from './toKebabCase';
export * from './typeChecking';
export * from './types';
5 changes: 0 additions & 5 deletions packages/ovee/src/utils/isDefined.ts

This file was deleted.

3 changes: 0 additions & 3 deletions packages/ovee/src/utils/isNil.ts

This file was deleted.

Loading

0 comments on commit f1c8b9b

Please sign in to comment.