diff --git a/packages/inula-novdom/package.json b/packages/inula-novdom/package.json index 4136563f..c5597b82 100644 --- a/packages/inula-novdom/package.json +++ b/packages/inula-novdom/package.json @@ -4,11 +4,15 @@ "description": "no vdom runtime", "main": "index.js", "scripts": { - "test": "vitest --ui" + "test": "vitest --ui", + "bench": "vitest bench" }, "dependencies": { - "inula-reactive": "workspace:^0.0.1", + "inula-reactive": "workspace:^0.0.1" + }, + "devDependencies": { "jsdom": "^24.0.0", + "@testing-library/user-event": "^12.1.10", "@vitest/ui": "^0.34.5", "vitest": "^1.2.2" } diff --git a/packages/inula-novdom/src/core.ts b/packages/inula-novdom/src/core.ts index e71ac0cd..02b2dc7f 100644 --- a/packages/inula-novdom/src/core.ts +++ b/packages/inula-novdom/src/core.ts @@ -23,6 +23,10 @@ export function createComponent(Comp: ComponentConstructor, props: T = {} } export function render(codeFn: CodeFunction, element: HTMLElement): () => void { + if (!element) { + throw new Error('Render target is not provided'); + } + const disposer = (): void => { // TODO }; diff --git a/packages/inula-novdom/src/dom.ts b/packages/inula-novdom/src/dom.ts index 2b52487f..077b6a28 100644 --- a/packages/inula-novdom/src/dom.ts +++ b/packages/inula-novdom/src/dom.ts @@ -13,7 +13,8 @@ * See the Mulan PSL v2 for more details. */ -import { isReactiveObj, ReactiveObj, watch } from 'inula-reactive'; +import { watch } from 'inula-reactive'; +import { isReactiveObj } from 'inula-reactive'; /** * Creates a function that returns a Node created from the provided HTML string. @@ -21,26 +22,32 @@ import { isReactiveObj, ReactiveObj, watch } from 'inula-reactive'; * @returns {() => Node} A function that returns a Node created from the provided HTML string. */ export function template(html: string): () => Node { + let node: Node | null; const create = (): Node => { const t = document.createElement('template'); t.innerHTML = html; - return t.content.firstChild as Node; + return t.content.firstChild; }; - return (): Node => create(); + return function () { + if (!node) { + node = create(); + } + return node.cloneNode(true); + }; } -export function insert(parent: Node, accessor: ReactiveObj | any, marker?: Node, initial?: any[]): any { +export function insert(parent: Node, maybeSignal: any, marker?: Node, initial?: any[]): any { if (marker !== undefined && !initial) { initial = []; } - if (isReactiveObj(accessor)) { + if (isReactiveObj(maybeSignal)) { watchRender((current: any) => { - return insertExpression(parent, accessor.get(), current, marker); + return insertExpression(parent, maybeSignal.get(), current, marker); }, initial); } else { - return insertExpression(parent, accessor, initial, marker); + return insertExpression(parent, maybeSignal, initial, marker); } } @@ -161,10 +168,10 @@ function appendNodes(parent: Node, array: Node[], marker: Node | null = null): v } // 拆解数组,如:[[a, b], [c, d], ...] to [a, b, c, d] -function normalizeIncomingArray(normalized: Node[], array: any[], unwrap?: boolean): boolean { +function normalizeIncomingArray(normalized: Node[], array: any[]): boolean { let dynamic = false; for (let i = 0, len = array.length; i < len; i++) { - let item = array[i]; + const item = array[i]; let t: string; if (item == null || item === true || item === false) { // matches null, undefined, true or false @@ -174,15 +181,8 @@ function normalizeIncomingArray(normalized: Node[], array: any[], unwrap?: boole } else if ((t = typeof item) === 'string' || t === 'number') { normalized.push(document.createTextNode(item)); } else if (t === 'function') { - if (unwrap) { - while (typeof item === 'function') { - item = item(); - } - dynamic = normalizeIncomingArray(normalized, Array.isArray(item) ? item : [item]) || dynamic; - } else { - normalized.push(item); - dynamic = true; - } + normalized.push(item); + dynamic = true; } else { normalized.push(item); } @@ -192,13 +192,13 @@ function normalizeIncomingArray(normalized: Node[], array: any[], unwrap?: boole // 原本有节点,现在也有节点 export default function reconcileArrays(parentNode: Node, oldChildren: Node[], newChildren: Node[]): void { - let nLength = newChildren.length, - oEnd = oldChildren.length, - nEnd = nLength, - oStart = 0, - nStart = 0, - after = oldChildren[oEnd - 1].nextSibling, - map = null; + const nLength = newChildren.length; + let oEnd = oldChildren.length; + let nEnd = nLength; + let oStart = 0; + let nStart = 0; + const after = oldChildren[oEnd - 1].nextSibling; + let map = null; while (oStart < oEnd || nStart < nEnd) { // 从前到后对比相同内容 @@ -290,10 +290,59 @@ export function setAttribute(node: Element, name: string, value: string | null): } } -export function className(node: Element, value: string | null): void { +export function className(node: Element, value: string | Record | null): void { if (value == null) { node.removeAttribute('class'); } else { + // value is an array, like ['active', 'text-red'] + if (Array.isArray(value)) { + node.className = value.join(' '); + return; + } + + // value is a object, like { active: true, 'text-red': false } + if (typeof value === 'object') { + let className = ''; + for (const key in value) { + if (value[key]) { + className += key + ' '; + } + } + node.className = className.trim(); + return; + } + // or value is a string node.className = value; } } + +export const effect = watch; + +export function style( + node: HTMLElement, + value: Record | string | null, + prevVal?: Record | string +): void { + if (!value && prevVal) { + // remove all styles + setAttribute(node, 'style', null); + } + if (typeof value === 'string') { + node.style.cssText = value; + } + + // Traverse the previous style object and remove properties that are not in the new style object + if (typeof prevVal === 'object') { + for (const key in prevVal) { + if (value == null || !(key in value)) { + node.style[key] = ''; + } + } + } + // Traverse the new style object and set the properties + if (typeof value === 'object') { + for (const key in value) { + node.style[key] = value[key]; + } + } +} diff --git a/packages/inula-novdom/src/event.ts b/packages/inula-novdom/src/event.ts index f89ef510..48b9f98b 100644 --- a/packages/inula-novdom/src/event.ts +++ b/packages/inula-novdom/src/event.ts @@ -13,7 +13,7 @@ * See the Mulan PSL v2 for more details. */ -const $$EVENTS = "_$DX_DELEGATE"; +const $$EVENTS = '_$DX_DELEGATE'; /** * 在 document上注册事件 @@ -34,34 +34,33 @@ export function delegateEvents(eventNames: string[], document: Document = window export function clearDelegatedEvents(document: Document = window.document): void { const events: Set | undefined = document[$$EVENTS]; if (events) { - for (let name of events.keys()) document.removeEventListener(name, eventHandler); + for (const name of events.keys()) document.removeEventListener(name, eventHandler); delete document[$$EVENTS]; } } - function eventHandler(e: Event) { const key = `$$${e.type}`; let node: EventTarget & Element = (e.composedPath && e.composedPath()[0]) || e.target; if (e.target !== node) { - Object.defineProperty(e, "target", { + Object.defineProperty(e, 'target', { configurable: true, - value: node + value: node, }); } - Object.defineProperty(e, "currentTarget", { + Object.defineProperty(e, 'currentTarget', { configurable: true, get() { return node || document; - } + }, }); // 冒泡执行事件 while (node) { - const handler = node[key as keyof typeof node]; + const handler = node[key] as EventListener | undefined; if (handler && !node.disabled) { const data = node[`${key}Data` as keyof typeof node]; - data !== undefined ? (handler as Function).call(node, data, e) : (handler as Function).call(node, e); + data !== undefined ? handler.call(node, data, e) : handler.call(node, e); if (e.cancelBubble) { return; } @@ -70,18 +69,13 @@ function eventHandler(e: Event) { } } -export function addEventListener(node: Element, name: string, handler: Function | [Function, any], delegate?: boolean): void { - if (delegate) { - if (Array.isArray(handler)) { - node[`$$${name}`] = handler[0]; - node[`$$${name}Data`] = handler[1]; - } else { - node[`$$${name}`] = handler; +export function addEventListener(node: Element, name: string, handler: EventListener, delegate?: boolean): void { + const prev = node[`$$${name}`]; + if (!delegate) { + if (prev) { + node.removeEventListener(name, prev); } - } else if (Array.isArray(handler)) { - const handlerFn = handler[0]; - node.addEventListener(name, (handler[0] = (e: Event) => handlerFn.call(node, handler[1], e))); - } else { node.addEventListener(name, handler); } + node[`$$${name}`] = handler; } diff --git a/packages/inula-novdom/src/type.ts b/packages/inula-novdom/src/type.ts index 9894859f..4e06fa03 100644 --- a/packages/inula-novdom/src/type.ts +++ b/packages/inula-novdom/src/type.ts @@ -14,5 +14,5 @@ */ // TODO: JSX type -export type FunctionComponent = (props: Props) => unknown; +export type FunctionComponent> = (props: Props) => unknown; export type AppDisposer = () => void; diff --git a/packages/inula-novdom/tests/For.bench.ts b/packages/inula-novdom/tests/For.bench.ts new file mode 100644 index 00000000..182945ae --- /dev/null +++ b/packages/inula-novdom/tests/For.bench.ts @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2024 Huawei Technologies Co.,Ltd. + * + * openInula is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ +import { bench } from 'vitest'; +import { computed, reactive, watch } from 'inula-reactive'; +import { + template as _$template, + insert as _$insert, + setAttribute as _$setAttribute, +} from '../src/dom'; +import { createComponent as _$createComponent, render } from '../src/core'; +import { delegateEvents as _$delegateEvents, addEventListener as _$addEventListener } from '../src/event'; +import { For } from '../src/components/For'; + +const container = document.createElement('div'); +document.body.appendChild(container); + +bench('For', () => { + /** + * 源码: + * const A = ['pretty', 'large', 'big', 'small', 'tall', 'short', 'long', 'handsome', 'plain', 'quaint', 'clean', + * 'elegant', 'easy', 'angry', 'crazy', 'helpful', 'mushy', 'odd', 'unsightly', 'adorable', 'important', 'inexpensive', + * 'cheap', 'expensive', 'fancy']; + * + * const random = (max: any) => Math.round(Math.random() * 1000) % max; + * + * let nextId = 1; + * + * function buildData(count: number) { + * let data = new Array(count); + * + * for (let i = 0; i < count; i++) { + * data[i] = { + * id: nextId++, + * label: `${A[random(A.length)]}`, + * } + * } + * return data; + * } + * + * const Row = (props) => { + * const selected = createMemo(() => { + * return props.item.selected ? 'danger' : ''; + * }); + * + * return ( + * + * {props.item.label} + * + * ) + * }; + * + * const RowList = (props) => { + * return + * {(item) => } + * ; + * }; + * + * const Button = (props) => ( + *
+ * + *
+ * ); + * + * const Main = () => { + * const [state, setState] = createStore({data: [{id: 1, label: '111', selected: false}, {id: 2, label: '222', selected: false}], num: 2}); + * + * function run() { + * setState('data', buildData(5)); + * } + * + * return ( + *
+ *
+ *
+ *

Horizon-reactive-novnode

+ *
+ *
+ *
+ *
+ *
+ *
+ * + * + *
+ *
+ * ); + * }; + * + * render(() =>
, document.getElementById("app")); + */ + + // 编译后: + const _tmpl$ = /*#__PURE__*/ _$template(''), + _tmpl$2 = /*#__PURE__*/ _$template('