refactor(parse): use bitmap instead of dependency map

This commit is contained in:
Hoikan 2024-04-29 17:58:19 +08:00
parent f32da0e9c7
commit 0dcad572f3
10 changed files with 121 additions and 215 deletions

View File

@ -16,7 +16,7 @@
import { NodePath, type types as t } from '@babel/core'; import { NodePath, type types as t } from '@babel/core';
import { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable } from './types'; import { ComponentNode, FunctionalExpression, LifeCycle, ReactiveVariable } from './types';
import { PropType } from '../constants'; import { PropType } from '../constants';
import { ViewParticle, PrevMap } from '@openinula/reactivity-parser'; import { ViewParticle } from '@openinula/reactivity-parser';
export function createComponentNode( export function createComponentNode(
name: string, name: string,
@ -29,7 +29,7 @@ export function createComponentNode(
name, name,
children: undefined, children: undefined,
variables: [], variables: [],
dependencyMap: parent ? { [PrevMap]: parent.dependencyMap } : {}, _reactiveBitMap: parent ? new Map<string, number>(parent._reactiveBitMap) : new Map<string, number>(),
lifecycle: {}, lifecycle: {},
parent, parent,
fnNode, fnNode,
@ -46,11 +46,14 @@ export function createComponentNode(
return comp; return comp;
} }
export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, deps: string[] | null) { export function addProperty(comp: ComponentNode, name: string, value: t.Expression | null, depBits: number) {
comp.variables.push({ name, value, isComputed: !!deps?.length, type: 'reactive', deps }); // The index of the variable in the availableVariables
if (comp.dependencyMap[name] === undefined) { const idx = comp.availableVariables.length;
comp.dependencyMap[name] = null; const bit = 1 << idx;
} const bitmap = depBits ? depBits | bit : bit;
comp._reactiveBitMap.set(name, bitmap);
comp.variables.push({ name, value, isComputed: !!depBits, type: 'reactive', bitmap, level: comp.level });
} }
export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) { export function addMethod(comp: ComponentNode, name: string, value: FunctionalExpression) {
@ -58,7 +61,7 @@ export function addMethod(comp: ComponentNode, name: string, value: FunctionalEx
} }
export function addSubComponent(comp: ComponentNode, subComp: ComponentNode) { export function addSubComponent(comp: ComponentNode, subComp: ComponentNode) {
comp.variables.push({ name: subComp.name, value: subComp, type: 'subComp' }); comp.variables.push({ ...subComp, type: 'subComp' });
} }
export function addProp( export function addProp(

View File

@ -14,10 +14,9 @@
*/ */
import type { NodePath } from '@babel/core'; import type { NodePath } from '@babel/core';
import { AnalyzeContext, DependencyMap } from '../types'; import { AnalyzeContext } from '../types';
import { types as t } from '@openinula/babel-api'; import { types as t } from '@openinula/babel-api';
import { reactivityFuncNames } from '../../const'; import { reactivityFuncNames } from '../../const';
import { PrevMap } from '@openinula/reactivity-parser';
/** /**
* @brief Get all valid dependencies of a babel path * @brief Get all valid dependencies of a babel path
@ -32,21 +31,24 @@ export function getDependenciesFromNode(
{ current }: AnalyzeContext { current }: AnalyzeContext
) { ) {
// ---- Deps: console.log(count) // ---- Deps: console.log(count)
const deps = new Set<string>(); let depsBit = 0;
// ---- Assign deps: count = 1 or count++ // ---- Assign deps: count = 1 or count++
const assignDeps = new Set<string>(); let assignDepBit = 0;
const depNodes: Record<string, t.Expression[]> = {}; const depNodes: Record<string, t.Expression[]> = {};
const visitor = (innerPath: NodePath<t.Identifier>) => { const visitor = (innerPath: NodePath<t.Identifier>) => {
const propertyKey = innerPath.node.name; const propertyKey = innerPath.node.name;
const reactiveBitmap = current._reactiveBitMap.get(propertyKey);
if (reactiveBitmap !== undefined) {
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) { if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath)) {
assignDeps.add(propertyKey); assignDepBit |= reactiveBitmap;
} else if (current.availableVariables.includes(propertyKey)) { } else {
deps.add(propertyKey); depsBit |= reactiveBitmap;
findDependency(current.dependencyMap, propertyKey)?.forEach(deps.add.bind(deps));
if (!depNodes[propertyKey]) depNodes[propertyKey] = []; if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
depNodes[propertyKey].push(t.cloneNode(innerPath.node)); depNodes[propertyKey].push(t.cloneNode(innerPath.node));
} }
}
}; };
if (path.isIdentifier()) { if (path.isIdentifier()) {
visitor(path); visitor(path);
@ -59,16 +61,11 @@ export function getDependenciesFromNode(
// e.g. { console.log(count); count = 1 } // e.g. { console.log(count); count = 1 }
// this will cause infinite loop // this will cause infinite loop
// so we eliminate "count" from deps // so we eliminate "count" from deps
assignDeps.forEach(dep => { if (assignDepBit & depsBit) {
deps.delete(dep); // TODO: I think we should throw an error here to indicate the user that there is a loop
});
const depArr = [...deps];
if (deps.size > 0) {
current.dependencyMap[propertyKey] = depArr;
} }
return depArr; return depsBit;
} }
/** /**
@ -110,15 +107,3 @@ function isAssignmentFunction(innerPath: NodePath): boolean {
reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name) reactivityFuncNames.includes((parentPath.get('callee').node as t.Identifier).name)
); );
} }
function findDependency(dependencyMap: DependencyMap, propertyKey: string) {
let currentMap: DependencyMap | undefined = dependencyMap;
do {
if (currentMap[propertyKey] !== undefined) {
return currentMap[propertyKey];
}
// trace back to the previous map
currentMap = currentMap[PrevMap];
} while (currentMap);
return null;
}

View File

@ -30,7 +30,14 @@ interface BaseVariable<V> {
export interface ReactiveVariable extends BaseVariable<t.Expression | null> { export interface ReactiveVariable extends BaseVariable<t.Expression | null> {
type: 'reactive'; type: 'reactive';
level: number; level: number;
bitmap?: Bitmap; /**
* indicate the dependency of the variable | the index of the reactive variable
* i.e.
* let name = 'John'; // name's bitmap is 0x0001
* let age = 18; // age's bitmap is 0x0010
* let greeting = `Hello, ${name}`; // greeting's bitmap is 0x0101
*/
bitmap: Bitmap;
// need a flag for computed to gen a getter // need a flag for computed to gen a getter
// watch is a static computed // watch is a static computed
isComputed: boolean; isComputed: boolean;
@ -40,9 +47,7 @@ export interface MethodVariable extends BaseVariable<FunctionalExpression> {
type: 'method'; type: 'method';
} }
export interface SubCompVariable extends BaseVariable<ComponentNode> { export type SubCompVariable = ComponentNode<'subComp'>;
type: 'subComp';
}
export type Variable = ReactiveVariable | MethodVariable | SubCompVariable; export type Variable = ReactiveVariable | MethodVariable | SubCompVariable;
@ -55,8 +60,8 @@ export interface Prop {
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null; nestedRelationship: t.ObjectPattern | t.ArrayPattern | null;
} }
export interface ComponentNode { export interface ComponentNode<Type = 'comp'> {
type: 'comp'; type: Type;
name: string; name: string;
level: number; level: number;
// The variables defined in the component // The variables defined in the component
@ -66,9 +71,9 @@ export interface ComponentNode {
*/ */
usedPropertySet?: Set<string>; usedPropertySet?: Set<string>;
/** /**
* The available props for the component, including the nested props * The map to find the reactive bitmap by name
*/ */
availableProps: string[]; _reactiveBitMap: Map<string, Bitmap>;
/** /**
* The available variables and props owned by the component * The available variables and props owned by the component
*/ */

View File

@ -43,7 +43,7 @@ export function variablesAnalyze(): Visitor {
} else if (id.isIdentifier()) { } else if (id.isIdentifier()) {
// --- properties: the state / computed / plain properties / methods --- // --- properties: the state / computed / plain properties / methods ---
const init = declaration.get('init'); const init = declaration.get('init');
let deps: string[] | null = null; let depBits = 0;
if (isValidPath(init)) { if (isValidPath(init)) {
// handle the method // handle the method
if (init.isArrowFunctionExpression() || init.isFunctionExpression()) { if (init.isArrowFunctionExpression() || init.isFunctionExpression()) {
@ -68,9 +68,9 @@ export function variablesAnalyze(): Visitor {
return; return;
} }
deps = getDependenciesFromNode(id.node.name, init, ctx); depBits = getDependenciesFromNode(id.node.name, init, ctx);
} }
addProperty(ctx.current, id.node.name, init.node || null, deps); addProperty(ctx.current, id.node.name, init.node || null, depBits);
} }
}); });
}, },

