!136 test(novdom): render and event

* chore: fix some lint
* test(novdom): render and event
This commit is contained in:
Hoikan 2024-02-06 09:43:36 +00:00 committed by 陈超涛
parent 05f11d2c35
commit 62614d2dd5
13 changed files with 1482 additions and 295 deletions

View File

@ -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"
}

View File

@ -23,6 +23,10 @@ export function createComponent<T>(Comp: ComponentConstructor<T>, props: T = {}
}
export function render(codeFn: CodeFunction, element: HTMLElement): () => void {
if (!element) {
throw new Error('Render target is not provided');
}
const disposer = (): void => {
// TODO
};

View File

@ -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> | 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<string, boolean> | 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, string> | string | null,
prevVal?: Record<string, string> | 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];
}
}
}

View File

@ -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<string> | 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;
}

View File

@ -14,5 +14,5 @@
*/
// TODO: JSX type
export type FunctionComponent<Props = {}> = (props: Props) => unknown;
export type FunctionComponent<Props = Record<string, unknown>> = (props: Props) => unknown;
export type AppDisposer = () => void;

View File

@ -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 (
* <tr class={selected()}>
* <td class="col-md-1">{props.item.label}</td>
* </tr>
* )
* };
*
* const RowList = (props) => {
* return <For each={props.list}>
* {(item) => <Row item={item}/>}
* </For>;
* };
*
* const Button = (props) => (
* <div class="col-sm-6">
* <button type="button" id={props.id} onClick={props.cb}>{props.title}</button>
* </div>
* );
*
* 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 (
* <div>
* <div>
* <div>
* <div><h1>Horizon-reactive-novnode</h1></div>
* <div>
* <div>
* <Button id="run" title="Create 1,000 rows" cb={run}/>
* </div>
* </div>
* </div>
* </div>
* <table>
* <tbody id="tbody"><RowList list={state.data}/></tbody>
* </table>
* </div>
* );
* };
*
* render(() => <Main />, document.getElementById("app"));
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ _$template('<tr><td class="col-md-1">'),
_tmpl$2 = /*#__PURE__*/ _$template('<div class="col-sm-6"><button type="button">'),
_tmpl$3 = /*#__PURE__*/ _$template(
'<div><div><div><div><h1>Horizon-reactive-novnode</h1></div><div><div></div></div></div></div><table><tbody id="tbody">'
);
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 => Math.round(Math.random() * 1000) % max;
let nextId = 1;
function buildData(count) {
const 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 = computed(() => {
return props.item.selected.get() ? 'danger' : '';
});
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
_$insert(_el$2, () => props.item.label);
return _el$;
})();
};
const RowList = props => {
return _$createComponent(For, {
get each() {
return props.list;
},
children: item =>
_$createComponent(Row, {
item: item,
}),
});
};
const Button = props =>
(() => {
const _el$3 = _tmpl$2(),
_el$4 = _el$3.firstChild;
_$addEventListener(_el$4, 'click', props.cb, true);
_$insert(_el$4, () => props.title);
watch(() => _$setAttribute(_el$4, 'id', props.id));
return _el$3;
})();
const Main = () => {
const state = reactive({
list: [
{
id: 1,
label: '111',
},
{
id: 2,
label: '222',
},
],
num: 2,
});
function run() {
state.list.set(buildData(5));
}
return (() => {
const _el$5 = _tmpl$3(),
_el$6 = _el$5.firstChild,
_el$7 = _el$6.firstChild,
_el$8 = _el$7.firstChild,
_el$9 = _el$8.nextSibling,
_el$10 = _el$9.firstChild,
_el$11 = _el$6.nextSibling,
_el$12 = _el$11.firstChild;
_$insert(
_el$10,
_$createComponent(Button, {
id: 'run',
title: 'Create 1,000 rows',
cb: run,
})
);
_$insert(
_el$12,
_$createComponent(RowList, {
get list() {
return state.list;
},
})
);
return _el$5;
})();
};
render(() => _$createComponent(Main, {}), container);
_$delegateEvents(['click']);
container.querySelector('#run').click();
});

View File

