@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
526 lines • 21.4 kB
JavaScript
/**
* @fileoverview OrdoJS DOM Optimizer - Generates efficient DOM update code
*/
import { DirectiveType, ExpressionType } from '../types/index.js';
import {} from './dependency-analyzer.js';
/**
* Types of DOM update operations
*/
export var DOMUpdateType;
(function (DOMUpdateType) {
DOMUpdateType["TEXT_CONTENT"] = "TEXT_CONTENT";
DOMUpdateType["ATTRIBUTE"] = "ATTRIBUTE";
DOMUpdateType["PROPERTY"] = "PROPERTY";
DOMUpdateType["CLASS_TOGGLE"] = "CLASS_TOGGLE";
DOMUpdateType["STYLE"] = "STYLE";
DOMUpdateType["VISIBILITY"] = "VISIBILITY";
DOMUpdateType["LIST_UPDATE"] = "LIST_UPDATE";
DOMUpdateType["CONDITIONAL_RENDER"] = "CONDITIONAL_RENDER";
})(DOMUpdateType || (DOMUpdateType = {}));
/**
* DOM optimizer for efficient DOM manipulation code generation
*/
export class DOMOptimizer {
componentId = '';
updateOperations = new Map();
updateBatches = [];
twoWayBindings = [];
elementCounter = 0;
/**
* Optimize DOM updates for a component
*/
optimize(ast, dependencyGraph) {
this.reset();
this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`;
// Analyze the component for DOM update opportunities
this.analyzeComponent(ast.component, dependencyGraph);
// Generate batched update operations
this.generateUpdateBatches();
// Generate the optimized update code
const updateCode = this.generateUpdateCode();
const setupCode = this.generateSetupCode();
const cleanupCode = this.generateCleanupCode();
return {
updateCode,
setupCode,
cleanupCode
};
}
/**
* Generate selective DOM updates (only changed elements)
*/
generateSelectiveUpdates(ast, dependencyGraph, changedVariables) {
this.reset();
this.componentId = `ordojs_${ast.component.name}_${Date.now().toString(36)}`;
// Only analyze operations that depend on changed variables
this.analyzeComponent(ast.component, dependencyGraph, changedVariables);
this.generateUpdateBatches();
return this.generateUpdateCode();
}
/**
* Reset optimizer state
*/
reset() {
this.componentId = '';
this.updateOperations.clear();
this.updateBatches = [];
this.twoWayBindings = [];
this.elementCounter = 0;
}
/**
* Analyze component for DOM update opportunities
*/
analyzeComponent(component, dependencyGraph, changedVariables) {
if (!component.markupBlock)
return;
// Analyze markup block for update operations
this.analyzeMarkupBlock(component.markupBlock, dependencyGraph, changedVariables);
// Analyze reactive variables for two-way bindings
if (component.clientBlock) {
this.analyzeTwoWayBindings(component.markupBlock, component.clientBlock.reactiveVariables);
}
}
/**
* Analyze markup block for DOM updates
*/
analyzeMarkupBlock(markupBlock, dependencyGraph, changedVariables) {
// Analyze interpolations
for (const interpolation of markupBlock.interpolations) {
this.analyzeInterpolation(interpolation, dependencyGraph, changedVariables);
}
// Analyze HTML elements
for (const element of markupBlock.elements) {
this.analyzeHTMLElement(element, dependencyGraph, changedVariables);
}
}
/**
* Analyze HTML element for DOM updates
*/
analyzeHTMLElement(element, dependencyGraph, changedVariables, elementPath = '') {
const elementId = this.generateElementId();
const currentPath = elementPath ? `${elementPath} > ${element.tagName}` : element.tagName;
// Analyze attributes for reactive updates
for (const attr of element.attributes) {
this.analyzeAttribute(attr, elementId, dependencyGraph, changedVariables);
}
// Recursively analyze children
for (let i = 0; i < element.children.length; i++) {
const child = element.children[i];
if (child.type === 'HTMLElement') {
this.analyzeHTMLElement(child, dependencyGraph, changedVariables, `${currentPath}:nth-child(${i + 1})`);
}
else if (child.type === 'Interpolation') {
this.analyzeInterpolation(child, dependencyGraph, changedVariables, `${currentPath}:nth-child(${i + 1})`);
}
}
}
/**
* Analyze attribute for reactive updates
*/
analyzeAttribute(attr, elementId, dependencyGraph, changedVariables) {
if (!attr.isDirective || typeof attr.value === 'string') {
return;
}
const expression = attr.value;
const dependencies = this.extractDependencies(expression, dependencyGraph);
// Filter by changed variables if specified
if (changedVariables && !dependencies.some(dep => changedVariables.includes(dep))) {
return;
}
const selector = `[data-ordojs-id="${elementId}"]`;
switch (attr.directiveType) {
case DirectiveType.BIND:
this.createTwoWayBinding(attr, elementId, dependencies);
break;
case DirectiveType.ON:
// Event handlers don't need DOM updates, just setup
break;
case DirectiveType.CLASS:
this.createClassUpdate(attr, selector, expression, dependencies);
break;
case DirectiveType.STYLE:
this.createStyleUpdate(attr, selector, expression, dependencies);
break;
default:
this.createAttributeUpdate(attr, selector, expression, dependencies);
break;
}
}
/**
* Analyze interpolation for reactive updates
*/
analyzeInterpolation(interpolation, dependencyGraph, changedVariables, elementPath) {
const dependencies = this.extractDependencies(interpolation.expression, dependencyGraph);
// Filter by changed variables if specified
if (changedVariables && !dependencies.some(dep => changedVariables.includes(dep))) {
return;
}
const elementId = this.generateElementId();
const selector = `[data-ordojs-interpolation="${elementId}"]`;
const operation = {
id: this.generateOperationId(),
type: DOMUpdateType.TEXT_CONTENT,
selector,
property: 'textContent',
expression: this.generateExpressionCode(interpolation.expression),
dependencies
};
this.addUpdateOperation(dependencies[0] || 'default', operation);
}
/**
* Create two-way binding
*/
createTwoWayBinding(attr, elementId, dependencies) {
const bindProperty = attr.name.substring(5); // Remove 'bind:' prefix
const selector = `[data-ordojs-id="${elementId}"]`;
// Determine event type based on property
let eventType = 'input';
let propertyName = 'value';
switch (bindProperty) {
case 'checked':
eventType = 'change';
propertyName = 'checked';
break;
case 'value':
eventType = 'input';
propertyName = 'value';
break;
default:
eventType = 'input';
propertyName = bindProperty;
}
const binding = {
elementSelector: selector,
variableName: dependencies[0] || '',
eventType,
propertyName
};
this.twoWayBindings.push(binding);
// Also create a property update operation
const operation = {
id: this.generateOperationId(),
type: DOMUpdateType.PROPERTY,
selector,
property: propertyName,
expression: `component.state.${binding.variableName}`,
dependencies
};
this.addUpdateOperation(binding.variableName, operation);
}
/**
* Create class update operation
*/
createClassUpdate(attr, selector, expression, dependencies) {
const className = attr.name.substring(6); // Remove 'class:' prefix
const operation = {
id: this.generateOperationId(),
type: DOMUpdateType.CLASS_TOGGLE,
selector,
property: className,
expression: this.generateExpressionCode(expression),
dependencies
};
this.addUpdateOperation(dependencies[0] || 'default', operation);
}
/**
* Create style update operation
*/
createStyleUpdate(attr, selector, expression, dependencies) {
const styleProperty = attr.name.substring(6); // Remove 'style:' prefix
const operation = {
id: this.generateOperationId(),
type: DOMUpdateType.STYLE,
selector,
property: styleProperty,
expression: this.generateExpressionCode(expression),
dependencies
};
this.addUpdateOperation(dependencies[0] || 'default', operation);
}
/**
* Create attribute update operation
*/
createAttributeUpdate(attr, selector, expression, dependencies) {
const operation = {
id: this.generateOperationId(),
type: DOMUpdateType.ATTRIBUTE,
selector,
property: attr.name,
expression: this.generateExpressionCode(expression),
dependencies
};
this.addUpdateOperation(dependencies[0] || 'default', operation);
}
/**
* Analyze two-way bindings
*/
analyzeTwoWayBindings(markupBlock, reactiveVariables) {
// This is handled in analyzeAttribute method
// Additional analysis could be added here if needed
}
/**
* Extract dependencies from expression
*/
extractDependencies(expression, dependencyGraph) {
const dependencies = [];
const extractFromExpr = (expr) => {
switch (expr.expressionType) {
case ExpressionType.IDENTIFIER:
if (expr.identifier && dependencyGraph.nodes.has(expr.identifier)) {
dependencies.push(expr.identifier);
}
break;
case ExpressionType.BINARY:
case ExpressionType.ASSIGNMENT:
if (expr.left)
extractFromExpr(expr.left);
if (expr.right)
extractFromExpr(expr.right);
break;
case ExpressionType.UNARY:
if (expr.right)
extractFromExpr(expr.right);
break;
case ExpressionType.CALL:
if (expr.callee)
extractFromExpr(expr.callee);
if (expr.arguments) {
expr.arguments.forEach(arg => extractFromExpr(arg));
}
break;
case ExpressionType.MEMBER:
if (expr.object)
extractFromExpr(expr.object);
// For member expressions, we don't extract dependencies from the property
// since it's usually a literal property name, but we still need to check
// if the property itself is an identifier that references a reactive variable
if (expr.property && expr.property.expressionType === ExpressionType.IDENTIFIER) {
if (expr.property.identifier && dependencyGraph.nodes.has(expr.property.identifier)) {
dependencies.push(expr.property.identifier);
}
}
break;
}
};
extractFromExpr(expression);
return [...new Set(dependencies)]; // Remove duplicates
}
/**
* Generate expression code
*/
generateExpressionCode(expression) {
switch (expression.expressionType) {
case ExpressionType.LITERAL:
return typeof expression.value === 'string'
? `"${expression.value}"`
: String(expression.value);
case ExpressionType.IDENTIFIER:
return `component.state.${expression.identifier}`;
case ExpressionType.BINARY:
if (expression.left && expression.right && expression.operator) {
const left = this.generateExpressionCode(expression.left);
const right = this.generateExpressionCode(expression.right);
return `(${left} ${expression.operator} ${right})`;
}
return '';
case ExpressionType.UNARY:
if (expression.right && expression.operator) {
const right = this.generateExpressionCode(expression.right);
return `(${expression.operator}${right})`;
}
return '';
case ExpressionType.CALL:
if (expression.callee && expression.arguments) {
const callee = this.generateExpressionCode(expression.callee);
const args = expression.arguments
.map(arg => this.generateExpressionCode(arg))
.join(', ');
return `${callee}(${args})`;
}
return '';
case ExpressionType.MEMBER:
if (expression.object && expression.property) {
const object = this.generateExpressionCode(expression.object);
// For member expressions, the property should be treated as a literal property name
const property = expression.property.expressionType === ExpressionType.IDENTIFIER
? expression.property.identifier
: this.generateExpressionCode(expression.property);
return `${object}.${property}`;
}
return '';
default:
return '';
}
}
/**
* Add update operation to the appropriate group
*/
addUpdateOperation(variableName, operation) {
// Use the first dependency as the key, or 'default' if no dependencies
const key = operation.dependencies.length > 0 ? operation.dependencies[0] : 'default';
if (!this.updateOperations.has(key)) {
this.updateOperations.set(key, []);
}
this.updateOperations.get(key).push(operation);
}
/**
* Generate batched update operations
*/
generateUpdateBatches() {
let batchId = 0;
for (const [variableName, operations] of this.updateOperations) {
// Create a single batch per variable to reduce the number of batches
const batch = {
id: `batch_${batchId++}`,
operations,
dependencies: [variableName],
priority: Math.min(...operations.map(op => this.getUpdatePriority(op.type)))
};
this.updateBatches.push(batch);
}
// Sort batches by priority
this.updateBatches.sort((a, b) => a.priority - b.priority);
}
/**
* Get update priority for operation type
*/
getUpdatePriority(type) {
switch (type) {
case DOMUpdateType.VISIBILITY:
return 1;
case DOMUpdateType.CLASS_TOGGLE:
return 2;
case DOMUpdateType.STYLE:
return 3;
case DOMUpdateType.ATTRIBUTE:
return 4;
case DOMUpdateType.PROPERTY:
return 5;
case DOMUpdateType.TEXT_CONTENT:
return 6;
case DOMUpdateType.LIST_UPDATE:
return 7;
case DOMUpdateType.CONDITIONAL_RENDER:
return 8;
default:
return 9;
}
}
/**
* Generate optimized update code
*/
generateUpdateCode() {
const lines = [];
lines.push('// Optimized DOM update functions');
lines.push('const updateFunctions = {');
for (const batch of this.updateBatches) {
lines.push(` ${batch.id}: function() {`);
lines.push(' // Batch DOM updates for better performance');
// Group operations by selector for efficiency
const operationsBySelector = new Map();
for (const operation of batch.operations) {
if (!operationsBySelector.has(operation.selector)) {
operationsBySelector.set(operation.selector, []);
}
operationsBySelector.get(operation.selector).push(operation);
}
// Generate update code for each selector
for (const [selector, operations] of operationsBySelector) {
lines.push(` const elements = document.querySelectorAll('${selector}');`);
lines.push(' elements.forEach(el => {');
for (const operation of operations) {
lines.push(` // ${operation.type}: ${operation.property}`);
switch (operation.type) {
case DOMUpdateType.TEXT_CONTENT:
lines.push(` el.textContent = ${operation.expression};`);
break;
case DOMUpdateType.ATTRIBUTE:
lines.push(` el.setAttribute('${operation.property}', ${operation.expression});`);
break;
case DOMUpdateType.PROPERTY:
lines.push(` el.${operation.property} = ${operation.expression};`);
break;
case DOMUpdateType.CLASS_TOGGLE:
lines.push(` el.classList.toggle('${operation.property}', !!(${operation.expression}));`);
break;
case DOMUpdateType.STYLE:
lines.push(` el.style.${operation.property} = ${operation.expression};`);
break;
}
}
lines.push(' });');
}
lines.push(' },');
}
lines.push('};');
lines.push('');
// Generate main update function
lines.push('function updateDOM(component, changedVariables = []) {');
lines.push(' // Execute update batches based on changed variables');
for (const batch of this.updateBatches) {
const condition = batch.dependencies
.map(dep => `changedVariables.includes('${dep}')`)
.join(' || ');
lines.push(` if (changedVariables.length === 0 || ${condition}) {`);
lines.push(` updateFunctions.${batch.id}();`);
lines.push(' }');
}
lines.push('}');
return lines.join('\n');
}
/**
* Generate setup code for two-way bindings
*/
generateSetupCode() {
if (this.twoWayBindings.length === 0) {
return '';
}
const lines = [];
lines.push('// Setup two-way data bindings');
lines.push('function setupTwoWayBindings(component) {');
for (const binding of this.twoWayBindings) {
lines.push(` const ${binding.variableName}Elements = document.querySelectorAll('${binding.elementSelector}');`);
lines.push(` ${binding.variableName}Elements.forEach(el => {`);
lines.push(` el.addEventListener('${binding.eventType}', (event) => {`);
if (binding.transformFunction) {
lines.push(` const value = ${binding.transformFunction}(event.target.${binding.propertyName});`);
}
else {
lines.push(` const value = event.target.${binding.propertyName};`);
}
lines.push(` component.state.${binding.variableName} = value;`);
lines.push(' });');
lines.push(' });');
}
lines.push('}');
return lines.join('\n');
}
/**
* Generate cleanup code
*/
generateCleanupCode() {
if (this.twoWayBindings.length === 0) {
return '';
}
const lines = [];
lines.push('// Cleanup event listeners');
lines.push('function cleanupBindings() {');
lines.push(' // Event listeners will be automatically cleaned up when elements are removed');
lines.push(' // Additional cleanup logic can be added here if needed');
lines.push('}');
return lines.join('\n');
}
/**
* Generate unique element ID
*/
generateElementId() {
return `${this.componentId}_el_${this.elementCounter++}`;
}
/**
* Generate unique operation ID
*/
generateOperationId() {
return `op_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
}
}
//# sourceMappingURL=dom-optimizer.js.map