refactor(parse): use bitmap instead of dependency map

This commit is contained in:
Hoikan 2024-05-08 15:56:42 +08:00
parent 0dcad572f3
commit 601381032d
10 changed files with 63 additions and 109 deletions

View File

@ -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;
}

View File

@ -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<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>,
deps: NodePath<t.ArrayExpression> | 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<string>) {

View File

@ -26,14 +26,13 @@ import { reactivityFuncNames } from '../../const';
* @returns
*/
export function getDependenciesFromNode(
propertyKey: string,
path: NodePath<t.Expression | t.ClassDeclaration>,
{ 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<string, t.Expression[]> = {};
const visitor = (innerPath: NodePath<t.Identifier>) => {
@ -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;
}
/**

View File

@ -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<t.Expression | null> {
* 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<Type = 'comp'> {
* The watch fn in the component
*/
watch?: {
bit: Bitmap;
deps: NodePath<t.ArrayExpression> | null;
depMask: Bitmap;
callback: NodePath<t.ArrowFunctionExpression> | NodePath<t.FunctionExpression>;
}[];
}

View File

@ -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);
}

View File

@ -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);
});
});

View File

@ -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]"');
});
});

View File

@ -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]"');
});
});

View File

@ -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<string, t.Node[]> = {};
const deps = new Set<string>();
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];
}
/**

View File

@ -4,14 +4,14 @@ import type Babel from '@babel/core';
export interface DependencyValue<T> {
value: T;
dynamic: boolean; // to removed
depsBit: number; // -> bit
depMask: number; // -> bit
dependenciesNode: t.ArrayExpression;
}
export interface DependencyProp {
value: t.Expression;
viewPropMap: Record<string, ViewParticle[]>;
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;
}