fix(reactivity): fix for dependency

This commit is contained in:
Hoikan 2024-05-10 16:54:38 +08:00
parent 601381032d
commit 4ca2d66fac
11 changed files with 99 additions and 223 deletions

View File

@ -53,9 +53,9 @@ export function functionalMacroAnalyze(): Visitor {
const fnNode = extractFnFromMacro(expression, WATCH);
const deps = getWatchDeps(expression);
const depBits = getDependenciesFromNode(deps ?? fnNode, ctx);
const depMask = getDependenciesFromNode(deps ?? fnNode, ctx);
addWatch(ctx.current, fnNode, deps);
addWatch(ctx.current, fnNode, depMask);
return;
}
}

View File

@ -64,18 +64,6 @@ export function addSubComponent(comp: ComponentNode, subComp: ComponentNode) {
comp.variables.push({ ...subComp, type: 'subComp' });
}
export function addProp(
comp: ComponentNode,
type: PropType,
key: string,
defaultVal: t.Expression | null = null,
alias: string | null = null,
nestedProps: string[] | null = null,
nestedRelationship: t.ObjectPattern | t.ArrayPattern | null = null
) {
comp.props.push({ name: key, type, default: defaultVal, alias, nestedProps, nestedRelationship });
}
export function addLifecycle(comp: ComponentNode, lifeCycle: LifeCycle, block: t.BlockStatement) {
const compLifecycle = comp.lifecycle;
if (!compLifecycle[lifeCycle]) {

View File

@ -1,6 +1,5 @@
import { type NodePath } from '@babel/core';
import { AnalyzeContext, Visitor } from './types';
import { addProp } from './nodeFactory';
import { PropType } from '../constants';
import { types as t } from '@openinula/babel-api';

View File

@ -45,4 +45,19 @@ describe('viewAnalyze', () => {
expect(inputSecondExp.content.depMask).toEqual(0b1101);
expect(genCode(inputSecondExp.content.dependenciesNode)).toMatchInlineSnapshot('"[doubleCount]"');
});
it('should analyze object state', () => {
const root = analyze(/*js*/ `
Component(({}) => {
const info = {
firstName: 'John',
lastName: 'Doe'
}
return <h1>{info.firstName}</h1>;
});
`);
const div = root.children![0] as any;
expect(div.children[0].content.depMask).toEqual(0b1);
expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot(`"[info?.firstName]"`);
});
});

View File