View File

@ -34,10 +34,11 @@ export function viewAnalyze(): Visitor {
htmlTags, htmlTags,
parseTemplate: false, parseTemplate: false,
}); });
// @ts-expect-error TODO: FIX TYPE
const [viewParticles, usedPropertySet] = parseReactivity(viewUnits, { const [viewParticles, usedPropertySet] = parseReactivity(viewUnits, {
babelApi: getBabelApi(), babelApi: getBabelApi(),
availableProperties: current.availableVariables, availableProperties: current.availableVariables,
dependencyMap: current.dependencyMap, reactiveBitMap: current._reactiveBitMap,
reactivityFuncNames, reactivityFuncNames,
}); });

View File

@ -16,10 +16,9 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { genCode, mockAnalyze } from '../mock'; import { genCode, mockAnalyze } from '../mock';
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze'; import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
import { propsAnalyze } from '../../src/analyzer/propsAnalyze'; import { ReactiveVariable, SubCompVariable } from '../../src/analyzer/types';
import { ComponentNode, ReactiveVariable } from '../../src/analyzer/types';
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, variablesAnalyze]); const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze]);
describe('analyze properties', () => { describe('analyze properties', () => {
it('should work', () => { it('should work', () => {
@ -48,7 +47,7 @@ describe('analyze properties', () => {
const barVar = root.variables[1] as ReactiveVariable; const barVar = root.variables[1] as ReactiveVariable;
expect(barVar.isComputed).toBe(true); expect(barVar.isComputed).toBe(true);
expect(genCode(barVar.value)).toBe('foo'); expect(genCode(barVar.value)).toBe('foo');
expect(root.dependencyMap).toEqual({ bar: ['foo'], foo: null }); expect(barVar.bitmap).toEqual(0b11);
}); });
it('should analyze dependency from state in different shape', () => { it('should analyze dependency from state in different shape', () => {
@ -69,10 +68,11 @@ describe('analyze properties', () => {
foo: foo ? a : b foo: foo ? a : b
}" }"
`); `);
expect(root.dependencyMap).toEqual({ bar: ['foo', 'a', 'b'], foo: null, a: null, b: null }); expect(barVar.bitmap).toEqual(0b1111);
}); });
it('should analyze dependency from props', () => { // TODO:MOVE TO PROPS PLUGIN TEST
it.skip('should analyze dependency from props', () => {
const root = analyze(` const root = analyze(`
Component(({ foo }) => { Component(({ foo }) => {
let bar = foo; let bar = foo;
@ -82,10 +82,10 @@ describe('analyze properties', () => {
const barVar = root.variables[0] as ReactiveVariable; const barVar = root.variables[0] as ReactiveVariable;
expect(barVar.isComputed).toBe(true); expect(barVar.isComputed).toBe(true);
expect(root.dependencyMap).toEqual({ bar: ['foo'] });
}); });
it('should analyze dependency from nested props', () => { // TODO:MOVE TO PROPS PLUGIN TEST
it.skip('should analyze dependency from nested props', () => {
const root = analyze(` const root = analyze(`
Component(({ foo: foo1, name: [first, last] }) => { Component(({ foo: foo1, name: [first, last] }) => {
let bar = [foo1, first, last]; let bar = [foo1, first, last];
@ -94,6 +94,7 @@ describe('analyze properties', () => {
expect(root.variables.length).toBe(1); expect(root.variables.length).toBe(1);
const barVar = root.variables[0] as ReactiveVariable; const barVar = root.variables[0] as ReactiveVariable;
expect(barVar.isComputed).toBe(true); expect(barVar.isComputed).toBe(true);
// @ts-expect-error ignore ts here
expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] }); expect(root.dependencyMap).toEqual({ bar: ['foo1', 'first', 'last'] });
}); });
@ -107,7 +108,7 @@ describe('analyze properties', () => {
expect(root.variables.length).toBe(1); expect(root.variables.length).toBe(1);
const barVar = root.variables[0] as ReactiveVariable; const barVar = root.variables[0] as ReactiveVariable;
expect(barVar.isComputed).toBe(false); expect(barVar.isComputed).toBe(false);
expect(root.dependencyMap).toEqual({ bar: null }); expect(barVar.bitmap).toEqual(0b1);
}); });
}); });
@ -122,22 +123,14 @@ describe('analyze properties', () => {
}) })
`); `);
expect(root.variables.length).toBe(2); expect(root.variables.length).toBe(2);
expect(root.dependencyMap).toEqual({ foo: null }); expect(root.availableVariables[0].bitmap).toEqual(0b1);
expect((root.variables[1].value as ComponentNode).dependencyMap).toMatchInlineSnapshot(` expect((root.variables[1] as SubCompVariable).ownAvailableVariables[0].bitmap).toBe(0b11);
{
"bar": [
"foo",
],
Symbol(prevMap): {
"foo": null,
},
}
`);
}); });
it('should analyze dependency in parent', () => { it('should analyze dependency in parent', () => {
const root = analyze(` const root = analyze(`
Component(({lastName}) => { Component(() => {
let lastName;
let parentFirstName = 'sheldon'; let parentFirstName = 'sheldon';
const parentName = parentFirstName + lastName; const parentName = parentFirstName + lastName;
const Son = Component(() => { const Son = Component(() => {
@ -149,63 +142,15 @@ describe('analyze properties', () => {
}); });
}) })
`); `);
const sonNode = root.variables[2].value as ComponentNode; const sonNode = root.variables[3] as SubCompVariable;
expect(sonNode.dependencyMap).toMatchInlineSnapshot(` // Son > middleName
{ expect(sonNode.ownAvailableVariables[0].bitmap).toBe(0b1111);
"middleName": [ // Son > name
"parentName", expect(sonNode.ownAvailableVariables[1].bitmap).toBe(0b11111);
"parentFirstName", const grandSonNode = sonNode.variables[2] as SubCompVariable;
"lastName", // GrandSon > grandSonName
], expect(grandSonNode.ownAvailableVariables[0].bitmap).toBe(0b100001);
"name": [
"middleName",
"parentName",
"parentFirstName",
"lastName",
],
Symbol(prevMap): {
"parentFirstName": null,
"parentName": [
"parentFirstName",
"lastName",
],
},
}
`);
const grandSonNode = sonNode.variables[2].value as ComponentNode;
expect(grandSonNode.dependencyMap).toMatchInlineSnapshot(`
{
"grandSonName": [
"lastName",
],
Symbol(prevMap): {
"middleName": [
"parentName",
"parentFirstName",
"lastName",
],
"name": [
"middleName",
"parentName",
"parentFirstName",
"lastName",
],
Symbol(prevMap): {
"parentFirstName": null,
"parentName": [
"parentFirstName",
"lastName",
],
},
},
}
`);
}); });
// SubscriptionTree
// const SubscriptionTree = {
// lastName: ['parentName','son:middleName','son:name','son,grandSon:grandSonName'],
//
// }
}); });
it('should collect method', () => { it('should collect method', () => {
@ -222,10 +167,5 @@ describe('analyze properties', () => {
expect(root.variables.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']); expect(root.variables.map(p => p.name)).toEqual(['foo', 'onClick', 'onHover', 'onInput']);
expect(root.variables[1].type).toBe('method'); expect(root.variables[1].type).toBe('method');
expect(root.variables[2].type).toBe('method'); expect(root.variables[2].type).toBe('method');
expect(root.dependencyMap).toMatchInlineSnapshot(`
{
"foo": null,
}
`);
}); });
}); });

View File

@ -5,27 +5,18 @@ import { viewAnalyze } from '../../src/analyzer/viewAnalyze';
import { genCode, mockAnalyze } from '../mock'; import { genCode, mockAnalyze } from '../mock';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
const analyze = (code: string) => mockAnalyze(code, [propsAnalyze, variablesAnalyze, viewAnalyze]); const analyze = (code: string) => mockAnalyze(code, [variablesAnalyze, viewAnalyze]);
describe('viewAnalyze', () => { describe('viewAnalyze', () => {
it('should analyze view', () => { it('should analyze view', () => {
const root = analyze(/*js*/ ` const root = analyze(/*js*/ `
Component(({name ,className}) => { Component(({}) => {
let name;
let className;
let count = name; // 1 let count = name; // 1
let doubleCount = count* 2; // 2 let doubleCount = count* 2; // 2
let doubleCount2 = doubleCount* 2; // 4 let doubleCount2 = doubleCount* 2; // 4
const Input = Component(() => { const Input = Component(() => {
let count = 1; let count = 1;
watch(() => {
if (doubleCount2 > 10) {
count++;
}
console.log(doubleCount2);
});
const update = changed => {
if (changed & 0x1011) {
node1.update(_$this0.count, _$this0.doubleCount);
}
};
return <input>{count}{doubleCount}</input>; return <input>{count}{doubleCount}</input>;
}); });
return <div className={className + count}>{doubleCount2}</div>; return <div className={className + count}>{doubleCount2}</div>;

View File

@ -12,7 +12,7 @@ import {
type ForParticle, type ForParticle,
type IfParticle, type IfParticle,
type EnvParticle, type EnvParticle,
DependencyMap, ReactiveBitMap,
} from './types'; } from './types';
import { type NodePath, type types as t, type traverse } from '@babel/core'; import { type NodePath, type types as t, type traverse } from '@babel/core';
import { import {
@ -36,7 +36,7 @@ export class ReactivityParser {
private readonly traverse: typeof traverse; private readonly traverse: typeof traverse;
private readonly availableProperties: string[]; private readonly availableProperties: string[];
private readonly availableIdentifiers?: string[]; private readonly availableIdentifiers?: string[];
private readonly dependencyMap: DependencyMap; private readonly reactiveBitMap: ReactiveBitMap;
private readonly identifierDepMap: Record<string, string[]>; private readonly identifierDepMap: Record<string, string[]>;
private readonly dependencyParseType; private readonly dependencyParseType;
private readonly reactivityFuncNames; private readonly reactivityFuncNames;
@ -70,7 +70,7 @@ export class ReactivityParser {
this.traverse = config.babelApi.traverse; this.traverse = config.babelApi.traverse;
this.availableProperties = config.availableProperties; this.availableProperties = config.availableProperties;
this.availableIdentifiers = config.availableIdentifiers; this.availableIdentifiers = config.availableIdentifiers;
this.dependencyMap = config.dependencyMap; this.reactiveBitMap = config.reactiveBitMap;
this.identifierDepMap = config.identifierDepMap ?? {}; this.identifierDepMap = config.identifierDepMap ?? {};
this.dependencyParseType = config.dependencyParseType ?? 'property'; this.dependencyParseType = config.dependencyParseType ?? 'property';
this.reactivityFuncNames = config.reactivityFuncNames ?? []; this.reactivityFuncNames = config.reactivityFuncNames ?? [];
@ -123,8 +123,8 @@ export class ReactivityParser {
* @brief Generate a template * @brief Generate a template
* There'll be a situation where the tag is dynamic, e.g. tag(this.htmlTag), * There'll be a situation where the tag is dynamic, e.g. tag(this.htmlTag),
* which we can't generate a template string for it, so we'll wrap it in an ExpParticle in parseHTML() section * which we can't generate a template string for it, so we'll wrap it in an ExpParticle in parseHTML() section
* @param htmlUnit
* @returns template string * @returns template string
* @param unit
*/ */
private generateTemplate(unit: HTMLUnit): HTMLParticle { private generateTemplate(unit: HTMLUnit): HTMLParticle {
const staticProps = this.filterTemplateProps( const staticProps = this.filterTemplateProps(
@ -240,7 +240,7 @@ export class ReactivityParser {
key: 'value', key: 'value',
path: [...path, idx], path: [...path, idx],
value: child.content, value: child.content,
dependencyIndexArr: [], depsBit: [],
dependenciesNode: this.t.arrayExpression([]), dependenciesNode: this.t.arrayExpression([]),
dynamic: false, dynamic: false,
}); });
@ -280,7 +280,7 @@ export class ReactivityParser {
* @returns ExpParticle | HTMLParticle * @returns ExpParticle | HTMLParticle
*/ */
private parseHTML(htmlUnit: HTMLUnit): ExpParticle | HTMLParticle { private parseHTML(htmlUnit: HTMLUnit): ExpParticle | HTMLParticle {
const { dependencyIndexArr, dependenciesNode, dynamic } = this.getDependencies(htmlUnit.tag); const { depsBit, dependenciesNode } = this.getDependencies(htmlUnit.tag);
const innerHTMLParticle: HTMLParticle = { const innerHTMLParticle: HTMLParticle = {
type: 'html', type: 'html',
@ -295,9 +295,6 @@ export class ReactivityParser {
innerHTMLParticle.children = htmlUnit.children.map(this.parseViewParticle.bind(this)); 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 // ---- Dynamic tag, wrap it in an ExpParticle to make the tag reactive
const id = this.uid(); const id = this.uid();
return { return {
@ -307,9 +304,8 @@ export class ReactivityParser {
viewPropMap: { viewPropMap: {
[id]: [innerHTMLParticle], [id]: [innerHTMLParticle],
}, },
dependencyIndexArr, depsBit,
dependenciesNode, dependenciesNode,
dynamic,
}, },
props: {}, props: {},
}; };
@ -324,7 +320,7 @@ export class ReactivityParser {
* @returns CompParticle | ExpParticle * @returns CompParticle | ExpParticle
*/ */
private parseComp(compUnit: CompUnit): CompParticle | ExpParticle { private parseComp(compUnit: CompUnit): CompParticle | ExpParticle {
const { dependencyIndexArr, dependenciesNode, dynamic } = this.getDependencies(compUnit.tag); const { depsBit, dependenciesNode } = this.getDependencies(compUnit.tag);
const compParticle: CompParticle = { const compParticle: CompParticle = {
type: 'comp', type: 'comp',
@ -348,7 +344,7 @@ export class ReactivityParser {
viewPropMap: { viewPropMap: {
[id]: [compParticle], [id]: [compParticle],
}, },
dependencyIndexArr, depsBit,
dependenciesNode, dependenciesNode,
dynamic, dynamic,
}, },
@ -364,7 +360,7 @@ export class ReactivityParser {
* @returns ForParticle * @returns ForParticle
*/ */
private parseFor(forUnit: ForUnit): ForParticle { private parseFor(forUnit: ForUnit): ForParticle {
const { dependencyIndexArr, dependenciesNode, dynamic } = this.getDependencies(forUnit.array); const { depsBit, dependenciesNode } = this.getDependencies(forUnit.array);
const prevIdentifierDepMap = this.config.identifierDepMap; const prevIdentifierDepMap = this.config.identifierDepMap;
// ---- Find all the identifiers in the key and remove them from the 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, // because once the key is changed, that identifier related dependencies will be changed too,
@ -375,7 +371,7 @@ export class ReactivityParser {
this.config.identifierDepMap = Object.fromEntries( this.config.identifierDepMap = Object.fromEntries(
this.getIdentifiers(this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([]))) this.getIdentifiers(this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([])))
.filter(id => !keyDep || id !== keyDep) .filter(id => !keyDep || id !== keyDep)
.map(id => [id, dependencyIndexArr.map(n => this.availableProperties[n])]) .map(id => [id, depsBit.map(n => this.availableProperties[n])])
); );
const forParticle: ForParticle = { const forParticle: ForParticle = {
@ -384,7 +380,7 @@ export class ReactivityParser {
array: { array: {
value: forUnit.array, value: forUnit.array,
dynamic, dynamic,
dependencyIndexArr, depsBit,
dependenciesNode, dependenciesNode,
}, },
children: forUnit.children.map(this.parseViewParticle.bind(this)), children: forUnit.children.map(this.parseViewParticle.bind(this)),
@ -474,30 +470,24 @@ export class ReactivityParser {
* @returns dependency index array * @returns dependency index array
*/ */
private getDependencies(node: t.Expression | t.Statement): { private getDependencies(node: t.Expression | t.Statement): {
dynamic: boolean; depsBit: number;
dependencyIndexArr: number[];
dependenciesNode: t.ArrayExpression; dependenciesNode: t.ArrayExpression;
} { } {
if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) { if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) {
return { return {
dynamic: false, depsBit: 0,
dependencyIndexArr: [],
dependenciesNode: this.t.arrayExpression([]), dependenciesNode: this.t.arrayExpression([]),
}; };
} }
// ---- Both id and prop deps need to be calculated because // ---- Both id and prop deps need to be calculated because
// id is for snippet update, prop is normal update // id is for snippet update, prop is normal update
// in a snippet, the depsNode should be both id and prop // in a snippet, the depsNode should be both id and prop
const [directPropertyDeps, propertyDepNodes] = this.getPropertyDependencies(node); const [deps, propertyDepNodes] = this.getPropertyDependencies(node);
const directDependencies = directPropertyDeps;
const identifierMapDependencies = this.getIdentifierMapDependencies(node);
const deps = [...new Set([...directDependencies, ...identifierMapDependencies])];
const depNodes = [...propertyDepNodes] as t.Expression[]; const depNodes = [...propertyDepNodes] as t.Expression[];
return { return {
dynamic: depNodes.length > 0 || deps.length > 0, depsBit: deps,
dependencyIndexArr: deps,
dependenciesNode: this.t.arrayExpression(depNodes), dependenciesNode: this.t.arrayExpression(depNodes),
}; };
} }
@ -534,7 +524,7 @@ export class ReactivityParser {
!this.isMemberInManualFunction(innerPath) !this.isMemberInManualFunction(innerPath)
) { ) {
deps.add(idName); deps.add(idName);
this.dependencyMap[idName]?.forEach(deps.add.bind(deps)); this.reactiveBitMap[idName]?.forEach(deps.add.bind(deps));
if (!depNodes[idName]) depNodes[idName] = []; if (!depNodes[idName]) depNodes[idName] = [];
depNodes[idName].push(this.geneDependencyNode(innerPath)); depNodes[idName].push(this.geneDependencyNode(innerPath));
} }
@ -567,34 +557,39 @@ export class ReactivityParser {
* @param node * @param node
* @returns dependency index array * @returns dependency index array
*/ */
private getPropertyDependencies(node: t.Expression | t.Statement): [number[], t.Node[]] { private getPropertyDependencies(node: t.Expression | t.Statement): [number, t.Node[]] {
const deps = new Set<string>(); // ---- Deps: console.log(count)
const assignDeps = new Set<string>(); let depsBit = 0;
// ---- Assign deps: count = 1 or count++
let assignDepBit = 0;
const depNodes: Record<string, t.Node[]> = {}; const depNodes: Record<string, t.Node[]> = {};
const wrappedNode = this.valueWrapper(node); const wrappedNode = this.valueWrapper(node);
this.traverse(wrappedNode, { this.traverse(wrappedNode, {
Identifier: innerPath => { Identifier: innerPath => {
const propertyKey = innerPath.node.name; const propertyKey = innerPath.node.name;
const reactiveBitmap = this.reactiveBitMap.get(propertyKey);
if (reactiveBitmap !== undefined) {
if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) { if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) {
assignDeps.add(propertyKey); assignDepBit |= reactiveBitmap;
} else if ( } else {
this.availableProperties.includes(propertyKey) && depsBit |= reactiveBitmap;
!this.isMemberInEscapeFunction(innerPath) &&
!this.isMemberInManualFunction(innerPath)
) {
deps.add(propertyKey);
if (!depNodes[propertyKey]) depNodes[propertyKey] = []; if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
depNodes[propertyKey].push(this.geneDependencyNode(innerPath)); depNodes[propertyKey].push(this.t.cloneNode(innerPath.node));
}
} }
}, },
}); });
const dependencyIdxArr = deduplicate([...deps].map(this.calDependencyIndexArr).flat());
assignDeps.forEach(dep => { // ---- Eliminate deps that are assigned in the same method
deps.delete(dep); // e.g. { console.log(count); count = 1 }
delete depNodes[dep]; // this will cause infinite loop
}); // so we eliminate "count" from deps
if (assignDepBit & depsBit) {
// TODO: I think we should throw an error here to indicate the user that there is a loop
}
let dependencyNodes = Object.values(depNodes).flat(); let dependencyNodes = Object.values(depNodes).flat();
// ---- deduplicate the dependency nodes // ---- deduplicate the dependency nodes
dependencyNodes = dependencyNodes.filter((n, i) => { dependencyNodes = dependencyNodes.filter((n, i) => {
@ -602,8 +597,8 @@ export class ReactivityParser {
return idx === i; return idx === i;
}); });
deps.forEach(this.usedProperties.add.bind(this.usedProperties)); // deps.forEach(this.usedProperties.add.bind(this.usedProperties));
return [dependencyIdxArr, dependencyNodes]; return [depsBit, dependencyNodes];
} }
private calDependencyIndexArr = (directDepKey: string) => { private calDependencyIndexArr = (directDepKey: string) => {
@ -625,7 +620,7 @@ export class ReactivityParser {
}; };
private findDependency(propertyKey: string) { private findDependency(propertyKey: string) {
let currentMap: DependencyMap | undefined = this.dependencyMap; let currentMap: ReactiveBitMap | undefined = this.reactiveBitMap;
do { do {
if (currentMap[propertyKey] !== undefined) { if (currentMap[propertyKey] !== undefined) {
return currentMap[propertyKey]; return currentMap[propertyKey];

View File

@ -1,19 +1,17 @@
import { type types as t } from '@babel/core'; import { type types as t } from '@babel/core';
import type Babel from '@babel/core'; import type Babel from '@babel/core';
import { PrevMap } from '.';
export interface DependencyValue<T> { export interface DependencyValue<T> {
value: T; value: T;
dynamic: boolean; // to removed dynamic: boolean; // to removed
dependencyIndexArr: number[]; // -> bit depsBit: number; // -> bit
dependenciesNode: t.ArrayExpression; dependenciesNode: t.ArrayExpression;
} }
export interface DependencyProp { export interface DependencyProp {
value: t.Expression; value: t.Expression;
viewPropMap: Record<string, ViewParticle[]>; viewPropMap: Record<string, ViewParticle[]>;
dynamic: boolean; depsBit: number;
dependencyIndexArr: number[];
dependenciesNode: t.ArrayExpression; dependenciesNode: t.ArrayExpression;
} }
@ -23,7 +21,7 @@ export interface TemplateProp {
path: number[]; path: number[];
value: t.Expression; value: t.Expression;
dynamic: boolean; dynamic: boolean;
dependencyIndexArr: number[]; depsBit: number;
dependenciesNode: t.ArrayExpression; dependenciesNode: t.ArrayExpression;
} }
@ -128,25 +126,13 @@ export interface ReactivityParserConfig {
babelApi: typeof Babel; babelApi: typeof Babel;
availableProperties: string[]; availableProperties: string[];
availableIdentifiers?: string[]; availableIdentifiers?: string[];
dependencyMap: Record<string, string[] | null>; reactiveBitMap: ReactiveBitMap;
identifierDepMap?: Record<string, string[]>; identifierDepMap?: Record<string, string[]>;
dependencyParseType?: 'property' | 'identifier'; dependencyParseType?: 'property' | 'identifier';
parseTemplate?: boolean; parseTemplate?: boolean;
reactivityFuncNames?: string[]; reactivityFuncNames?: string[];
} }
export interface DependencyMap { // TODO: unify with the types in babel-inula-next-core
/** type Bitmap = number;
* key is the variable name, value is the dependencies export type ReactiveBitMap = Map<string, Bitmap>;
* i.e. {
* count: ['flag'],
* state1: ['count', 'flag'],
* state2: ['count', 'flag', 'state1'],
* state3: ['count', 'flag', 'state1', 'state2'],
* state4: ['count', 'flag', 'state1', 'state2', 'state3'],
* }
*/
[key: string]: string[] | null;
[PrevMap]?: DependencyMap;
}