From 601381032d8f952c74432fc9a054ff604602b208 Mon Sep 17 00:00:00 2001 From: Hoikan <30694822+HoikanChan@users.noreply.github.com> Date: Wed, 8 May 2024 15:56:42 +0800 Subject: [PATCH] refactor(parse): use bitmap instead of dependency map --- .../src/analyzer/functionalMacroAnalyze.ts | 7 +- .../src/analyzer/nodeFactory.ts | 8 +-- .../src/analyzer/reactive/getDependencies.ts | 13 ++-- .../src/analyzer/types.ts | 9 ++- .../src/analyzer/variablesAnalyze.ts | 2 +- .../test/analyze/properties.test.ts | 16 ++--- .../test/analyze/viewAnalyze.test.ts | 37 ++-------- .../test/analyze/watchAnalyze.test.ts | 4 +- .../reactivity-parser/src/parser.ts | 70 +++++++------------ .../transpiler/reactivity-parser/src/types.ts | 6 +- 10 files changed, 63 insertions(+), 109 deletions(-) diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts index 6b9e024f..b8fc2589 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/functionalMacroAnalyze.ts @@ -19,6 +19,7 @@ import { addLifecycle, addWatch } from './nodeFactory'; import { types as t } from '@openinula/babel-api'; import { ON_MOUNT, ON_UNMOUNT, WATCH, WILL_MOUNT, WILL_UNMOUNT } from '../constants'; import { extractFnFromMacro, getFnBody } from '../utils'; +import { getDependenciesFromNode } from './reactive/getDependencies'; function isLifeCycleName(name: string): name is LifeCycle { return [WILL_MOUNT, ON_MOUNT, WILL_UNMOUNT, ON_UNMOUNT].includes(name); @@ -51,9 +52,9 @@ export function functionalMacroAnalyze(): Visitor { if (calleeName === WATCH) { const fnNode = extractFnFromMacro(expression, WATCH); const deps = getWatchDeps(expression); - if (!deps) { - // we auto collect the deps from the function body - } + + const depBits = getDependenciesFromNode(deps ?? fnNode, ctx); + addWatch(ctx.current, fnNode, deps); return; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts index 1008b2d6..da400b55 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/nodeFactory.ts @@ -14,7 +14,7 @@ */ import { NodePath, type types as t } from '@babel/core'; -import { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable } from './types'; +import type { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable, Bitmap } from './types'; import { PropType } from '../constants'; import { ViewParticle } from '@openinula/reactivity-parser'; @@ -53,7 +53,7 @@ export function addProperty(comp: ComponentNode, name: string, value: t.Expressi const bitmap = depBits ? depBits | bit : bit; comp._reactiveBitMap.set(name, bitmap); - comp.variables.push({ name, value, isComputed: !!depBits, type: 'reactive', bitmap, level: comp.level }); + comp.variables.push({ name, value, isComputed: !!depBits, type: 'reactive', depMask: bitmap, level: comp.level }); } export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) { @@ -87,13 +87,13 @@ export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: t export function addWatch( comp: ComponentNode, callback: NodePath | NodePath, - deps: NodePath | null + depMask: Bitmap ) { // if watch not exist, create a new one if (!comp.watch) { comp.watch = []; } - comp.watch.push({ callback, deps }); + comp.watch.push({ callback, depMask }); } export function setViewChild(comp: ComponentNode, view: ViewParticle[], usedPropertySet: Set) { diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/reactive/getDependencies.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/reactive/getDependencies.ts index 0013d6c9..6e103bfd 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/reactive/getDependencies.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/reactive/getDependencies.ts @@ -26,14 +26,13 @@ import { reactivityFuncNames } from '../../const'; * @returns */ export function getDependenciesFromNode( - propertyKey: string, path: NodePath, { current }: AnalyzeContext ) { // ---- Deps: console.log(count) - let depsBit = 0; + let depMask = 0; // ---- Assign deps: count = 1 or count++ - let assignDepBit = 0; + let assignDepMask = 0; const depNodes: Record = {}; const visitor = (innerPath: NodePath) => { @@ -42,9 +41,9 @@ export function getDependenciesFromNode( if (reactiveBitmap !== undefined) { if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) { - assignDepBit |= reactiveBitmap; + assignDepMask |= reactiveBitmap; } else { - depsBit |= reactiveBitmap; + depMask |= reactiveBitmap; if (!depNodes[propertyKey]) depNodes[propertyKey] = []; depNodes[propertyKey].push(t.cloneNode(innerPath.node)); } @@ -61,11 +60,11 @@ export function getDependenciesFromNode( // e.g. { console.log(count); count = 1 } // this will cause infinite loop // so we eliminate "count" from deps - if (assignDepBit & depsBit) { + if (assignDepMask & depMask) { // TODO: I think we should throw an error here to indicate the user that there is a loop } - return depsBit; + return depMask; } /** diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/types.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/types.ts index 0f0c83b2..52902c7b 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/types.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/types.ts @@ -15,10 +15,10 @@ import { type NodePath, types as t } from '@babel/core'; import { ON_MOUNT, ON_UNMOUNT, PropType, WILL_MOUNT, WILL_UNMOUNT } from '../constants'; -import { ViewParticle, PrevMap } from '@openinula/reactivity-parser'; +import { ViewParticle } from '@openinula/reactivity-parser'; export type LifeCycle = typeof WILL_MOUNT | typeof ON_MOUNT | typeof WILL_UNMOUNT | typeof ON_UNMOUNT; -type Bitmap = number; +export type Bitmap = number; export type FunctionalExpression = t.FunctionExpression | t.ArrowFunctionExpression; @@ -37,7 +37,7 @@ export interface ReactiveVariable extends BaseVariable { * let age = 18; // age's bitmap is 0x0010 * let greeting = `Hello, ${name}`; // greeting's bitmap is 0x0101 */ - bitmap: Bitmap; + depMask: Bitmap; // need a flag for computed to gen a getter // watch is a static computed isComputed: boolean; @@ -93,8 +93,7 @@ export interface ComponentNode { * The watch fn in the component */ watch?: { - bit: Bitmap; - deps: NodePath | null; + depMask: Bitmap; callback: NodePath | NodePath; }[]; } diff --git a/packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts b/packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts index 46f4bec5..a42dccc9 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyzer/variablesAnalyze.ts @@ -68,7 +68,7 @@ export function variablesAnalyze(): Visitor { return; } - depBits = getDependenciesFromNode(id.node.name, init, ctx); + depBits = getDependenciesFromNode(init, ctx); } addProperty(ctx.current, id.node.name, init.node || null, depBits); } diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts index 73f20bd2..fa4df586 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/properties.test.ts @@ -47,7 +47,7 @@ describe('analyze properties', () => { const barVar = root.variables[1] as ReactiveVariable; expect(barVar.isComputed).toBe(true); expect(genCode(barVar.value)).toBe('foo'); - expect(barVar.bitmap).toEqual(0b11); + expect(barVar.depMask).toEqual(0b11); }); it('should analyze dependency from state in different shape', () => { @@ -68,7 +68,7 @@ describe('analyze properties', () => { foo: foo ? a : b }" `); - expect(barVar.bitmap).toEqual(0b1111); + expect(barVar.depMask).toEqual(0b1111); }); // TODO:MOVE TO PROPS PLUGIN TEST @@ -108,7 +108,7 @@ describe('analyze properties', () => { expect(root.variables.length).toBe(1); const barVar = root.variables[0] as ReactiveVariable; expect(barVar.isComputed).toBe(false); - expect(barVar.bitmap).toEqual(0b1); + expect(barVar.depMask).toEqual(0b1); }); }); @@ -123,8 +123,8 @@ describe('analyze properties', () => { }) `); expect(root.variables.length).toBe(2); - expect(root.availableVariables[0].bitmap).toEqual(0b1); - expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].bitmap).toBe(0b11); + expect(root.availableVariables[0].depMask).toEqual(0b1); + expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].depMask).toBe(0b11); }); it('should analyze dependency in parent', () => { @@ -144,12 +144,12 @@ describe('analyze properties', () => { `); const sonNode = root.variables[3] as SubCompVariable; // Son > middleName - expect(sonNode.ownAvailableVariables[0].bitmap).toBe(0b1111); + expect(sonNode.ownAvailableVariables[0].depMask).toBe(0b1111); // Son > name - expect(sonNode.ownAvailableVariables[1].bitmap).toBe(0b11111); + expect(sonNode.ownAvailableVariables[1].depMask).toBe(0b11111); const grandSonNode = sonNode.variables[2] as SubCompVariable; // GrandSon > grandSonName - expect(grandSonNode.ownAvailableVariables[0].bitmap).toBe(0b100001); + expect(grandSonNode.ownAvailableVariables[0].depMask).toBe(0b100001); }); }); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts index a8c9a001..65b3f681 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/viewAnalyze.test.ts @@ -1,5 +1,4 @@ import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze'; -import { propsAnalyze } from '../../src/analyzer/propsAnalyze'; import { ComponentNode } from '../../src/analyzer/types'; import { viewAnalyze } from '../../src/analyzer/viewAnalyze'; import { genCode, mockAnalyze } from '../mock'; @@ -23,51 +22,27 @@ describe('viewAnalyze', () => { }); `); const div = root.children![0] as any; - expect(div.children[0].content.dependencyIndexArr).toMatchInlineSnapshot(` - [ - 4, - 3, - 2, - 0, - ] - `); + expect(div.children[0].content.depMask).toEqual(0b11101); expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount2]"'); - expect(div.props.className.dependencyIndexArr).toMatchInlineSnapshot(` - [ - 1, - 2, - 0, - ] - `); + expect(div.props.className.depMask).toEqual(0b111); expect(genCode(div.props.className.value)).toMatchInlineSnapshot('"className + count"'); // @ts-expect-error ignore ts here - const InputCompNode = (root.variables[3] as ComponentNode).value; + const InputCompNode = root.variables[5] as ComponentNode; expect(InputCompNode.usedPropertySet).toMatchInlineSnapshot(` Set { "count", "doubleCount", - "name", } `); // it's the {count} - const inputFirstExp = InputCompNode.children[0].children[0]; - expect(inputFirstExp.content.dependencyIndexArr).toMatchInlineSnapshot(` - [ - 5, - ] - `); + const inputFirstExp = InputCompNode.children![0].children[0]; + expect(inputFirstExp.content.depMask).toEqual(0b100000); expect(genCode(inputFirstExp.content.dependenciesNode)).toMatchInlineSnapshot('"[count]"'); // it's the {doubleCount} const inputSecondExp = InputCompNode.children[0].children[1]; - expect(inputSecondExp.content.dependencyIndexArr).toMatchInlineSnapshot(` - [ - 3, - 2, - 0, - ] - `); + expect(inputSecondExp.content.depMask).toEqual(0b1101); expect(genCode(inputSecondExp.content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount]"'); }); }); diff --git a/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts b/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts index 2140aa03..849cbb0e 100644 --- a/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts +++ b/packages/transpiler/babel-inula-next-core/test/analyze/watchAnalyze.test.ts @@ -22,9 +22,9 @@ describe('watchAnalyze', () => { // watch expression }" `); - if (!root.watch[0].deps) { + if (!root.watch[0].depMask) { throw new Error('watch deps not found'); } - expect(genCode(root.watch[0].deps.node)).toMatchInlineSnapshot('"[a, b]"'); + expect(genCode(root.watch[0].depMask)).toMatchInlineSnapshot('"[a, b]"'); }); }); diff --git a/packages/transpiler/reactivity-parser/src/parser.ts b/packages/transpiler/reactivity-parser/src/parser.ts index 5f534619..444ff663 100644 --- a/packages/transpiler/reactivity-parser/src/parser.ts +++ b/packages/transpiler/reactivity-parser/src/parser.ts @@ -240,7 +240,7 @@ export class ReactivityParser { key: 'value', path: [...path, idx], value: child.content, - depsBit: [], + depMask: 0, dependenciesNode: this.t.arrayExpression([]), dynamic: false, }); @@ -280,7 +280,7 @@ export class ReactivityParser { * @returns ExpParticle | HTMLParticle */ private parseHTML(htmlUnit: HTMLUnit): ExpParticle | HTMLParticle { - const { depsBit, dependenciesNode } = this.getDependencies(htmlUnit.tag); + const { depMask, dependenciesNode, dynamic } = this.getDependencies(htmlUnit.tag); const innerHTMLParticle: HTMLParticle = { type: 'html', @@ -295,6 +295,9 @@ export class ReactivityParser { innerHTMLParticle.children = htmlUnit.children.map(this.parseViewParticle.bind(this)); + // ---- Not a dynamic tag + if (!dynamic) return innerHTMLParticle; + // ---- Dynamic tag, wrap it in an ExpParticle to make the tag reactive const id = this.uid(); return { @@ -304,7 +307,7 @@ export class ReactivityParser { viewPropMap: { [id]: [innerHTMLParticle], }, - depsBit, + depMask: depMask, dependenciesNode, }, props: {}, @@ -320,7 +323,7 @@ export class ReactivityParser { * @returns CompParticle | ExpParticle */ private parseComp(compUnit: CompUnit): CompParticle | ExpParticle { - const { depsBit, dependenciesNode } = this.getDependencies(compUnit.tag); + const { depMask, dependenciesNode, dynamic } = this.getDependencies(compUnit.tag); const compParticle: CompParticle = { type: 'comp', @@ -344,7 +347,7 @@ export class ReactivityParser { viewPropMap: { [id]: [compParticle], }, - depsBit, + depMask: depMask, dependenciesNode, dynamic, }, @@ -360,7 +363,7 @@ export class ReactivityParser { * @returns ForParticle */ private parseFor(forUnit: ForUnit): ForParticle { - const { depsBit, dependenciesNode } = this.getDependencies(forUnit.array); + const { depMask, dependenciesNode } = this.getDependencies(forUnit.array); const prevIdentifierDepMap = this.config.identifierDepMap; // ---- Find all the identifiers in the key and remove them from the identifierDepMap // because once the key is changed, that identifier related dependencies will be changed too, @@ -371,7 +374,7 @@ export class ReactivityParser { this.config.identifierDepMap = Object.fromEntries( this.getIdentifiers(this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([]))) .filter(id => !keyDep || id !== keyDep) - .map(id => [id, depsBit.map(n => this.availableProperties[n])]) + .map(id => [id, depMask.map(n => this.availableProperties[n])]) ); const forParticle: ForParticle = { @@ -380,7 +383,7 @@ export class ReactivityParser { array: { value: forUnit.array, dynamic, - depsBit, + depMask: depMask, dependenciesNode, }, children: forUnit.children.map(this.parseViewParticle.bind(this)), @@ -470,12 +473,14 @@ export class ReactivityParser { * @returns dependency index array */ private getDependencies(node: t.Expression | t.Statement): { - depsBit: number; + dynamic: boolean; + depMask: number; dependenciesNode: t.ArrayExpression; } { if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) { return { - depsBit: 0, + dynamic: false, + depMask: 0, dependenciesNode: this.t.arrayExpression([]), }; } @@ -487,7 +492,8 @@ export class ReactivityParser { const depNodes = [...propertyDepNodes] as t.Expression[]; return { - depsBit: deps, + dynamic: depNodes.length > 0 || !!deps, + depMask: deps, dependenciesNode: this.t.arrayExpression(depNodes), }; } @@ -559,10 +565,11 @@ export class ReactivityParser { */ private getPropertyDependencies(node: t.Expression | t.Statement): [number, t.Node[]] { // ---- Deps: console.log(count) - let depsBit = 0; + let depMask = 0; // ---- Assign deps: count = 1 or count++ let assignDepBit = 0; const depNodes: Record = {}; + const deps = new Set(); const wrappedNode = this.valueWrapper(node); this.traverse(wrappedNode, { @@ -574,7 +581,9 @@ export class ReactivityParser { if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) { assignDepBit |= reactiveBitmap; } else { - depsBit |= reactiveBitmap; + depMask |= reactiveBitmap; + deps.add(propertyKey); + if (!depNodes[propertyKey]) depNodes[propertyKey] = []; depNodes[propertyKey].push(this.t.cloneNode(innerPath.node)); } @@ -586,7 +595,7 @@ export class ReactivityParser { // e.g. { console.log(count); count = 1 } // this will cause infinite loop // so we eliminate "count" from deps - if (assignDepBit & depsBit) { + if (assignDepBit & depMask) { // TODO: I think we should throw an error here to indicate the user that there is a loop } @@ -597,37 +606,8 @@ export class ReactivityParser { return idx === i; }); - // deps.forEach(this.usedProperties.add.bind(this.usedProperties)); - return [depsBit, dependencyNodes]; - } - - private calDependencyIndexArr = (directDepKey: string) => { - // iterate the availableProperties reversely to find the index of the property - // cause the availableProperties is in the order of the code - const chainedDepKeys = this.findDependency(directDepKey); - const depKeyQueue = chainedDepKeys ? [directDepKey, ...chainedDepKeys] : [directDepKey]; - depKeyQueue.forEach(this.usedProperties.add.bind(this.usedProperties)); - - let dep = depKeyQueue.shift(); - const result: number[] = []; - for (let i = this.availableProperties.length - 1; i >= 0; i--) { - if (this.availableProperties[i] === dep) { - result.push(i); - dep = depKeyQueue.shift(); - } - } - return result; - }; - - private findDependency(propertyKey: string) { - let currentMap: ReactiveBitMap | undefined = this.reactiveBitMap; - do { - if (currentMap[propertyKey] !== undefined) { - return currentMap[propertyKey]; - } - currentMap = currentMap[PrevMap]; - } while (currentMap); - return null; + deps.forEach(this.usedProperties.add.bind(this.usedProperties)); + return [depMask, dependencyNodes]; } /** diff --git a/packages/transpiler/reactivity-parser/src/types.ts b/packages/transpiler/reactivity-parser/src/types.ts index 202654ce..09a291c7 100644 --- a/packages/transpiler/reactivity-parser/src/types.ts +++ b/packages/transpiler/reactivity-parser/src/types.ts @@ -4,14 +4,14 @@ import type Babel from '@babel/core'; export interface DependencyValue { value: T; dynamic: boolean; // to removed - depsBit: number; // -> bit + depMask: number; // -> bit dependenciesNode: t.ArrayExpression; } export interface DependencyProp { value: t.Expression; viewPropMap: Record; - depsBit: number; + depMask: number; dependenciesNode: t.ArrayExpression; } @@ -21,7 +21,7 @@ export interface TemplateProp { path: number[]; value: t.Expression; dynamic: boolean; - depsBit: number; + depMask: number; dependenciesNode: t.ArrayExpression; }