@ -1,13 +1,41 @@
import { functionalMacroAnalyze } from '../../src/analyzer/functionalMacroAnalyze';
import { genCode, mockAnalyze } from '../mock';
import { describe, expect, it } from 'vitest';
import { variablesAnalyze } from '../../src/analyzer/variablesAnalyze';
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze]);
const analyze = (code: string) => mockAnalyze(code, [functionalMacroAnalyze, variablesAnalyze]);
describe('watchAnalyze', () => {
it('should analyze watch expressions', () => {
const root = analyze(/*js*/ `
Comp(() => {
let a = 0;
let b = 0;
watch(() => {
console.log(a, b);
});
})
`);
expect(root.watch).toHaveLength(1);
if (!root?.watch?.[0].callback) {
throw new Error('watch callback not found');
}
expect(genCode(root.watch[0].callback.node)).toMatchInlineSnapshot(`
"() => {
console.log(a, b);
}"
`);
if (!root.watch[0].depMask) {
throw new Error('watch deps not found');
}
expect(root.watch[0].depMask).toBe(0b11);
});
it('should analyze watch expressions with dependency array', () => {
const root = analyze(/*js*/ `
Comp(() => {
let a = 0;
let b = 0;
watch(() => {
// watch expression
}, [a, b]);
@ -25,6 +53,6 @@ describe('watchAnalyze', () => {
if (!root.watch[0].depMask) {
throw new Error('watch deps not found');
}
expect(genCode(root.watch[0].depMask)).toMatchInlineSnapshot('"[a, b]"');
expect(root.watch[0].depMask).toBe(0b11);
});
});

View File

@ -20,9 +20,5 @@ export function parseReactivity(viewUnits: ViewUnit[], config: ReactivityParserC
});
return [dlParticles, usedProperties];
}
/**
* The key to get the previous map in DependencyMap Chain
*/
export const PrevMap = Symbol('prevMap');
export type * from './types';

View File

@ -12,7 +12,7 @@ import {
type ForParticle,
type IfParticle,
type EnvParticle,
ReactiveBitMap,
DepMaskMap,
} from './types';
import { type NodePath, type types as t, type traverse } from '@babel/core';
import {
@ -27,7 +27,6 @@ import {
type ExpUnit,
} from '@openinula/jsx-view-parser';
import { DLError } from './error';
import { PrevMap } from '.';
export class ReactivityParser {
private readonly config: ReactivityParserConfig;
@ -35,10 +34,8 @@ export class ReactivityParser {
private readonly t: typeof t;
private readonly traverse: typeof traverse;
private readonly availableProperties: string[];
private readonly availableIdentifiers?: string[];
private readonly reactiveBitMap: ReactiveBitMap;
private readonly depMaskMap: DepMaskMap;
private readonly identifierDepMap: Record<string, string[]>;
private readonly dependencyParseType;
private readonly reactivityFuncNames;
private readonly escapeNamings = ['escape', '$'];
@ -69,10 +66,7 @@ export class ReactivityParser {
this.t = config.babelApi.types;
this.traverse = config.babelApi.traverse;
this.availableProperties = config.availableProperties;
this.availableIdentifiers = config.availableIdentifiers;
this.reactiveBitMap = config.reactiveBitMap;
this.identifierDepMap = config.identifierDepMap ?? {};
this.dependencyParseType = config.dependencyParseType ?? 'property';
this.depMaskMap = config.depMaskMap;
this.reactivityFuncNames = config.reactivityFuncNames ?? [];
}
@ -139,9 +133,7 @@ export class ReactivityParser {
key,
{
...prop,
dependencyIndexArr: [],
dependenciesNode: this.t.arrayExpression([]),
dynamic: false,
},
]);
@ -242,7 +234,6 @@ export class ReactivityParser {
value: child.content,
depMask: 0,
dependenciesNode: this.t.arrayExpression([]),
dynamic: false,
});
}
});
@ -280,8 +271,6 @@ export class ReactivityParser {
* @returns ExpParticle | HTMLParticle
*/
private parseHTML(htmlUnit: HTMLUnit): ExpParticle | HTMLParticle {
const { depMask, dependenciesNode, dynamic } = this.getDependencies(htmlUnit.tag);
const innerHTMLParticle: HTMLParticle = {
type: 'html',
tag: htmlUnit.tag,
@ -296,22 +285,7 @@ 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 {
type: 'exp',
content: {
value: this.t.stringLiteral(id),
viewPropMap: {
[id]: [innerHTMLParticle],
},
depMask: depMask,
dependenciesNode,
},
props: {},
};
return innerHTMLParticle;
}
// ---- @Comp ----
@ -322,9 +296,7 @@ export class ReactivityParser {
* @param compUnit
* @returns CompParticle | ExpParticle
*/
private parseComp(compUnit: CompUnit): CompParticle | ExpParticle {
const { depMask, dependenciesNode, dynamic } = this.getDependencies(compUnit.tag);
private parseComp(compUnit: CompUnit): CompParticle {
const compParticle: CompParticle = {
type: 'comp',
tag: compUnit.tag,
@ -337,22 +309,7 @@ export class ReactivityParser {
);
compParticle.children = compUnit.children.map(this.parseViewParticle.bind(this));
if (!dynamic) return compParticle;
const id = this.uid();
return {
type: 'exp',
content: {
value: this.t.stringLiteral(id),
viewPropMap: {
[id]: [compParticle],
},
depMask: depMask,
dependenciesNode,
dynamic,
},
props: {},
};
return compParticle;
}
// ---- @For ----
@ -364,25 +321,25 @@ export class ReactivityParser {
*/
private parseFor(forUnit: ForUnit): ForParticle {
const { depMask, dependenciesNode } = this.getDependencies(forUnit.array);
const prevIdentifierDepMap = this.config.identifierDepMap;
const prevIdentifierDepMap = this.config.depMaskMap;
// ---- 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,
// so no need to update them
const keyDep = this.t.isIdentifier(forUnit.key) && forUnit.key.name;
// ---- Generate an identifierDepMap to track identifiers in item and make them reactive
// based on the dependencies from the array
this.config.identifierDepMap = Object.fromEntries(
this.getIdentifiers(this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([])))
this.config.depMaskMap = new Map([
...this.config.depMaskMap,
...this.getIdentifiers(this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([])))
.filter(id => !keyDep || id !== keyDep)
.map(id => [id, depMask.map(n => this.availableProperties[n])])
);
.map(id => [id, depMask]),
]);
const forParticle: ForParticle = {
type: 'for',
item: forUnit.item,
array: {
value: forUnit.array,
dynamic,
depMask: depMask,
dependenciesNode,
},
@ -473,13 +430,11 @@ export class ReactivityParser {
* @returns dependency index array
*/
private getDependencies(node: t.Expression | t.Statement): {
dynamic: boolean;
depMask: number;
dependenciesNode: t.ArrayExpression;
} {
if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) {
return {
dynamic: false,
depMask: 0,
dependenciesNode: this.t.arrayExpression([]),
};
@ -492,66 +447,11 @@ export class ReactivityParser {
const depNodes = [...propertyDepNodes] as t.Expression[];
return {
dynamic: depNodes.length > 0 || !!deps,
depMask: deps,
dependenciesNode: this.t.arrayExpression(depNodes),
};
}
/**
* @brief Get all the dependencies of a node if a property is a valid dependency as
* 1. the identifier is in the availableProperties
* 2. the identifier is a stand alone identifier
* 3. the identifier is not in an escape function
* 4. the identifier is not in a manual function
* 5. the identifier is not the left side of an assignment expression, which is an assignment expression
* 6. the identifier is not the right side of an assignment expression, which is an update expression
* @param node
* @returns dependency index array
*/
private getIdentifierDependencies(node: t.Expression | t.Statement): [number[], t.Node[]] {
const availableIdentifiers = this.availableIdentifiers ?? this.availableProperties;
const deps = new Set<string>();
const assignDeps = new Set<string>();
const depNodes: Record<string, t.Node[]> = {};
const wrappedNode = this.valueWrapper(node);
this.traverse(wrappedNode, {
Identifier: innerPath => {
const identifier = innerPath.node;
const idName = identifier.name;
if (!availableIdentifiers.includes(idName)) return;
if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) {
assignDeps.add(idName);
} else if (
this.isStandAloneIdentifier(innerPath) &&
!this.isMemberInEscapeFunction(innerPath) &&
!this.isMemberInManualFunction(innerPath)
) {
deps.add(idName);
this.reactiveBitMap[idName]?.forEach(deps.add.bind(deps));
if (!depNodes[idName]) depNodes[idName] = [];
depNodes[idName].push(this.geneDependencyNode(innerPath));
}
},
});
assignDeps.forEach(dep => {
deps.delete(dep);
delete depNodes[dep];
});
let dependencyNodes = Object.values(depNodes).flat();
// ---- deduplicate the dependency nodes
dependencyNodes = dependencyNodes.filter((n, i) => {
const idx = dependencyNodes.findIndex(m => this.t.isNodesEquivalent(m, n));
return idx === i;
});
deps.forEach(this.usedProperties.add.bind(this.usedProperties));
return [[...deps].map(dep => this.availableProperties.lastIndexOf(dep)), dependencyNodes];
}
/**
* @brief Get all the dependencies of a node if a member expression is a valid dependency as
* 1. the property is in the availableProperties
@ -575,7 +475,7 @@ export class ReactivityParser {
this.traverse(wrappedNode, {
Identifier: innerPath => {
const propertyKey = innerPath.node.name;
const reactiveBitmap = this.reactiveBitMap.get(propertyKey);
const reactiveBitmap = this.depMaskMap.get(propertyKey);
if (reactiveBitmap !== undefined) {
if (this.isAssignmentExpressionLeft(innerPath) || this.isAssignmentFunction(innerPath)) {
@ -585,7 +485,7 @@ export class ReactivityParser {
deps.add(propertyKey);
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
depNodes[propertyKey].push(this.t.cloneNode(innerPath.node));
depNodes[propertyKey].push(this.geneDependencyNode(innerPath));
}
}
},

View File

@ -6,66 +6,63 @@ describe('Dependency', () => {
it('should parse the correct dependency', () => {
const viewParticles = parse('Comp(flag)');
const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toContain(0);
expect(content?.depMask).toEqual(0b1);
});
it('should parse the correct dependency when interfacing the dependency chain', () => {
const viewParticles = parse('Comp(doubleCount)');
const content = (viewParticles[0] as CompParticle).props._$content;
const dependency = content?.dependencyIndexArr;
const dependency = content?.depMask;
// ---- doubleCount depends on count, count depends on flag
// so doubleCount depends on flag, count and doubleCount
expect(dependency).toContain(availableProperties.indexOf('flag'));
expect(dependency).toContain(availableProperties.indexOf('count'));
expect(dependency).toContain(availableProperties.indexOf('doubleCount'));
expect(dependency).toEqual(0b111);
});
it('should not parse the dependency if the property is not in the availableProperties', () => {
const viewParticles = parse('Comp(notExist)');
const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0);
expect(content?.depMask).toEqual(0);
});
it('should not parse the dependency if the member expression is in an escaped function', () => {
it.skip('should not parse the dependency if the member expression is in an escaped function', () => {
let viewParticles = parse('Comp(escape(flag))');
let content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0);
expect(content?.depMask).toEqual(0);
viewParticles = parse('Comp($(flag))');
content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0);
expect(content?.depMask).toEqual(0);
});
it('should not parse the dependency if the member expression is in a manual function', () => {
it.skip('should not parse the dependency if the member expression is in a manual function', () => {
const viewParticles = parse('Comp(manual(() => count, []))');
const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0);
expect(content?.depMask).toEqual(0);
});
it("should parse the dependencies in manual function's second parameter", () => {
it.skip("should parse the dependencies in manual function's second parameter", () => {
const viewParticles = parse('Comp(manual(() => {let a = count}, [flag]))');
const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(1);
expect(content?.depMask).toEqual(1);
});
it('should not parse the dependency if the member expression is the left side of an assignment expression', () => {
const viewParticles = parse('Comp(flag = 1)');
const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0);
expect(content?.depMask).toEqual(0);
});
it('should not parse the dependency if the member expression is right side of an assignment expression', () => {
const viewParticles = parse('Comp(flag = flag + 1)');
const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toHaveLength(0);
expect(content?.depMask).toEqual(0);
});
it('should parse the dependency as identifiers', () => {
reactivityConfig.dependencyParseType = 'identifier';
const viewParticles = parse('Comp(flag + count)');
const content = (viewParticles[0] as CompParticle).props._$content;
expect(content?.dependencyIndexArr).toContain(availableProperties.indexOf('flag'));
expect(content?.dependencyIndexArr).toContain(availableProperties.indexOf('count'));
expect(content?.depMask).toEqual(0b11);
reactivityConfig.dependencyParseType = 'property';
});
});

View File

@ -16,12 +16,6 @@ describe('OtherParticle', () => {
expect(viewParticles[0].type).toBe('if');
});
it('should parse an IfUnit as an SwitchParticle', () => {
const viewParticles = parse('switch(this.flag) { }');
expect(viewParticles.length).toBe(1);
expect(viewParticles[0].type).toBe('switch');
});
it('should parse a ForUnit as a ForParticle', () => {
const viewParticles = parse('for(const item of this.items) { div() }');
expect(viewParticles.length).toBe(1);
@ -34,11 +28,9 @@ describe('OtherParticle', () => {
expect(viewParticles.length).toBe(1);
expect(viewParticles[0].type).toBe('for');
const divParticle = (viewParticles[0] as ForParticle).children[0] as HTMLParticle;
const divDependency = divParticle.props?.textContent?.dependencyIndexArr;
expect(divDependency).toContain(0);
expect(divDependency).toContain(1);
expect(divDependency).toContain(3);
const divParticle = (viewParticles[0] as unknown as ForParticle).children[0] as unknown as HTMLParticle;
const divDependency = divParticle.props?.textContent?.depMask;
expect(divDependency).toEqual(0b1011);
});
it("should correctly parse ForUnit's deconstruct item dependencies from array", () => {
@ -46,11 +38,9 @@ describe('OtherParticle', () => {
expect(viewParticles.length).toBe(1);
expect(viewParticles[0].type).toBe('for');
const divParticle = (viewParticles[0] as ForParticle).children[0] as HTMLParticle;
const divDependency = divParticle.props?.textContent?.dependencyIndexArr;
expect(divDependency).toContain(0);
expect(divDependency).toContain(1);
expect(divDependency).toContain(3);
const divParticle = (viewParticles[0] as unknown as ForParticle).children[0] as unknown as HTMLParticle;
const divDependency = divParticle.props?.textContent?.depMask;
expect(divDependency).toEqual(0b1011);
});
it('should parse a EnvUnit as a EnvParticle', () => {
@ -64,10 +54,4 @@ describe('OtherParticle', () => {
expect(viewParticles.length).toBe(1);
expect(viewParticles[0].type).toBe('exp');
});
it('should parse a TryUnit as a TryParticle', () => {
const viewParticles = parse('try { div() } catch(e) { div() }');
expect(viewParticles.length).toBe(1);
expect(viewParticles[0].type).toBe('try');
});
});

View File

@ -207,15 +207,18 @@ const htmlTags = [
const snippetNames = ['MySnippet', 'InnerButton'];
export const availableProperties = ['flag', 'count', 'doubleCount', 'array', 'state1', 'state2', 'state3', 'state4'];
const dependencyMap = {
count: ['flag'],
doubleCount: ['count', 'flag'],
array: ['count', 'flag'],
state1: ['count', 'flag'],
state2: ['count', 'flag', 'state1'],
state3: ['count', 'flag', 'state1', 'state2'],
state4: ['count', 'flag', 'state1', 'state2', 'state3'],
};
const depMaskMap = new Map(
Object.entries({
flag: 0b1,
count: 0b11,
doubleCount: 0b111,
array: 0b1011,
state1: 0b10011,
state2: 0b110011,
state3: 0b1110011,
state4: 0b11110011,
})
);
const viewConfig: ViewParserConfig = {
babelApi,
@ -226,7 +229,7 @@ const viewConfig: ViewParserConfig = {
export const reactivityConfig: ReactivityParserConfig = {
babelApi,
availableProperties,
dependencyMap,
depMaskMap,
};
export function parseCode(code: string) {
@ -241,10 +244,7 @@ export function parseView(code: string) {
return parseViewFromStatement(parseCode(code));
}
export function parseReactivity(statement: t.BlockStatement) {
return pR(parseViewFromStatement(statement), reactivityConfig)[0];
}
export function parse(code: string) {
// @ts-expect-error TODO: switch unit test to jsx-parser
return pR(parseView(code), reactivityConfig)[0];
}

View File

@ -3,7 +3,6 @@ import type Babel from '@babel/core';
export interface DependencyValue<T> {
value: T;
dynamic: boolean; // to removed
depMask: number; // -> bit
dependenciesNode: t.ArrayExpression;
}
@ -20,7 +19,6 @@ export interface TemplateProp {
key: string;
path: number[];
value: t.Expression;
dynamic: boolean;
depMask: number;
dependenciesNode: t.ArrayExpression;
}
@ -71,25 +69,6 @@ export interface IfParticle {
branches: IfBranch[];
}
export interface SwitchBranch {
case: DependencyValue<t.Expression>;
children: ViewParticle[];
break: boolean;
}
export interface SwitchParticle {
type: 'switch';
discriminant: DependencyValue<t.Expression>;
branches: SwitchBranch[];
}
export interface TryParticle {
type: 'try';
children: ViewParticle[];
exception: t.Identifier | t.ArrayPattern | t.ObjectPattern | null;
catchChildren: ViewParticle[];
}
export interface EnvParticle {
type: 'env';
props: Record<string, DependencyProp>;
@ -102,13 +81,6 @@ export interface ExpParticle {
props: Record<string, DependencyProp>;
}
export interface SnippetParticle {
type: 'snippet';
tag: string;
props: Record<string, DependencyProp>;
children: ViewParticle[];
}
export type ViewParticle =
| TemplateParticle
| TextParticle
@ -117,17 +89,14 @@ export type ViewParticle =
| ForParticle
| IfParticle
| EnvParticle
| ExpParticle
| SwitchParticle
| SnippetParticle
| TryParticle;
| ExpParticle;
export interface ReactivityParserConfig {
babelApi: typeof Babel;
availableProperties: string[];
availableIdentifiers?: string[];
reactiveBitMap: ReactiveBitMap;
identifierDepMap?: Record<string, string[]>;
depMaskMap: DepMaskMap;
identifierDepMap?: Record<string, Bitmap>;
dependencyParseType?: 'property' | 'identifier';
parseTemplate?: boolean;
reactivityFuncNames?: string[];
@ -135,4 +104,4 @@ export interface ReactivityParserConfig {
// TODO: unify with the types in babel-inula-next-core
type Bitmap = number;
export type ReactiveBitMap = Map<string, Bitmap>;
export type DepMaskMap = Map<string, Bitmap>;