onelaraveljs 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 +87 -0
- package/docs/integration_analysis.md +116 -0
- package/docs/onejs_analysis.md +108 -0
- package/docs/optimization_implementation_group2.md +458 -0
- package/docs/optimization_plan.md +130 -0
- package/index.js +16 -0
- package/package.json +13 -0
- package/src/app.js +61 -0
- package/src/core/API.js +72 -0
- package/src/core/ChildrenRegistry.js +410 -0
- package/src/core/DOMBatcher.js +207 -0
- package/src/core/ErrorBoundary.js +226 -0
- package/src/core/EventDelegator.js +416 -0
- package/src/core/Helper.js +817 -0
- package/src/core/LoopContext.js +97 -0
- package/src/core/OneDOM.js +246 -0
- package/src/core/OneMarkup.js +444 -0
- package/src/core/Router.js +996 -0
- package/src/core/SEOConfig.js +321 -0
- package/src/core/SectionEngine.js +75 -0
- package/src/core/TemplateEngine.js +83 -0
- package/src/core/View.js +273 -0
- package/src/core/ViewConfig.js +229 -0
- package/src/core/ViewController.js +1410 -0
- package/src/core/ViewControllerOptimized.js +164 -0
- package/src/core/ViewIdentifier.js +361 -0
- package/src/core/ViewLoader.js +272 -0
- package/src/core/ViewManager.js +1962 -0
- package/src/core/ViewState.js +761 -0
- package/src/core/ViewSystem.js +301 -0
- package/src/core/ViewTemplate.js +4 -0
- package/src/core/helpers/BindingHelper.js +239 -0
- package/src/core/helpers/ConfigHelper.js +37 -0
- package/src/core/helpers/EventHelper.js +172 -0
- package/src/core/helpers/LifecycleHelper.js +17 -0
- package/src/core/helpers/ReactiveHelper.js +169 -0
- package/src/core/helpers/RenderHelper.js +15 -0
- package/src/core/helpers/ResourceHelper.js +89 -0
- package/src/core/helpers/TemplateHelper.js +11 -0
- package/src/core/managers/BindingManager.js +671 -0
- package/src/core/managers/ConfigurationManager.js +136 -0
- package/src/core/managers/EventManager.js +309 -0
- package/src/core/managers/LifecycleManager.js +356 -0
- package/src/core/managers/ReactiveManager.js +334 -0
- package/src/core/managers/RenderEngine.js +292 -0
- package/src/core/managers/ResourceManager.js +441 -0
- package/src/core/managers/ViewHierarchyManager.js +258 -0
- package/src/core/managers/ViewTemplateManager.js +127 -0
- package/src/core/reactive/ReactiveComponent.js +592 -0
- package/src/core/services/EventService.js +418 -0
- package/src/core/services/HttpService.js +106 -0
- package/src/core/services/LoggerService.js +57 -0
- package/src/core/services/StateService.js +512 -0
- package/src/core/services/StorageService.js +856 -0
- package/src/core/services/StoreService.js +258 -0
- package/src/core/services/TemplateDetectorService.js +361 -0
- package/src/core/services/Test.js +18 -0
- package/src/helpers/devWarnings.js +205 -0
- package/src/helpers/performance.js +226 -0
- package/src/helpers/utils.js +287 -0
- package/src/init.js +343 -0
- package/src/plugins/auto-plugin.js +34 -0
- package/src/services/Test.js +18 -0
- package/src/types/index.js +193 -0
- package/src/utils/date-helper.js +51 -0
- package/src/utils/helpers.js +39 -0
- package/src/utils/validation.js +32 -0
package/src/core/API.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Module
|
|
3
|
+
* ES6 Module for Blade Compiler
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Import App from global init
|
|
7
|
+
import { HttpService } from './services/HttpService.js';
|
|
8
|
+
|
|
9
|
+
export class AppAPI {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.http = new HttpService();
|
|
12
|
+
this.http.setHeader('Content-Type', 'application/json');
|
|
13
|
+
this.http.setHeader('Accept', 'application/json');
|
|
14
|
+
this.http.setHeader('X-Requested-With', 'XMLHttpRequest');
|
|
15
|
+
// Conditional document access for Node.js compatibility
|
|
16
|
+
if (typeof document !== 'undefined') {
|
|
17
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]');
|
|
18
|
+
if (csrfToken) {
|
|
19
|
+
this.http.setHeader('X-CSRF-TOKEN', csrfToken.getAttribute('content'));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
this.http.setHeader('X-DATA-TYPE', 'json');
|
|
23
|
+
this.endpoints = typeof window !== 'undefined' ? window.APP_CONFIGS?.api?.endpoints || {} : {};
|
|
24
|
+
}
|
|
25
|
+
async getViewData(uri) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.http.get(uri).then(response => {
|
|
28
|
+
return response.status ? response.data : {};
|
|
29
|
+
}).catch(error => {
|
|
30
|
+
console.error('❌ AppAPI: Error getting view data:', error);
|
|
31
|
+
return {};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return response;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('❌ AppAPI: Error getting view data:', error);
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
async getSystemConfig() {
|
|
42
|
+
return {};
|
|
43
|
+
return this.http.get('/api/system/config').then(response => {
|
|
44
|
+
return response.status ? response.data : {};
|
|
45
|
+
}).catch(error => {
|
|
46
|
+
console.error('❌ AppAPI: Error getting system config:', error);
|
|
47
|
+
return {};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async getSystemData() {
|
|
51
|
+
let endpoint = this.endpoints?.system?.data;
|
|
52
|
+
if (!endpoint) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
return this.http.get(endpoint).then(response => {
|
|
56
|
+
return response.status ? response.data : {};
|
|
57
|
+
}).catch(error => {
|
|
58
|
+
console.error('❌ AppAPI: Error getting system data:', error);
|
|
59
|
+
return {};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getURIDAta() {
|
|
64
|
+
const uri = typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/';
|
|
65
|
+
return this.getViewData(uri);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const API = new AppAPI();
|
|
71
|
+
|
|
72
|
+
export default API;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChildrenRegistry - Centralized Children Management System
|
|
3
|
+
*
|
|
4
|
+
* Replaces the complex 4-array tracking system (scanChildrenIDs, renewChildrenIDs, rcChildrenIDs, children)
|
|
5
|
+
* with a unified Map-based registry for better:
|
|
6
|
+
* - Memory management (automatic cleanup)
|
|
7
|
+
* - Lifecycle coordination (smart mount/unmount)
|
|
8
|
+
* - State synchronization (reactive data binding)
|
|
9
|
+
* - Performance (efficient queries and diffs)
|
|
10
|
+
*
|
|
11
|
+
* @see docs/dev/CHILDREN_VIEW_MANAGEMENT_ANALYSIS.md
|
|
12
|
+
* @see docs/dev/CONTROLLER_VIEW_SEPARATION.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ChildrenRegistry Class
|
|
17
|
+
*
|
|
18
|
+
* Centralized management for all child views with:
|
|
19
|
+
* - Map-based storage for O(1) lookups
|
|
20
|
+
* - Automatic lifecycle management
|
|
21
|
+
* - Reactive data binding support
|
|
22
|
+
* - Memory leak prevention
|
|
23
|
+
*/
|
|
24
|
+
export class ChildrenRegistry {
|
|
25
|
+
/**
|
|
26
|
+
* @param {ViewController} controller - Parent controller
|
|
27
|
+
*/
|
|
28
|
+
constructor(controller) {
|
|
29
|
+
/** @type {ViewController} */
|
|
30
|
+
this.controller = controller;
|
|
31
|
+
|
|
32
|
+
/** @type {Map<string, Object>} Main registry: childId → ChildNode */
|
|
33
|
+
this.registry = new Map();
|
|
34
|
+
|
|
35
|
+
/** @type {Set<string>} Set of currently mounted child IDs */
|
|
36
|
+
this.mountedChildren = new Set();
|
|
37
|
+
|
|
38
|
+
/** @type {Map<string, Set<string>>} Reactive component → child IDs mapping */
|
|
39
|
+
this.reactiveComponentChildren = new Map();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register a child view
|
|
44
|
+
*
|
|
45
|
+
* Stores the controller internally and provides view access via getter.
|
|
46
|
+
* Links child to parent reactive component if applicable.
|
|
47
|
+
*
|
|
48
|
+
* @param {View} child - Child view instance (child.__ is ViewController)
|
|
49
|
+
* @param {Object} options - Registration options
|
|
50
|
+
* @param {Object} options.data - Initial data for child
|
|
51
|
+
* @param {Object} options.parentReactiveComponent - Parent reactive component if applicable
|
|
52
|
+
* @param {number} options.index - Child index
|
|
53
|
+
* @returns {Object} ChildNode
|
|
54
|
+
*/
|
|
55
|
+
register(child, options = {}) {
|
|
56
|
+
const {
|
|
57
|
+
data = {},
|
|
58
|
+
parentReactiveComponent = null,
|
|
59
|
+
index = this.registry.size
|
|
60
|
+
} = options;
|
|
61
|
+
|
|
62
|
+
const childCtrl = child.__; // Get controller from view
|
|
63
|
+
|
|
64
|
+
const childNode = {
|
|
65
|
+
controller: childCtrl,
|
|
66
|
+
get view() {
|
|
67
|
+
return childCtrl.view; // ✅ Fixed: return child controller's view
|
|
68
|
+
},
|
|
69
|
+
state: 'pending',
|
|
70
|
+
lifecycle: {
|
|
71
|
+
created: Date.now(),
|
|
72
|
+
mounted: null,
|
|
73
|
+
unmounted: null
|
|
74
|
+
},
|
|
75
|
+
scope: {
|
|
76
|
+
name: childCtrl.view.path,
|
|
77
|
+
id: childCtrl.view.id,
|
|
78
|
+
index,
|
|
79
|
+
data: this._createReactiveData(data),
|
|
80
|
+
subscriptions: new Set()
|
|
81
|
+
},
|
|
82
|
+
parent: this.controller,
|
|
83
|
+
parentReactiveComponent
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this.registry.set(childCtrl.view.id, childNode);
|
|
87
|
+
|
|
88
|
+
// Track in reactive component if applicable
|
|
89
|
+
if (parentReactiveComponent) {
|
|
90
|
+
if (!this.reactiveComponentChildren.has(parentReactiveComponent.id)) {
|
|
91
|
+
this.reactiveComponentChildren.set(parentReactiveComponent.id, new Set());
|
|
92
|
+
}
|
|
93
|
+
this.reactiveComponentChildren.get(parentReactiveComponent.id).add(childCtrl.view.id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Link to main children array (backward compatibility)
|
|
97
|
+
if (!this.controller.children.includes(childCtrl)) {
|
|
98
|
+
this.controller.children.push(childCtrl);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return childNode;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create reactive data proxy for child scope
|
|
106
|
+
*
|
|
107
|
+
* Wraps data in a Proxy to detect changes and notify children.
|
|
108
|
+
*
|
|
109
|
+
* @param {Object} data - Initial data object
|
|
110
|
+
* @returns {Object} Proxied data object
|
|
111
|
+
*/
|
|
112
|
+
_createReactiveData(data) {
|
|
113
|
+
return new Proxy(data, {
|
|
114
|
+
set: (target, key, value) => {
|
|
115
|
+
const oldValue = target[key];
|
|
116
|
+
target[key] = value;
|
|
117
|
+
|
|
118
|
+
// Notify child about data change
|
|
119
|
+
if (oldValue !== value) {
|
|
120
|
+
this._notifyChildDataChange(key, value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get child node by ID
|
|
130
|
+
*
|
|
131
|
+
* @param {string} childId - Child view ID
|
|
132
|
+
* @returns {Object|undefined} ChildNode or undefined
|
|
133
|
+
*/
|
|
134
|
+
get(childId) {
|
|
135
|
+
return this.registry.get(childId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get all children for a reactive component
|
|
140
|
+
*
|
|
141
|
+
* @param {string} reactiveComponentId - Reactive component ID
|
|
142
|
+
* @returns {Array<Object>} Array of ChildNodes
|
|
143
|
+
*/
|
|
144
|
+
getReactiveComponentChildren(reactiveComponentId) {
|
|
145
|
+
const childIds = this.reactiveComponentChildren.get(reactiveComponentId);
|
|
146
|
+
if (!childIds) return [];
|
|
147
|
+
|
|
148
|
+
return Array.from(childIds)
|
|
149
|
+
.map(id => this.registry.get(id))
|
|
150
|
+
.filter(node => node !== undefined);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Mark child as mounted
|
|
155
|
+
*
|
|
156
|
+
* Updates state, timestamps, and calls child's mounted lifecycle hook.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} childId - Child view ID
|
|
159
|
+
*/
|
|
160
|
+
mount(childId) {
|
|
161
|
+
const childNode = this.registry.get(childId);
|
|
162
|
+
if (!childNode) return;
|
|
163
|
+
|
|
164
|
+
// Prevent double mounting
|
|
165
|
+
if (childNode.state === 'mounted' || this.mountedChildren.has(childId)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
childNode.state = 'mounted';
|
|
170
|
+
childNode.lifecycle.mounted = Date.now();
|
|
171
|
+
this.mountedChildren.add(childId);
|
|
172
|
+
|
|
173
|
+
// Call child controller's mounted hook
|
|
174
|
+
if (childNode.controller._lifecycleManager) {
|
|
175
|
+
childNode.controller._lifecycleManager.mounted();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Unmount child
|
|
181
|
+
*
|
|
182
|
+
* Updates state, timestamps, and calls child's unmounted lifecycle hook.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} childId - Child view ID
|
|
185
|
+
*/
|
|
186
|
+
unmount(childId) {
|
|
187
|
+
const childNode = this.registry.get(childId);
|
|
188
|
+
if (!childNode) return;
|
|
189
|
+
|
|
190
|
+
// Only unmount if currently mounted
|
|
191
|
+
if (childNode.state !== 'mounted' && !this.mountedChildren.has(childId)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
childNode.state = 'unmounted';
|
|
196
|
+
childNode.lifecycle.unmounted = Date.now();
|
|
197
|
+
this.mountedChildren.delete(childId);
|
|
198
|
+
|
|
199
|
+
// Call child controller's unmounted hook
|
|
200
|
+
if (childNode.controller._lifecycleManager) {
|
|
201
|
+
childNode.controller._lifecycleManager.unmounted();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Unregister and destroy child
|
|
207
|
+
*
|
|
208
|
+
* Performs complete cleanup:
|
|
209
|
+
* 1. Unmounts if still mounted
|
|
210
|
+
* 2. Clears subscriptions
|
|
211
|
+
* 3. Removes from reactive component tracking
|
|
212
|
+
* 4. Destroys child controller
|
|
213
|
+
* 5. Removes from registry and main children array
|
|
214
|
+
*
|
|
215
|
+
* @param {string} childId - Child view ID
|
|
216
|
+
*/
|
|
217
|
+
destroy(childId) {
|
|
218
|
+
const childNode = this.registry.get(childId);
|
|
219
|
+
if (!childNode) return;
|
|
220
|
+
|
|
221
|
+
// Unmount if still mounted
|
|
222
|
+
if (this.mountedChildren.has(childId)) {
|
|
223
|
+
this.unmount(childId);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Clean up subscriptions
|
|
227
|
+
childNode.scope.subscriptions.forEach(unsub => unsub());
|
|
228
|
+
childNode.scope.subscriptions.clear();
|
|
229
|
+
|
|
230
|
+
// Remove from reactive component tracking
|
|
231
|
+
if (childNode.parentReactiveComponent) {
|
|
232
|
+
const rcId = childNode.parentReactiveComponent.id;
|
|
233
|
+
this.reactiveComponentChildren.get(rcId)?.delete(childId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Destroy child controller
|
|
237
|
+
if (childNode.controller._lifecycleManager) {
|
|
238
|
+
childNode.controller._lifecycleManager.destroy();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Remove from registry
|
|
242
|
+
this.registry.delete(childId);
|
|
243
|
+
|
|
244
|
+
// Note: Removal from controller.children array is handled by ViewHierarchyManager.removeChild()
|
|
245
|
+
// This keeps separation of concerns - ViewHierarchyManager manages the children array,
|
|
246
|
+
// ChildrenRegistry only manages the registry Map and tracking Sets
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Find children that can be reused (by path)
|
|
251
|
+
*
|
|
252
|
+
* Searches for unmounted children with matching path for reuse during
|
|
253
|
+
* reactive component updates.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} path - View path to search for
|
|
256
|
+
* @param {boolean} excludeMounted - Exclude mounted children from results
|
|
257
|
+
* @returns {Array<{id: string, childNode: Object}>} Array of candidates
|
|
258
|
+
*/
|
|
259
|
+
findReusable(path, excludeMounted = true) {
|
|
260
|
+
const candidates = [];
|
|
261
|
+
|
|
262
|
+
for (const [id, childNode] of this.registry) {
|
|
263
|
+
if (childNode.controller.view.path === path) {
|
|
264
|
+
if (excludeMounted && this.mountedChildren.has(id)) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
candidates.push({ id, childNode });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return candidates;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Clean up orphaned children
|
|
276
|
+
*
|
|
277
|
+
* Destroys children that are not in the active IDs set and are unmounted.
|
|
278
|
+
* Useful for cleaning up after reactive component updates.
|
|
279
|
+
*
|
|
280
|
+
* @param {Array<string>} activeIds - Set of currently active child IDs
|
|
281
|
+
* @returns {number} Number of children destroyed
|
|
282
|
+
*/
|
|
283
|
+
cleanupOrphaned(activeIds) {
|
|
284
|
+
const activeSet = new Set(activeIds);
|
|
285
|
+
const toDestroy = [];
|
|
286
|
+
|
|
287
|
+
for (const [id, childNode] of this.registry) {
|
|
288
|
+
if (!activeSet.has(id) && childNode.state !== 'mounted') {
|
|
289
|
+
toDestroy.push(id);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
toDestroy.forEach(id => this.destroy(id));
|
|
294
|
+
|
|
295
|
+
return toDestroy.length;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get all child IDs (for backward compatibility)
|
|
300
|
+
*
|
|
301
|
+
* @returns {Array<string>} Array of child IDs
|
|
302
|
+
*/
|
|
303
|
+
getAllIds() {
|
|
304
|
+
return Array.from(this.registry.keys());
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Update parent reactive component for a child
|
|
309
|
+
* Used when a child is reused in a different reactive component
|
|
310
|
+
*
|
|
311
|
+
* @param {string} childId - Child view ID
|
|
312
|
+
* @param {Object} newParentRC - New parent reactive component
|
|
313
|
+
*/
|
|
314
|
+
updateParentReactiveComponent(childId, newParentRC) {
|
|
315
|
+
const childNode = this.registry.get(childId);
|
|
316
|
+
if (!childNode) return;
|
|
317
|
+
|
|
318
|
+
// Remove from old reactive component tracking
|
|
319
|
+
if (childNode.parentReactiveComponent) {
|
|
320
|
+
const oldRcId = childNode.parentReactiveComponent.id;
|
|
321
|
+
this.reactiveComponentChildren.get(oldRcId)?.delete(childId);
|
|
322
|
+
|
|
323
|
+
// Clean up empty set
|
|
324
|
+
if (this.reactiveComponentChildren.get(oldRcId)?.size === 0) {
|
|
325
|
+
this.reactiveComponentChildren.delete(oldRcId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Update to new reactive component
|
|
330
|
+
childNode.parentReactiveComponent = newParentRC;
|
|
331
|
+
|
|
332
|
+
// Add to new reactive component tracking
|
|
333
|
+
if (newParentRC) {
|
|
334
|
+
if (!this.reactiveComponentChildren.has(newParentRC.id)) {
|
|
335
|
+
this.reactiveComponentChildren.set(newParentRC.id, new Set());
|
|
336
|
+
}
|
|
337
|
+
this.reactiveComponentChildren.get(newParentRC.id).add(childId);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get all mounted child IDs
|
|
343
|
+
*
|
|
344
|
+
* @returns {Array<string>} Array of mounted child IDs
|
|
345
|
+
*/
|
|
346
|
+
getMountedIds() {
|
|
347
|
+
return Array.from(this.mountedChildren);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Check if child is mounted
|
|
352
|
+
*
|
|
353
|
+
* @param {string} childId - Child view ID
|
|
354
|
+
* @returns {boolean} true if mounted
|
|
355
|
+
*/
|
|
356
|
+
isMounted(childId) {
|
|
357
|
+
return this.mountedChildren.has(childId);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get registry size
|
|
362
|
+
*
|
|
363
|
+
* @returns {number} Number of registered children
|
|
364
|
+
*/
|
|
365
|
+
get size() {
|
|
366
|
+
return this.registry.size;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Clear all children
|
|
371
|
+
*
|
|
372
|
+
* Destroys all children and clears all tracking structures.
|
|
373
|
+
*/
|
|
374
|
+
/**
|
|
375
|
+
* Clear all children
|
|
376
|
+
*
|
|
377
|
+
* Destroys all registered children and clears all tracking.
|
|
378
|
+
*/
|
|
379
|
+
clear() {
|
|
380
|
+
const allChildIds = Array.from(this.registry.keys());
|
|
381
|
+
allChildIds.forEach(childId => {
|
|
382
|
+
this.destroy(childId);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
this.registry.clear();
|
|
386
|
+
this.mountedChildren.clear();
|
|
387
|
+
this.reactiveComponentChildren.clear();
|
|
388
|
+
|
|
389
|
+
// Clear children array (ViewHierarchyManager's responsibility, but clear() is special case)
|
|
390
|
+
if (this.controller.children) {
|
|
391
|
+
this.controller.children = [];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Notify child about parent data change
|
|
397
|
+
*
|
|
398
|
+
* Called when reactive data in child scope changes.
|
|
399
|
+
* Children can subscribe to these changes for automatic updates.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} key - Data key that changed
|
|
402
|
+
* @param {any} value - New value
|
|
403
|
+
* @private
|
|
404
|
+
*/
|
|
405
|
+
_notifyChildDataChange(key, value) {
|
|
406
|
+
// Implementation depends on child's subscription mechanism
|
|
407
|
+
// This is a placeholder for future reactive binding features
|
|
408
|
+
// Currently, subscriptions are managed manually via scope.subscriptions
|
|
409
|
+
}
|
|
410
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOMBatcher
|
|
3
|
+
*
|
|
4
|
+
* Tối ưu hóa các thao tác DOM bằng cách gom nhóm đọc và ghi để ngăn layout thrashing.
|
|
5
|
+
* Sử dụng RequestAnimationFrame để lên lịch các thao tác theo batch.
|
|
6
|
+
*
|
|
7
|
+
* Lợi ích hiệu suất:
|
|
8
|
+
* - Ngăn chặn forced synchronous layouts (reflows)
|
|
9
|
+
* - Gom nhóm đọc trước ghi trong mỗi frame
|
|
10
|
+
* - Giảm tính toán layout từ O(n²) xuống O(n)
|
|
11
|
+
* - Cải thiện hiệu suất render cho UI động
|
|
12
|
+
*
|
|
13
|
+
* Mẫu sử dụng:
|
|
14
|
+
* ```javascript
|
|
15
|
+
* // XẤU: Gây layout thrashing
|
|
16
|
+
* element1.style.width = element2.offsetWidth + 'px'; // đọc-ghi
|
|
17
|
+
* element3.style.height = element4.offsetHeight + 'px'; // đọc-ghi
|
|
18
|
+
*
|
|
19
|
+
* // TỐT: Thao tác theo batch
|
|
20
|
+
* DOMBatcher.read(() => element2.offsetWidth)
|
|
21
|
+
* .then(width => DOMBatcher.write(() => element1.style.width = width + 'px'));
|
|
22
|
+
* DOMBatcher.read(() => element4.offsetHeight)
|
|
23
|
+
* .then(height => DOMBatcher.write(() => element3.style.height = height + 'px'));
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @class DOMBatcher
|
|
27
|
+
* @version 1.0.0
|
|
28
|
+
* @since 2025-01-06
|
|
29
|
+
*/
|
|
30
|
+
class DOMBatcher {
|
|
31
|
+
constructor() {
|
|
32
|
+
/** @type {Array<{fn: Function, resolve: Function, reject: Function}>} */
|
|
33
|
+
this.readQueue = [];
|
|
34
|
+
|
|
35
|
+
/** @type {Array<{fn: Function, resolve: Function, reject: Function}>} */
|
|
36
|
+
this.writeQueue = [];
|
|
37
|
+
|
|
38
|
+
/** @type {number|null} */
|
|
39
|
+
this.rafId = null;
|
|
40
|
+
|
|
41
|
+
/** @type {boolean} */
|
|
42
|
+
this.isProcessing = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Lên lịch thao tác đọc DOM
|
|
47
|
+
* Tất cả thao tác đọc được gom nhóm và thực thi trước ghi trong frame tiếp theo
|
|
48
|
+
*
|
|
49
|
+
* @param {Function} fn - Hàm đọc từ DOM (trả về giá trị)
|
|
50
|
+
* @returns {Promise<any>} Promise resolve với giá trị đọc được
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const width = await DOMBatcher.read(() => element.offsetWidth);
|
|
54
|
+
* const rect = await DOMBatcher.read(() => element.getBoundingClientRect());
|
|
55
|
+
*/
|
|
56
|
+
read(fn) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
this.readQueue.push({ fn, resolve, reject });
|
|
59
|
+
this.scheduleFlush();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Lên lịch thao tác ghi DOM
|
|
65
|
+
* Tất cả thao tác ghi được gom nhóm và thực thi sau đọc trong frame tiếp theo
|
|
66
|
+
*
|
|
67
|
+
* @param {Function} fn - Hàm ghi vào DOM
|
|
68
|
+
* @returns {Promise<void>} Promise resolve khi ghi hoàn thành
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* await DOMBatcher.write(() => element.style.width = '100px');
|
|
72
|
+
* await DOMBatcher.write(() => element.classList.add('active'));
|
|
73
|
+
*/
|
|
74
|
+
write(fn) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
this.writeQueue.push({ fn, resolve, reject });
|
|
77
|
+
this.scheduleFlush();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Lên lịch flush nếu chưa được lên lịch
|
|
83
|
+
* Sử dụng RAF để gom nhóm thao tác trong frame tiếp theo
|
|
84
|
+
*
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
scheduleFlush() {
|
|
88
|
+
if (this.rafId !== null) {
|
|
89
|
+
return; // Đã được lên lịch
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.rafId = requestAnimationFrame(() => {
|
|
93
|
+
this.flush();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Thực thi tất cả thao tác đã gom nhóm
|
|
99
|
+
* Đọc trước, sau đó ghi để ngăn layout thrashing
|
|
100
|
+
*
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
flush() {
|
|
104
|
+
if (this.isProcessing) {
|
|
105
|
+
// Lên lịch lại nếu đang xử lý
|
|
106
|
+
this.rafId = null;
|
|
107
|
+
this.scheduleFlush();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.isProcessing = true;
|
|
112
|
+
this.rafId = null;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Giai đoạn 1: Thực thi tất cả thao tác đọc
|
|
116
|
+
const reads = this.readQueue.splice(0);
|
|
117
|
+
reads.forEach(({ fn, resolve, reject }) => {
|
|
118
|
+
try {
|
|
119
|
+
const result = fn();
|
|
120
|
+
resolve(result);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('[DOMBatcher] Read error:', error);
|
|
123
|
+
reject(error);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Giai đoạn 2: Thực thi tất cả thao tác ghi
|
|
128
|
+
const writes = this.writeQueue.splice(0);
|
|
129
|
+
writes.forEach(({ fn, resolve, reject }) => {
|
|
130
|
+
try {
|
|
131
|
+
fn();
|
|
132
|
+
resolve();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('[DOMBatcher] Write error:', error);
|
|
135
|
+
reject(error);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
} finally {
|
|
139
|
+
this.isProcessing = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Xóa tất cả thao tác đang chờ và hủy RAF
|
|
145
|
+
* Dùng để dọn dẹp hoặc khôi phục lỗi
|
|
146
|
+
*/
|
|
147
|
+
clear() {
|
|
148
|
+
if (this.rafId !== null) {
|
|
149
|
+
cancelAnimationFrame(this.rafId);
|
|
150
|
+
this.rafId = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.readQueue.length = 0;
|
|
154
|
+
this.writeQueue.length = 0;
|
|
155
|
+
this.isProcessing = false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Đo nhiều element cùng lúc (đọc theo batch tối ưu)
|
|
160
|
+
*
|
|
161
|
+
* @param {Array<{element: HTMLElement, properties: string[]}>} measurements
|
|
162
|
+
* @returns {Promise<Array<Object>>} Mảng kết quả đo
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* const results = await DOMBatcher.measure([
|
|
166
|
+
* { element: el1, properties: ['offsetWidth', 'offsetHeight'] },
|
|
167
|
+
* { element: el2, properties: ['clientWidth', 'scrollTop'] }
|
|
168
|
+
* ]);
|
|
169
|
+
*/
|
|
170
|
+
measure(measurements) {
|
|
171
|
+
return this.read(() => {
|
|
172
|
+
return measurements.map(({ element, properties }) => {
|
|
173
|
+
const result = {};
|
|
174
|
+
properties.forEach(prop => {
|
|
175
|
+
result[prop] = element[prop];
|
|
176
|
+
});
|
|
177
|
+
return result;
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Áp dụng nhiều thay đổi style cùng lúc (ghi theo batch tối ưu)
|
|
184
|
+
*
|
|
185
|
+
* @param {Array<{element: HTMLElement, styles: Object}>} styleChanges
|
|
186
|
+
* @returns {Promise<void>}
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* await DOMBatcher.applyStyles([
|
|
190
|
+
* { element: el1, styles: { width: '100px', height: '50px' } },
|
|
191
|
+
* { element: el2, styles: { display: 'none' } }
|
|
192
|
+
* ]);
|
|
193
|
+
*/
|
|
194
|
+
applyStyles(styleChanges) {
|
|
195
|
+
return this.write(() => {
|
|
196
|
+
styleChanges.forEach(({ element, styles }) => {
|
|
197
|
+
Object.entries(styles).forEach(([prop, value]) => {
|
|
198
|
+
element.style[prop] = value;
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Export instance singleton
|
|
206
|
+
const batcher = new DOMBatcher();
|
|
207
|
+
export default batcher;
|