@ -12,244 +12,22 @@
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck For the compiled code.
import { computed, reactive, watch } from 'inula-reactive';
import { template as _$template, insert as _$insert, setAttribute as _$setAttribute } from '../src/dom';
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 { describe, expect } from 'vitest';
import { domTest as it } from './utils';
import { Show } from '../src/components/Show';
import { For } from '../src/components/For';
describe('insertion', () => {
it('should support placeholder', ({ container }) => {
/**
*
* const count = reactive(0);
* const CountingComponent = () => {
* return <div id="count">Count value is {count()}.</div>;
* };
*
* render(() => <CountingComponent />, container);
*/
const count = reactive(0);
// 编译后:
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`);
const CountingComponent = () => {
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild,
_el$4 = _el$2.nextSibling,
_el$3 = _el$4.nextSibling;
_$insert(_el$, count, _el$4);
return _el$;
})();
};
render(() => _$createComponent(CountingComponent, {}), container);
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0<!---->.');
count.set(c => c + 1);
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
});
});
describe('test no-vdom', () => {
it('return数组click事件', ({ container }) => {
/**
*
* const CountingComponent = () => {
* const [count, setCount] = createSignal(0);
* const add = () => {
* setCount((c) => c + 1);
* }
* return <>
* <div id="count">Count value is {count()}.</div>
* <div><button onClick={add}>add</button></div>
* </>;
* };
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`),
_tmpl$2 = /*#__PURE__*/ _$template(`<div><button id="btn">add`);
const CountingComponent = () => {
const count = reactive(0);
const add = () => {
count.set(c => c + 1);
};
return [
(() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild,
_el$4 = _el$2.nextSibling,
_el$3 = _el$4.nextSibling;
_$insert(_el$, count, _el$4);
return _el$;
})(),
(() => {
const _el$5 = _tmpl$2(),
_el$6 = _el$5.firstChild;
_el$6.$$click = add;
return _el$5;
})(),
];
};
render(() => _$createComponent(CountingComponent, {}), container);
_$delegateEvents(['click']);
container.querySelector('#btn').click();
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
});
it('return 自定义组件', ({ container }) => {
/**
*
* const CountValue = (props) => {
* return <div>Count value is {props.count} .</div>;
* }
*
* const CountingComponent = () => {
* const [count, setCount] = createSignal(0);
* const add = () => {
* setCount((c) => c + 1);
* }
*
* return <div>
* <CountValue count={count} />
* <div><button onClick={add}>add</button></div>
* </div>;
* };
*
* render(() => <CountingComponent />, document.getElementById("app"));
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`),
_tmpl$2 = /*#__PURE__*/ _$template(`<div><div><button id="btn">add`);
const CountValue = props => {
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild,
_el$4 = _el$2.nextSibling,
_el$3 = _el$4.nextSibling;
_$insert(_el$, () => props.count, _el$4);
return _el$;
})();
};
const CountingComponent = () => {
const count = reactive(0);
const add = () => {
count.set(c => c + 1);
};
return (() => {
const _el$5 = _tmpl$2(),
_el$6 = _el$5.firstChild,
_el$7 = _el$6.firstChild;
_$insert(
_el$5,
_$createComponent(CountValue, {
count: count,
}),
_el$6
);
_el$7.$$click = add;
return _el$5;
})();
};
render(() => _$createComponent(CountingComponent, {}), container);
_$delegateEvents(['click']);
container.querySelector('#btn').click();
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
});
it('使用Show组件', ({ container }) => {
/**
*
* const CountValue = (props) => {
* return <div id="count">Count value is {props.count()}.</div>;
* }
*
* const CountingComponent = () => {
* const [count, setCount] = createSignal(0);
* const add = () => {
* setCount((c) => c + 1);
* }
*
* return <div>
* <Show when={count() > 0} fallback={<CountValue count={999} />}>
* <CountValue count={count} />
* </Show>
* <div><button id="btn" onClick={add}>add</button></div>
* </div>;
* };
*
* render(() => <CountingComponent />, document.getElementById("app"));
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ _$template(`<div id="count">Count value is <!>.`),
_tmpl$2 = /*#__PURE__*/ _$template(`<div><div><button id="btn">add`);
const CountValue = props => {
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild,
_el$4 = _el$2.nextSibling,
_el$3 = _el$4.nextSibling;
_$insert(_el$, () => props.count, _el$4);
return _el$;
})();
};
const CountingComponent = () => {
const count = reactive(0);
const add = () => {
count.set(c => c + 1);
};
return (() => {
const _el$5 = _tmpl$2(),
_el$6 = _el$5.firstChild,
_el$7 = _el$6.firstChild;
_$insert(
_el$5,
_$createComponent(Show, {
get if() {
return computed(() => count.get() > 0);
},
get else() {
return _$createComponent(CountValue, {
count: 999,
});
},
get children() {
return _$createComponent(CountValue, {
count: count,
});
},
}),
_el$6
);
_el$7.$$click = add;
return _el$5;
})();
};
render(() => _$createComponent(CountingComponent, {}), container);
_$delegateEvents(['click']);
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 999<!---->.');
container.querySelector('#btn').click();
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
});
describe('For', () => {
it('使用For组件', ({ container }) => {
/**
*
@ -298,9 +76,9 @@ describe('test no-vdom', () => {
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ _$template(`<div>Count value is <!>.`),
const _tmpl$ = /*#__PURE__*/ _$template('<div>Count value is <!>.'),
_tmpl$2 = /*#__PURE__*/ _$template(
`<div><div id="todos"></div><div><button id="btn">add</button></div><div><button id="btn-push">push`
'<div><div id="todos"></div><div><button id="btn">add</button></div><div><button id="btn-push">push'
);
const Todo = props => {
return (() => {
@ -476,10 +254,10 @@ describe('test no-vdom', () => {
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ _$template(`<tr><td class="col-md-1">`),
_tmpl$2 = /*#__PURE__*/ _$template(`<div class="col-sm-6"><button type="button">`),
const _tmpl$ = /*#__PURE__*/ _$template('<tr><td class="col-md-1">'),
_tmpl$2 = /*#__PURE__*/ _$template('<div class="col-sm-6"><button type="button">'),
_tmpl$3 = /*#__PURE__*/ _$template(
`<div><div><div><div><h1>Horizon-reactive-novnode</h1></div><div><div></div></div></div></div><table><tbody id="tbody">`
'<div><div><div><div><h1>Horizon-reactive-novnode</h1></div><div><div></div></div></div></div><table><tbody id="tbody">'
);
const A = [
'pretty',
@ -512,7 +290,7 @@ describe('test no-vdom', () => {
let nextId = 1;
function buildData(count) {
let data = new Array(count);
const data = new Array(count);
for (let i = 0; i < count; i++) {
data[i] = {
id: nextId++,

View File

@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest';
describe('DOM manipulation functions', () => {
describe('template function', () => {
it('should create a node from HTML string', () => {
const node = template('<div>Test</div>')();
const node = template('<div>Test</div>')() as HTMLDivElement;
expect(node.outerHTML).toBe('<div>Test</div>');
});

View File

@ -0,0 +1,120 @@
/*
* 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.
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck For the compiled code.
import { describe, expect, vi } from 'vitest';
import { domTest as it } from './utils';
import { template as _$template, effect as _$effect } from '../src/dom';
import { createComponent as _$createComponent, render } from '../src/core';
import { delegateEvents as _$delegateEvents, addEventListener as _$addEventListener } from '../src/event';
import { reactive } from 'inula-reactive';
function dispatchMouseEvent(element: HTMLElement, eventType = 'click') {
element.dispatchEvent(new MouseEvent(eventType, { bubbles: true }));
}
// mock input change event
function dispatchChangeEvent(input: HTMLElement, value: string) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(input, value);
input.dispatchEvent(new Event('change'));
}
describe('event', () => {
it('should trigger delegated and bound event', ({ container }) => {
/**
*
* const fn = vi.fn();
* const Comp = () => {
* const handler = () => fn();
* return <>
* <input id="inline-fn-change" onChange={() =>fn("bound")}/>
* <input id="var-change" onChange={handler}/>
* <input id="hoisted-var-change" onChange={fn}/>
* <button id="inline-fn-click" onClick={() =>fn("delegated")}>Click Delegated</button>
* <button id="var-click" onClick={handler}>Click Delegated</button>
* <button id="hoisted-var-click" onClick={fn}>Click Delegated</button>
* </>;
* };
*
* render(() => <CountingComponent />, container);
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ _$template(`<input id="inline-fn-change">`),
_tmpl$2 = /*#__PURE__*/ _$template(`<input id="var-change">`),
_tmpl$3 = /*#__PURE__*/ _$template(`<input id="hoisted-var-change">`),
_tmpl$4 = /*#__PURE__*/ _$template(`<button id="inline-fn-click">Click Delegated`),
_tmpl$5 = /*#__PURE__*/ _$template(`<button id="var-click">Click Delegated`),
_tmpl$6 = /*#__PURE__*/ _$template(`<button id="hoisted-var-click">Click Delegated`);
const fn = vi.fn();
const Comp = () => {
const handler = () => fn();
return [
(() => {
const _el$ = _tmpl$();
_el$.addEventListener('change', () => fn('bound'));
return _el$;
})(),
(() => {
const _el$2 = _tmpl$2();
_el$2.addEventListener('change', handler);
return _el$2;
})(),
(() => {
const _el$3 = _tmpl$3();
_el$3.addEventListener('change', fn);
return _el$3;
})(),
(() => {
const _el$4 = _tmpl$4();
_el$4.$$click = () => fn('delegated');
return _el$4;
})(),
(() => {
const _el$5 = _tmpl$5();
_el$5.$$click = handler;
return _el$5;
})(),
(() => {
const _el$6 = _tmpl$6();
_el$6.$$click = fn;
return _el$6;
})(),
];
};
render(() => _$createComponent(Comp), container);
_$delegateEvents(['click']);
dispatchChangeEvent(document.getElementById('inline-fn-change'), 'change');
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith('bound');
dispatchChangeEvent(document.getElementById('var-change'), 'change');
expect(fn).toHaveBeenCalledTimes(2);
dispatchChangeEvent(document.getElementById('hoisted-var-change'), 'change');
expect(fn).toHaveBeenCalledTimes(3);
dispatchMouseEvent(document.getElementById('inline-fn-click'));
expect(fn).toHaveBeenCalledTimes(4);
expect(fn).toHaveBeenCalledWith('delegated');
dispatchMouseEvent(document.getElementById('var-click'));
expect(fn).toHaveBeenCalledTimes(5);
dispatchMouseEvent(document.getElementById('hoisted-var-click'));
expect(fn).toHaveBeenCalledTimes(6);
});
});

File diff suppressed because it is too large Load Diff

View File

@ -175,8 +175,9 @@ export class RNode<T = any> implements Signal<T> {
sameGetsIndex = 0;
try {
// Run cleanups first.
if (this.cleanups.length) {
this.cleanups.forEach(c => c(this._value));
this.cleanups.forEach(cleanup => cleanup(this._value));
this.cleanups = [];
}

View File

@ -25,8 +25,9 @@ export function createReactive<T extends symbol>(raw?: T): Signal<symbol>;
export function createReactive<T extends number | string | symbol>(raw?: T): Signal<T>;
export function createReactive<T extends Record<any, any> | Array<any> | symbol>(raw?: T): DeepReactive<T>;
export function createReactive<T extends NonFunctionType>(raw: T): DeepReactive<T> | Signal<T> {
if (isPrimitive(raw) || raw === null || raw === undefined) {
return new RNode(raw, { isSignal: true });
// Function, Date, RegExp, null, undefined are simple signals
if (isPrimitive(raw) || raw === null || raw === undefined || raw instanceof Date || raw instanceof RegExp || typeof raw === 'function') {
return new RNode(raw, {isSignal: true});
} else {
const node = new RProxyNode(raw);
return node.proxy;
@ -34,14 +35,14 @@ export function createReactive<T extends NonFunctionType>(raw: T): DeepReactive<
}
export function createComputed<T extends NoArgFn>(fn: T) {
const rNode = new RProxyNode<T>(fn, { isComputed: true });
const rNode = new RProxyNode<T>(fn, {isComputed: true});
return rNode.proxy;
}
export function createWatch<T>(fn: T) {
const rNode = new RNode(fn, {
isEffect: true,
lazy: false
lazy: false,
});
return rNode;
@ -57,7 +58,7 @@ export function getOrCreateChildRNode(node: RProxyNode<any>, key: string | symbo
if (node.isComputed && !node.parent) {
const root = node.read();
node.root = {
$: root
$: root,
};
}
let child = node.children?.get(key);
@ -74,7 +75,7 @@ export function getOrCreateChildRNode(node: RProxyNode<any>, key: string | symbo
parent: node,
key: key,
root: node.root,
}
},
);
}

View File

@ -51,8 +51,7 @@ export type KEY = string | symbol;
/**
* RProxyNode
* @description
* RProxyNode is a proxy of RNode, it's used to create a reactive object.
* It's agent between Proxy and RNode, the createReactive will return the proxy.
* An agent between Proxy and RNode, the createReactive will return the proxy.
* When create a RProxyNode, it will create a signal as the root of the reactive object.
* When accessing the properties of the proxy, the RProxyNode will be created as the derived child of the root signal.
* And every layer of the RProxyNode will be created as the derived child of the parent RProxyNode.