the-react 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +34 -0
- package/src/hooks/index.ts +102 -0
- package/src/index.ts +5 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +102 -0
- package/src/my-react/index.ts +509 -0
- package/src/router-dom/Router.tsx +79 -0
- package/src/router-dom/hooks.ts +8 -0
- package/src/router-dom/index.ts +2 -0
- package/src/types/dom.ts +29 -0
- package/src/types/jsx.ts +39 -0
- package/tsconfig.json +28 -0
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "the-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A React library for TheBugs Project",
|
|
5
|
+
"main": "dist/my-react.umd.js",
|
|
6
|
+
"module": "dist/my-react.esm.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/ArtemS18/My-React.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"typescript",
|
|
18
|
+
"react",
|
|
19
|
+
"library",
|
|
20
|
+
"js",
|
|
21
|
+
"ts"
|
|
22
|
+
],
|
|
23
|
+
"author": "Artemii",
|
|
24
|
+
"license": "ISC",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/ArtemS18/My-React/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/ArtemS18/My-React#readme",
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "~5.9.3",
|
|
31
|
+
"vite": "^7.3.1",
|
|
32
|
+
"vite-tsconfig-paths": "^6.1.1"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ComponentInstance, markDirty } from "../my-react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Глобальный контекст активного компонента для хуков.
|
|
5
|
+
* Устанавливается во время рендера компонента функцией _setActiveInstance.
|
|
6
|
+
*/
|
|
7
|
+
let activeInstance: ComponentInstance<any> | undefined = undefined;
|
|
8
|
+
|
|
9
|
+
/** Индекс текущего состояния useState в массиве состояний компонента */
|
|
10
|
+
let activeStateIndex: number = 0;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Устанавливает активный экземпляр компонента для хуков.
|
|
14
|
+
* Вызывается автоматически в начале и конце updateVTree().
|
|
15
|
+
* @param instance - Текущий компонент или undefined (сброс).
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export function _setActiveInstance(instance: ComponentInstance<any> | undefined) {
|
|
19
|
+
activeInstance = instance;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Устанавливает индекс следующего состояния useState.
|
|
24
|
+
* Инкрементируется для каждого вызова useState в компоненте.
|
|
25
|
+
* @param stateIndex - Индекс состояния.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export function _setActiveStateIndex(stateIndex: number) {
|
|
29
|
+
activeStateIndex = stateIndex;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Хук useState - управляет локальным состоянием компонента.
|
|
34
|
+
* Состояние сохраняется в массиве states компонента по индексу вызова.
|
|
35
|
+
*
|
|
36
|
+
* @param initialState - Начальное значение состояния.
|
|
37
|
+
* @returns Массив [текущее_состояние, setter_функция].
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const [count, setCount] = useState(0);
|
|
42
|
+
* setCount(count + 1); // Обновит компонент через markDirty
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function useState<T>(
|
|
46
|
+
initialState: T
|
|
47
|
+
): [T, (newState: T) => void] {
|
|
48
|
+
if (activeInstance === undefined) {
|
|
49
|
+
throw new Error("useState must be called inside a component");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (activeInstance.states.length <= activeStateIndex) {
|
|
53
|
+
activeInstance.states.push(initialState);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const curInstance = activeInstance;
|
|
57
|
+
const idx = activeStateIndex;
|
|
58
|
+
activeStateIndex++;
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
activeInstance.states[idx] as T,
|
|
62
|
+
(newState: T) => {
|
|
63
|
+
curInstance.states[idx] = newState;
|
|
64
|
+
markDirty(curInstance);
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Хук useEffect - выполняет побочные эффекты после рендера.
|
|
71
|
+
* Поддерживает cleanup функции и зависимости для оптимизации.
|
|
72
|
+
*
|
|
73
|
+
* @param effect - Функция эффекта. Может вернуть cleanup функцию.
|
|
74
|
+
* @param deps - Массив зависимостей. Если undefined - выполняется каждый рендер.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* useEffect(() => {
|
|
79
|
+
* const timer = setInterval(() => console.log('tick'), 1000);
|
|
80
|
+
* return () => clearInterval(timer); // cleanup
|
|
81
|
+
* }, []);
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function useEffect(
|
|
85
|
+
effect: () => void | (() => void),
|
|
86
|
+
deps?: any[]
|
|
87
|
+
) {
|
|
88
|
+
if (activeInstance === undefined) {
|
|
89
|
+
throw new Error('useEffect must be called inside a component');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const idx = activeInstance.effectIndex++;
|
|
93
|
+
let eff = activeInstance.effects[idx];
|
|
94
|
+
|
|
95
|
+
if (!eff) {
|
|
96
|
+
eff = { execute: effect, deps };
|
|
97
|
+
activeInstance.effects[idx] = eff;
|
|
98
|
+
} else {
|
|
99
|
+
eff.execute = effect;
|
|
100
|
+
eff.deps = deps;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { jsx as jsxDEV } from '../jsx-runtime'; // Или ваша реализация jsxDEV
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChildrenType,
|
|
3
|
+
NormalizedChildrenType,
|
|
4
|
+
ComponentPropsType,
|
|
5
|
+
JSX,
|
|
6
|
+
JSXElement,
|
|
7
|
+
JSXElementType
|
|
8
|
+
} from "../types/jsx"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Нормализует children в плоский массив JSX-элементов и строк.
|
|
12
|
+
* Преобразует undefined → [], одиночные элементы → [element], массивы → flat().
|
|
13
|
+
* @param children - Исходные children из JSX.
|
|
14
|
+
* @returns Нормализованный массив для reconciler'а.
|
|
15
|
+
*/
|
|
16
|
+
const normalizedChildren = (children: ChildrenType): NormalizedChildrenType => {
|
|
17
|
+
if (children === undefined || children === null) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
if (!Array.isArray(children)) {
|
|
21
|
+
return [children];
|
|
22
|
+
}
|
|
23
|
+
return children.flat().filter(child =>
|
|
24
|
+
child !== null && child !== undefined
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Основная функция создания JSX-элементов и компонентов (jsx).
|
|
30
|
+
* Поддерживает создание DOM-элементов (string tag) и компонентов (function).
|
|
31
|
+
* Автоматически нормализует children и преобразует атрибуты.
|
|
32
|
+
*
|
|
33
|
+
* @param type - Тип элемента: string (div, span) или функция-компонент.
|
|
34
|
+
* @param props - Пропсы элемента/компонента + опциональные children.
|
|
35
|
+
* @param key - Уникальный ключ для reconciler'а (опционально).
|
|
36
|
+
* @returns JSXElement (DOM) или JSXComponent (компонент).
|
|
37
|
+
*
|
|
38
|
+
* @example Создание DOM-элемента:
|
|
39
|
+
* ```ts
|
|
40
|
+
* jsx('div', { className: 'container', children: 'Hello' }, 'unique-key')
|
|
41
|
+
* // → { type: 'element', tagName: 'div', attributes: Map, children: ['Hello'] }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example Создание компонента:
|
|
45
|
+
* ```ts
|
|
46
|
+
* const MyButton = (props) => jsx('button', props);
|
|
47
|
+
* jsx(MyButton, { text: 'Click me' }, 'btn-1')
|
|
48
|
+
* // → { type: 'component', func: MyButton, props, key: 'btn-1' }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function jsx<PropsType extends ComponentPropsType>(
|
|
52
|
+
type: string | ((props: PropsType) => any),
|
|
53
|
+
props: PropsType & { children?: ChildrenType },
|
|
54
|
+
key: string | undefined
|
|
55
|
+
): JSXElementType {
|
|
56
|
+
if (typeof type === 'string') {
|
|
57
|
+
// Создаем DOM-элемент (div, span, button)
|
|
58
|
+
const attributes = new Map<string, any>();
|
|
59
|
+
Object.entries(props).forEach(([k, v]) => {
|
|
60
|
+
if (k !== "children") {
|
|
61
|
+
if (k === "disabled" && !v) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
if (k === "className") {
|
|
65
|
+
attributes.set("class", v)
|
|
66
|
+
} else {
|
|
67
|
+
attributes.set(k, v)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
return {
|
|
72
|
+
type: "element",
|
|
73
|
+
tagName: type,
|
|
74
|
+
attributes: attributes,
|
|
75
|
+
children: normalizedChildren(props.children)
|
|
76
|
+
} as JSXElement
|
|
77
|
+
} else {
|
|
78
|
+
if (key === undefined) {
|
|
79
|
+
key = props.key as string
|
|
80
|
+
}
|
|
81
|
+
if (!key || key === null) {
|
|
82
|
+
const propsForHash = { ...props };
|
|
83
|
+
delete propsForHash.children;
|
|
84
|
+
key = `${type.name}_${stableHash(propsForHash)}`;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
type: "component",
|
|
88
|
+
key: key,
|
|
89
|
+
func: type,
|
|
90
|
+
props,
|
|
91
|
+
children: normalizedChildren(props.children)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function stableHash(obj: any): string {
|
|
96
|
+
return btoa(
|
|
97
|
+
JSON.stringify(obj, Object.keys(obj).sort())
|
|
98
|
+
).slice(0, 8);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type { JSX };
|
|
102
|
+
export { jsx, jsx as jsxs };
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ComponentPropsType,
|
|
3
|
+
JSXElement,
|
|
4
|
+
JSXComponent,
|
|
5
|
+
JSXElementType,
|
|
6
|
+
KeyType
|
|
7
|
+
} from '../types/jsx'
|
|
8
|
+
import type {
|
|
9
|
+
DOMElement,
|
|
10
|
+
DOMTextNode
|
|
11
|
+
} from '../types/dom'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
_setActiveInstance,
|
|
15
|
+
_setActiveStateIndex
|
|
16
|
+
} from '../hooks/index'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Обновляет атрибуты DOM-элемента согласно новой карте атрибутов JSX.
|
|
20
|
+
* Сравнивает старые и новые атрибуты, удаляет устаревшие, добавляет новые,
|
|
21
|
+
* обрабатывает события и специальные атрибуты (style, value).
|
|
22
|
+
* @param repr - DOM-представление элемента.
|
|
23
|
+
* @param newAttrs - Новые атрибуты из JSX-дерева.
|
|
24
|
+
*/
|
|
25
|
+
const patchAttributes = (repr: DOMElement, newAttrs: Map<string, any>) => {
|
|
26
|
+
repr.attrs.forEach((_, k) => {
|
|
27
|
+
if (!newAttrs.has(k)) {
|
|
28
|
+
repr.attrs.delete(k);
|
|
29
|
+
repr.elem.removeAttribute(k);
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
repr.attrs.forEach((v, k) => {
|
|
34
|
+
if (newAttrs.get(k) !== v) {
|
|
35
|
+
repr.attrs.set(k, newAttrs.get(k))
|
|
36
|
+
repr.elem.setAttribute(k, newAttrs.get(k));
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
repr.eventListeners.forEach((l) => {
|
|
40
|
+
repr.elem.removeEventListener(l.type, l.callback)
|
|
41
|
+
})
|
|
42
|
+
repr.eventListeners = []
|
|
43
|
+
|
|
44
|
+
newAttrs.forEach((v, k) => {
|
|
45
|
+
if (k.startsWith("on") && k[2] == k[2].toUpperCase()) {
|
|
46
|
+
const typeEvent = k.slice("on".length).toLocaleLowerCase()
|
|
47
|
+
repr.elem.addEventListener(typeEvent, v as () => void);
|
|
48
|
+
repr.eventListeners.push({type: typeEvent, callback: v})
|
|
49
|
+
} else if (k === "value" && repr.elem instanceof HTMLInputElement) {
|
|
50
|
+
repr.elem.value = v;
|
|
51
|
+
repr.attrs.set(k, v)
|
|
52
|
+
} else if (k === "style" && typeof v === "object" && v !== null) {
|
|
53
|
+
const oldStyle = repr.attrs.get("style") || {};
|
|
54
|
+
for (const prop in oldStyle) {
|
|
55
|
+
if (!(prop in v)) {
|
|
56
|
+
(repr.elem as any).style[prop] = "";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
Object.assign((repr.elem as any).style, v);
|
|
60
|
+
repr.attrs.set(k, v);
|
|
61
|
+
} else if(!repr.attrs.has(k)) {
|
|
62
|
+
// Обычные атрибуты
|
|
63
|
+
repr.attrs.set(k, v)
|
|
64
|
+
repr.elem.setAttribute(k, v)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Массив очередей "грязных" компонентов по уровням глубины */
|
|
70
|
+
const dirtyInstances: Set<ComponentInstance<any>>[] = [];
|
|
71
|
+
let isUpdateScheduled = false;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Глубокое сравнение двух значений с поддержкой функций и вложенных объектов.
|
|
75
|
+
* @param val1 - Первое значение для сравнения.
|
|
76
|
+
* @param val2 - Второе значение для сравнения.
|
|
77
|
+
* @returns true если значения равны.
|
|
78
|
+
*/
|
|
79
|
+
function deepEqual(val1: any, val2: any): boolean {
|
|
80
|
+
if (val1 === val2) return true;
|
|
81
|
+
|
|
82
|
+
if (typeof val1 === 'function' && typeof val2 === 'function') {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (val1 == null || val2 == null || typeof val1 !== 'object' || typeof val2 !== 'object') {
|
|
87
|
+
return val1 === val2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (Array.isArray(val1) && Array.isArray(val2)) {
|
|
91
|
+
if (val1.length !== val2.length) return false;
|
|
92
|
+
for (let i = 0; i < val1.length; i++) {
|
|
93
|
+
if (!deepEqual(val1[i], val2[i])) return false;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const keys1 = Object.keys(val1);
|
|
99
|
+
const keys2 = Object.keys(val2);
|
|
100
|
+
|
|
101
|
+
if (keys1.length !== keys2.length) return false;
|
|
102
|
+
|
|
103
|
+
for (const key of keys1) {
|
|
104
|
+
if (!Object.prototype.hasOwnProperty.call(val2, key) || !deepEqual(val1[key], val2[key])) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Помечает компонент как "грязный" для обновления и планирует raf-обновление.
|
|
113
|
+
* @param instance - Компонент для обновления.
|
|
114
|
+
*/
|
|
115
|
+
export const markDirty = (instance: ComponentInstance<any>) => {
|
|
116
|
+
while (dirtyInstances.length <= instance.depth) {
|
|
117
|
+
dirtyInstances.push(new Set());
|
|
118
|
+
}
|
|
119
|
+
dirtyInstances[instance.depth].add(instance);
|
|
120
|
+
if (!isUpdateScheduled) {
|
|
121
|
+
window.requestAnimationFrame(() => {
|
|
122
|
+
schedUpdate();
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Планировщик обновлений - обрабатывает "грязные" компоненты по уровням глубины.
|
|
129
|
+
* Использует requestAnimationFrame для батчинга обновлений.
|
|
130
|
+
*/
|
|
131
|
+
const schedUpdate = () => {
|
|
132
|
+
isUpdateScheduled = true;
|
|
133
|
+
for (let i = 0; i < dirtyInstances.length; i++) {
|
|
134
|
+
if (dirtyInstances[i].size === 0) {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
dirtyInstances[i].forEach((instance) => {
|
|
138
|
+
instance.update();
|
|
139
|
+
dirtyInstances[i].delete(instance);
|
|
140
|
+
});
|
|
141
|
+
window.requestAnimationFrame(() => {
|
|
142
|
+
schedUpdate();
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
isUpdateScheduled = false;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Структура эффекта useEffect с зависимостями и cleanup функцией.
|
|
152
|
+
*/
|
|
153
|
+
type Effect = {
|
|
154
|
+
execute: () => void | (() => void);
|
|
155
|
+
deps?: any[];
|
|
156
|
+
prevDeps?: any[];
|
|
157
|
+
cleanup?: () => void;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Экземпляр компонента React, управляющий жизненным циклом и DOM-патчингом.
|
|
162
|
+
* Содержит VTree, состояние, эффекты, дочерние компоненты и DOM-представление.
|
|
163
|
+
*/
|
|
164
|
+
export class ComponentInstance<PropsType extends ComponentPropsType> {
|
|
165
|
+
/** Функция рендера компонента */
|
|
166
|
+
func: (props: PropsType) => any;
|
|
167
|
+
/** Карта дочерних компонентов по ключам */
|
|
168
|
+
instanceMap: Map<KeyType, ComponentInstance<any>>;
|
|
169
|
+
/** DOM-представление корневого элемента */
|
|
170
|
+
domElement: DOMElement | undefined;
|
|
171
|
+
/** Виртуальное дерево JSX */
|
|
172
|
+
vTree: JSXElement | undefined;
|
|
173
|
+
/** Пропсы компонента */
|
|
174
|
+
props: PropsType;
|
|
175
|
+
/** Массив состояний useState */
|
|
176
|
+
states: any[] = [];
|
|
177
|
+
/** Уровень глубины в дереве компонентов */
|
|
178
|
+
depth: number;
|
|
179
|
+
/** Родительский компонент */
|
|
180
|
+
parent: ComponentInstance<any> | undefined;
|
|
181
|
+
/** Массив эффектов useEffect */
|
|
182
|
+
effects: Effect[] = [];
|
|
183
|
+
/** Индекс текущего эффекта */
|
|
184
|
+
|
|
185
|
+
effectIndex: number = 0;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Создает новый экземпляр компонента.
|
|
189
|
+
* @param func - Функция рендера компонента.
|
|
190
|
+
* @param props - Пропсы компонента.
|
|
191
|
+
* @param parent - Родительский компонент.
|
|
192
|
+
*/
|
|
193
|
+
constructor(
|
|
194
|
+
func: (props: PropsType) => any,
|
|
195
|
+
props: PropsType,
|
|
196
|
+
parent: ComponentInstance<any> | undefined
|
|
197
|
+
) {
|
|
198
|
+
this.func = func;
|
|
199
|
+
this.props = props;
|
|
200
|
+
this.instanceMap = new Map();
|
|
201
|
+
this.parent = parent;
|
|
202
|
+
this.depth = (parent?.depth ?? 1) + 1;
|
|
203
|
+
|
|
204
|
+
this.update();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Полный цикл обновления компонента */
|
|
208
|
+
update() {
|
|
209
|
+
this.updateVTree();
|
|
210
|
+
if (this.vTree !== null){
|
|
211
|
+
this.patchInstances();
|
|
212
|
+
this.patchDOMNodes();
|
|
213
|
+
this.flushEffects();
|
|
214
|
+
}else{
|
|
215
|
+
this.destroy()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Пересоздает виртуальное дерево JSX */
|
|
221
|
+
updateVTree() {
|
|
222
|
+
_setActiveInstance(this)
|
|
223
|
+
_setActiveStateIndex(0)
|
|
224
|
+
this.effectIndex = 0;
|
|
225
|
+
this.vTree = this.func(this.props)
|
|
226
|
+
_setActiveInstance(undefined)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Извлекает виртуальные компоненты из JSX-дерева в карту.
|
|
231
|
+
* @param branch - Текущая ветка JSX-дерева.
|
|
232
|
+
* @param mapToAdd - Карта для добавления компонентов.
|
|
233
|
+
*/
|
|
234
|
+
extractVirtualComponents(
|
|
235
|
+
branch: JSXElement,
|
|
236
|
+
mapToAdd: Map<KeyType, JSXComponent<any>>
|
|
237
|
+
) {
|
|
238
|
+
branch.children.forEach((ch) => {
|
|
239
|
+
if (typeof ch === "string") {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (ch === undefined || ch === null) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (ch.type == "element") {
|
|
246
|
+
this.extractVirtualComponents(ch, mapToAdd)
|
|
247
|
+
} else {
|
|
248
|
+
if (ch.key !== undefined) {
|
|
249
|
+
mapToAdd.set(ch.key, ch);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Синхронизирует дочерние компоненты с новым VTree */
|
|
256
|
+
patchInstances() {
|
|
257
|
+
if (this.vTree === undefined) {
|
|
258
|
+
throw new Error("vTree is undefined")
|
|
259
|
+
}
|
|
260
|
+
const newInstanceMap = new Map<KeyType, JSXComponent<any>>();
|
|
261
|
+
this.extractVirtualComponents(this.vTree, newInstanceMap);
|
|
262
|
+
|
|
263
|
+
this.instanceMap.forEach((v, k) => {
|
|
264
|
+
if (!newInstanceMap.has(k)) {
|
|
265
|
+
v.destroy();
|
|
266
|
+
this.instanceMap.delete(k);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
this.instanceMap.forEach((v, k) => {
|
|
271
|
+
const newProps = (newInstanceMap.get(k) as JSXComponent<any>).props
|
|
272
|
+
if (!deepEqual(v.props, newProps)) {
|
|
273
|
+
v.props = newProps
|
|
274
|
+
v.update();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
newInstanceMap.forEach((v, k) => {
|
|
279
|
+
if (!this.instanceMap.has(k)) {
|
|
280
|
+
this.instanceMap.set(k, new ComponentInstance<any>(v.func, v.props, this));
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Выполняет эффекты useEffect с проверкой зависимостей */
|
|
286
|
+
flushEffects() {
|
|
287
|
+
for (const eff of this.effects) {
|
|
288
|
+
if (!eff) continue;
|
|
289
|
+
|
|
290
|
+
const depsChanged =
|
|
291
|
+
!eff.prevDeps ||
|
|
292
|
+
eff.deps?.length !== eff.prevDeps.length ||
|
|
293
|
+
eff.deps?.some((dep, i) => !deepEqual(dep, eff.prevDeps?.[i]));
|
|
294
|
+
|
|
295
|
+
if (depsChanged) {
|
|
296
|
+
if (eff.cleanup) {
|
|
297
|
+
eff.cleanup();
|
|
298
|
+
}
|
|
299
|
+
const cleanup = eff.execute();
|
|
300
|
+
if (typeof cleanup === 'function') {
|
|
301
|
+
eff.cleanup = cleanup;
|
|
302
|
+
} else {
|
|
303
|
+
eff.cleanup = undefined;
|
|
304
|
+
}
|
|
305
|
+
eff.prevDeps = eff.deps ? [...eff.deps] : undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Патчит DOM согласно новому VTree */
|
|
311
|
+
patchDOMNodes() {
|
|
312
|
+
if (this.vTree === undefined) {
|
|
313
|
+
throw new Error()
|
|
314
|
+
}
|
|
315
|
+
const parentElem = this.domElement?.elem.parentElement;
|
|
316
|
+
if (this.domElement?.elem.tagName.toLowerCase() !== this.vTree.tagName) {
|
|
317
|
+
const prevChild = this.domElement?.elem
|
|
318
|
+
this.domElement = {
|
|
319
|
+
type: "element",
|
|
320
|
+
elem: document.createElement(this.vTree?.tagName),
|
|
321
|
+
attrs: new Map<string, any>(),
|
|
322
|
+
children: [],
|
|
323
|
+
eventListeners: [],
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (parentElem) {
|
|
327
|
+
parentElem.replaceChild(this.domElement.elem, prevChild as Node);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
patchAttributes(this.domElement, this.vTree.attributes)
|
|
331
|
+
this.patchDOMNodesImpl(this.vTree.children, this.domElement.children, this.domElement.elem)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Рекурсивно патчит DOM-дерево согласно JSX-ветке.
|
|
336
|
+
* @param branch - Ветка JSX-дерева.
|
|
337
|
+
* @param domRepr - DOM-представление.
|
|
338
|
+
* @param parentElement - Родительский DOM-элемент.
|
|
339
|
+
*/
|
|
340
|
+
patchDOMNodesImpl(
|
|
341
|
+
branch: (JSXElementType | string)[],
|
|
342
|
+
domRepr: (DOMElement | DOMTextNode)[],
|
|
343
|
+
parentElement: Element
|
|
344
|
+
) {
|
|
345
|
+
let branchIndex = 0;
|
|
346
|
+
let domReprIndex = 0;
|
|
347
|
+
|
|
348
|
+
while(true) {
|
|
349
|
+
if (branch.length <= branchIndex) {
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const vNode = branch[branchIndex];
|
|
354
|
+
if (vNode === null || vNode === undefined) {
|
|
355
|
+
branchIndex++;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (typeof vNode !== "string" && vNode?.type === undefined) {
|
|
359
|
+
branchIndex++;
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (typeof vNode !== "string" && vNode.type === "component") {
|
|
364
|
+
const compInstance = this.instanceMap.get(vNode.key) as ComponentInstance<any>;
|
|
365
|
+
const compDom = compInstance.domElement;
|
|
366
|
+
console.log("compInstance", compInstance)
|
|
367
|
+
if (!compDom) {
|
|
368
|
+
branchIndex++;
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (domReprIndex >= domRepr.length) {
|
|
373
|
+
domRepr.push(compDom);
|
|
374
|
+
} else if (domRepr[domReprIndex] !== compDom) {
|
|
375
|
+
domRepr.splice(domReprIndex, 0, compDom);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const currentNode = parentElement.childNodes[domReprIndex];
|
|
379
|
+
if (compDom.elem !== currentNode) {
|
|
380
|
+
if (currentNode) {
|
|
381
|
+
parentElement.insertBefore(compDom.elem, currentNode);
|
|
382
|
+
} else {
|
|
383
|
+
parentElement.appendChild(compDom.elem);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
branchIndex++;
|
|
388
|
+
domReprIndex++;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (domReprIndex >= domRepr.length) {
|
|
392
|
+
if (typeof vNode !== "string") {
|
|
393
|
+
domRepr.push({
|
|
394
|
+
type: "element",
|
|
395
|
+
attrs: new Map(),
|
|
396
|
+
elem: document.createElement(vNode.tagName),
|
|
397
|
+
children: [],
|
|
398
|
+
eventListeners: [],
|
|
399
|
+
})
|
|
400
|
+
} else {
|
|
401
|
+
domRepr.push({
|
|
402
|
+
type: "textNode",
|
|
403
|
+
text: vNode,
|
|
404
|
+
node: document.createTextNode(vNode),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const domNode = domRepr[domReprIndex];
|
|
410
|
+
|
|
411
|
+
if (typeof vNode === "string" && domNode.type === "element") {
|
|
412
|
+
domNode.elem.parentElement?.removeChild(domNode.elem);
|
|
413
|
+
domRepr.splice(domReprIndex, 1);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (typeof vNode !== "string" && domNode.type === "textNode") {
|
|
418
|
+
domNode.node.parentElement?.removeChild(domNode.node);
|
|
419
|
+
domRepr.splice(domReprIndex, 1)
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (typeof vNode === "string" && domNode.type === "textNode") {
|
|
424
|
+
domNode.node.textContent = vNode;
|
|
425
|
+
const refNode = parentElement.childNodes[domReprIndex];
|
|
426
|
+
if (refNode) {
|
|
427
|
+
parentElement.insertBefore(domNode.node, refNode);
|
|
428
|
+
} else {
|
|
429
|
+
parentElement.appendChild(domNode.node);
|
|
430
|
+
}
|
|
431
|
+
branchIndex++;
|
|
432
|
+
domReprIndex++;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (typeof vNode !== "string" && domNode.type !== "textNode") {
|
|
437
|
+
let elemRepr = domNode
|
|
438
|
+
if (domNode.elem.tagName.toLowerCase() !== vNode.tagName) {
|
|
439
|
+
const newElemRepr: DOMElement = {
|
|
440
|
+
type: "element",
|
|
441
|
+
attrs: new Map(),
|
|
442
|
+
elem: document.createElement(vNode.tagName),
|
|
443
|
+
children: [],
|
|
444
|
+
eventListeners: []
|
|
445
|
+
}
|
|
446
|
+
if (domNode.elem.parentElement === parentElement) {
|
|
447
|
+
parentElement.insertBefore(newElemRepr.elem, domNode.elem);
|
|
448
|
+
} else {
|
|
449
|
+
parentElement.appendChild(newElemRepr.elem);
|
|
450
|
+
}
|
|
451
|
+
domRepr.splice(domReprIndex, 0, newElemRepr);
|
|
452
|
+
elemRepr = newElemRepr
|
|
453
|
+
}
|
|
454
|
+
patchAttributes(elemRepr, vNode.attributes)
|
|
455
|
+
const currentNode = parentElement.childNodes[domReprIndex];
|
|
456
|
+
if (elemRepr.elem !== currentNode) {
|
|
457
|
+
if (currentNode) {
|
|
458
|
+
parentElement.insertBefore(elemRepr.elem, currentNode);
|
|
459
|
+
} else {
|
|
460
|
+
parentElement.appendChild(elemRepr.elem);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
this.patchDOMNodesImpl(vNode.children, elemRepr.children, elemRepr.elem)
|
|
465
|
+
branchIndex++;
|
|
466
|
+
domReprIndex++;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
while(domRepr.length > domReprIndex) {
|
|
471
|
+
const r = domRepr[domReprIndex]
|
|
472
|
+
if (r.type === "element") {
|
|
473
|
+
r.elem.parentElement?.removeChild(r.elem);
|
|
474
|
+
} else {
|
|
475
|
+
r.node.parentElement?.removeChild(r.node);
|
|
476
|
+
}
|
|
477
|
+
domRepr.splice(domReprIndex, 1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Разрушает компонент и все дочерние */
|
|
482
|
+
destroy() {
|
|
483
|
+
for (const eff of this.effects) {
|
|
484
|
+
if (eff?.cleanup) {
|
|
485
|
+
eff.cleanup();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
this.instanceMap.forEach((v) => {
|
|
489
|
+
v.destroy();
|
|
490
|
+
});
|
|
491
|
+
this.instanceMap.clear()
|
|
492
|
+
if (this.domElement?.elem.parentElement !== null) {
|
|
493
|
+
this.domElement?.elem.parentElement.removeChild(this.domElement.elem);
|
|
494
|
+
}
|
|
495
|
+
this.domElement = undefined;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Создает React-приложение и монтирует его в DOM-элемент.
|
|
501
|
+
* @param elem - Контейнер DOM-элемент.
|
|
502
|
+
* @param fn - Функция рендера корневого компонента.
|
|
503
|
+
*/
|
|
504
|
+
const createApp = (elem: Element, fn: () => JSXElementType) => {
|
|
505
|
+
const inst = new ComponentInstance<any>(fn, {}, undefined);
|
|
506
|
+
elem.appendChild(inst.domElement?.elem as Node);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export { createApp };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { JSXElementType } from "../types/jsx"
|
|
2
|
+
|
|
3
|
+
interface IRouterProps{
|
|
4
|
+
path: string
|
|
5
|
+
children: JSXElementType
|
|
6
|
+
currentPath: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function matchPath(pattern: string, path: string) {
|
|
10
|
+
const names: string[] = []
|
|
11
|
+
let regexStr = ''
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < pattern.length;) {
|
|
14
|
+
const ch = pattern[i]
|
|
15
|
+
if (ch === '{') {
|
|
16
|
+
const close = pattern.indexOf('}', i + 1)
|
|
17
|
+
if (close === -1) {
|
|
18
|
+
regexStr += '\\{'
|
|
19
|
+
i++
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
const name = pattern.substring(i + 1, close)
|
|
23
|
+
names.push(name)
|
|
24
|
+
regexStr += '([^/]+)'
|
|
25
|
+
i = close + 1
|
|
26
|
+
} else {
|
|
27
|
+
if ('^$\\.*+?()[]|/'.includes(ch)) {
|
|
28
|
+
regexStr += '\\' + ch
|
|
29
|
+
} else {
|
|
30
|
+
regexStr += ch
|
|
31
|
+
}
|
|
32
|
+
i++
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const regex = new RegExp('^' + regexStr + '$')
|
|
37
|
+
const m = regex.exec(path)
|
|
38
|
+
if (!m) return { matches: false, params: {} as Record<string, string> }
|
|
39
|
+
|
|
40
|
+
const params: Record<string, string> = {}
|
|
41
|
+
for (let i = 0; i < names.length; i++) {
|
|
42
|
+
params[names[i]] = decodeURIComponent(m[i + 1])
|
|
43
|
+
}
|
|
44
|
+
return { matches: true, params }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Router({path, children, currentPath}: IRouterProps){
|
|
48
|
+
const { matches, params } = matchPath(path, currentPath)
|
|
49
|
+
console.log(`Router ${path}:`, { currentPath, matches, params, children })
|
|
50
|
+
|
|
51
|
+
if (!matches && path != "*") return null
|
|
52
|
+
|
|
53
|
+
if (children.type === 'component') {
|
|
54
|
+
children.props = { ...children.props, ...params }
|
|
55
|
+
console.log(`render children ${path}`)
|
|
56
|
+
return (<div>{children}</div>)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (<div>{children}</div>)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ISwitchrProps{
|
|
63
|
+
children: any[]
|
|
64
|
+
currentPath: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function Switch({ currentPath, children }: ISwitchrProps) {
|
|
68
|
+
const childrenArray = Array.isArray(children) ? children : [children];
|
|
69
|
+
|
|
70
|
+
for (const child of childrenArray) {
|
|
71
|
+
if (child && child.type === 'component' && child.props.path) {
|
|
72
|
+
const { matches } = matchPath(child.props.path, currentPath);
|
|
73
|
+
if (matches || child.props.path == "*") {
|
|
74
|
+
return <div>{child}</div>;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
package/src/types/dom.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Представление DOM-элемента в виртуальном DOM.
|
|
3
|
+
* Содержит реальный DOM-узел, атрибуты, дочерние элементы и обработчики событий.
|
|
4
|
+
*/
|
|
5
|
+
export interface DOMElement {
|
|
6
|
+
/** Тип узла */
|
|
7
|
+
type: "element";
|
|
8
|
+
/** Реальный DOM-элемент браузера */
|
|
9
|
+
elem: Element;
|
|
10
|
+
/** Карта атрибутов элемента (ключ-значение) */
|
|
11
|
+
attrs: Map<string, any>;
|
|
12
|
+
/** Массив дочерних DOM-узлов */
|
|
13
|
+
children: (DOMElement | DOMTextNode)[];
|
|
14
|
+
/** Массив обработчиков событий элемента */
|
|
15
|
+
eventListeners: { type: string; callback: () => void; }[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Представление текстового DOM-узла в виртуальном DOM.
|
|
20
|
+
* Используется для рендера строкового контента между элементами.
|
|
21
|
+
*/
|
|
22
|
+
export interface DOMTextNode {
|
|
23
|
+
/** Тип узла */
|
|
24
|
+
type: "textNode";
|
|
25
|
+
/** Текстовое содержимое */
|
|
26
|
+
text: string;
|
|
27
|
+
/** Реальный DOM текстовый узел */
|
|
28
|
+
node: Node;
|
|
29
|
+
}
|
package/src/types/jsx.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type any1 = any;
|
|
2
|
+
export type KeyType = string;
|
|
3
|
+
|
|
4
|
+
export namespace JSX{
|
|
5
|
+
export interface IntrinsicAttributes extends any1{
|
|
6
|
+
key?: KeyType;
|
|
7
|
+
}
|
|
8
|
+
export type SVGAttributes = any;
|
|
9
|
+
export type HTMLAttributes = any;
|
|
10
|
+
export interface IntrinsicElements extends any1{}
|
|
11
|
+
}
|
|
12
|
+
export interface ComponentPropsType {
|
|
13
|
+
key: KeyType;
|
|
14
|
+
onClick: ()=>any | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface JSXElement {
|
|
18
|
+
type: "element"
|
|
19
|
+
tagName: string;
|
|
20
|
+
attributes: Map<string, any>;
|
|
21
|
+
children: NormalizedChildrenType;
|
|
22
|
+
}
|
|
23
|
+
export interface JSXComponent<PropsType extends ComponentPropsType>{
|
|
24
|
+
type: "component";
|
|
25
|
+
func: (props: any) => JSXElementType;
|
|
26
|
+
key: KeyType;
|
|
27
|
+
props: PropsType;
|
|
28
|
+
children: NormalizedChildrenType;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type JSXElementType = JSXElement | JSXComponent<any> ;
|
|
32
|
+
|
|
33
|
+
export type ChildrenType =
|
|
34
|
+
| JSXElementType
|
|
35
|
+
| string
|
|
36
|
+
| (JSXElementType | string)[]
|
|
37
|
+
| undefined;
|
|
38
|
+
|
|
39
|
+
export type NormalizedChildrenType = (JSXElementType | string)[];
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"paths":{
|
|
15
|
+
"@my-react/*": ["./src/*"]
|
|
16
|
+
},
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
"jsxImportSource": "@my-react",
|
|
19
|
+
"noEmit": true,
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|