fix(reactivity): fix for

This commit is contained in:
Hoikan 2024-05-24 17:20:30 +08:00
parent 00418eba82
commit c7de85630e
8 changed files with 90 additions and 49 deletions

View File

@ -31,10 +31,8 @@ export function pruneUnusedBit(
index = 1,
bitPositionToRemoveInParent: number[] = []
) {
// dfs the component tree
// To store the bitmap of the properties
const bitMap = new Map<string, number>();
const bitPositionToRemove: number[] = [...bitPositionToRemoveInParent];
// dfs the component tree
comp.variables.forEach(v => {
if (v.type === 'reactive') {
// get the origin bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100
@ -42,9 +40,7 @@ export function pruneUnusedBit(
if (comp.usedBit & originBit) {
v.bit = 1 << (index - bitPositionToRemove.length - 1);
bitMap.set(v.name, v.bit);
if (v.dependency) {
// 去掉最高位
v.dependency.depMask = getDepMask(v.dependency.depBitmaps, bitPositionToRemove);
}
} else {
@ -91,11 +87,19 @@ function pruneBitmap(depMask: Bitmap, bitPositionToRemove: number[]) {
return parseInt(result, 2);
}
/**
* Get the depMask by pruning the bitPositionToRemove
* The reason why we need to get the depMask from depBitmaps instead of fullDepMask is that
* the fullDepMask contains the bit of used variables, which is not the direct dependency
*
* @param depBitmaps
* @param bitPositionToRemove
*/
function getDepMask(depBitmaps: Bitmap[], bitPositionToRemove: number[]) {
// prune each dependency bitmap and combine them
return depBitmaps.reduce((acc, cur) => {
const a = pruneBitmap(cur, bitPositionToRemove);
return keepHighestBit(a) | acc;
// computed should keep the highest bit, others should be pruned
return keepHighestBit(pruneBitmap(cur, bitPositionToRemove)) | acc;
}, 0);
}

View File

@ -56,4 +56,21 @@ describe('viewAnalyze', () => {
expect(div.children[0].content.depMask).toEqual(0b1);
expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot(`"[info?.firstName]"`);
});
it('should analyze for loop', () => {
const root = analyze(/*js*/ `
Component(({}) => {
const unused = 0;
const prefix = 'test';
const list = [{name: 1}, {name: 2}, {name: 3}];
const list2 = [{name: 4}, {name: 5}, {name: 6}];
return <for each={[...list, ...list2]}>{
({name}) => <div>{prefix + name}</div>
}</for>;
});
`);
const forNode = root.children![0] as any;
expect(forNode.children[0].children[0].content.depMask).toEqual(0b111);
expect(genCode(forNode.children[0].children[0].content.dependenciesNode)).toMatchInlineSnapshot(`"[prefix, name]"`);
});
});

View File

@ -83,4 +83,15 @@ describe('jsx slice', () => {
}"
`);
});
// TODO: Fix this test
it.skip('should work with jsx slice in function', () => {
// function App() {
// const fn = ([x, y, z]) => {
// return <div>{x}, {y}, {z}</div>
// }
//
// return <div>{fn([1, 2, 3])}</div>
// }
});
});

View File

@ -537,8 +537,8 @@ export class ViewParser {
private pareFor(node: t.JSXElement) {
// ---- Get array
const arrayContainer = this.findProp(node, 'array');
if (!arrayContainer) throw new Error('Missing [array] prop in for loop');
const arrayContainer = this.findProp(node, 'each');
if (!arrayContainer) throw new Error('Missing [each] prop in for loop');
if (!this.t.isJSXExpressionContainer(arrayContainer.value))
throw new Error('Expected expression container for [array] prop');
const array = arrayContainer.value.expression;
@ -554,24 +554,31 @@ export class ViewParser {
}
// ---- Get Item
const itemProp = this.findProp(node, 'item');
if (!itemProp) throw new Error('Missing [item] prop in for loop');
if (!this.t.isJSXExpressionContainer(itemProp.value))
throw new Error('Expected expression container for [item] prop');
const item = itemProp.value.expression;
if (this.t.isJSXEmptyExpression(item)) throw new Error('Expected [item] expression not empty');
// ---- ObjectExpression to ObjectPattern / ArrayExpression to ArrayPattern
this.traverse(this.wrapWithFile(item), {
ObjectExpression: path => {
path.node.type = 'ObjectPattern' as any;
},
ArrayExpression: path => {
path.node.type = 'ArrayPattern' as any;
},
});
// <for each={data}>
// {(item, idx)=><Comp_$id1$item={item}idx={idx}/>)}
// </for>
const jsxChildren = node.children;
if (jsxChildren.length !== 1) throw new Error('Expected 1 child');
if (jsxChildren[0].type !== 'JSXExpressionContainer') throw new Error('Expected expression container');
const itemFnNode = jsxChildren[0].expression;
if (this.t.isJSXEmptyExpression(itemFnNode)) throw new Error('Expected expression not empty');
// ---- Get children
const children = this.t.jsxFragment(this.t.jsxOpeningFragment(), this.t.jsxClosingFragment(), node.children);
let children;
if (!this.t.isFunctionExpression(itemFnNode) && !this.t.isArrowFunctionExpression(itemFnNode)) {
throw new Error('For: Expected function expression');
}
// get the return value
if (this.t.isBlockStatement(itemFnNode.body)) {
if (itemFnNode.body.body.length !== 1) throw new Error('For: Expected 1 statement in block statement');
if (!this.t.isReturnStatement(itemFnNode.body.body[0]))
throw new Error('For: Expected return statement in block statement');
children = itemFnNode.body.body[0].argument;
} else {
children = itemFnNode.body;
}
const item = itemFnNode.params[0];
if (!this.t.isJSXElement(children)) throw new Error('For: Expected jsx element in return statement');
this.viewUnits.push({
type: 'for',

View File

@ -1,11 +1,10 @@
import { describe, expect, it } from 'vitest';
import { parse } from './mock';
describe('ForUnit', () => {
it('should identify for unit', () => {
const viewUnits = parse('<for array={items} item={item}> <div>{item}</div> </for>');
const viewUnits = parse('<for each={items}>{([x, y, z], idx) => <Comp$1 x={x} y={y} z={z} idx={idx}/>}</for>');
expect(viewUnits.length).toBe(1);
expect(viewUnits[0].type).toBe('for');
});
});
});

View File

@ -40,7 +40,7 @@ export type Dependency = {
*/
export function getDependenciesFromNode(
node: t.Expression | t.Statement,
reactiveMap: Map<string, number>,
reactiveMap: Map<string, Bitmap | Bitmap[]>,
reactivityFuncNames: string[]
): Dependency {
// ---- Deps: console.log(count)
@ -57,9 +57,15 @@ export function getDependenciesFromNode(
if (reactiveBitmap !== undefined) {
if (isAssignmentExpressionLeft(innerPath) || isAssignmentFunction(innerPath, reactivityFuncNames)) {
assignDepMask |= reactiveBitmap;
assignDepMask |= Array.isArray(reactiveBitmap)
? reactiveBitmap.reduce((acc, cur) => acc | cur, 0)
: reactiveBitmap;
} else {
depBitmaps.push(reactiveBitmap);
if (Array.isArray(reactiveBitmap)) {
depBitmaps.push(...reactiveBitmap);
} else {
depBitmaps.push(reactiveBitmap);
}
if (!depNodes[propertyKey]) depNodes[propertyKey] = [];
depNodes[propertyKey].push(geneDependencyNode(innerPath));

View File

@ -319,34 +319,31 @@ export class ReactivityParser {
* @returns ForParticle
*/
private parseFor(forUnit: ForUnit): ForParticle {
const { depMask, dependenciesNode } = this.getDependencies(forUnit.array);
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;
const { fullDepMask, dependenciesNode, depBitmaps } = this.getDependencies(forUnit.array);
const prevMap = this.config.depMaskMap;
// ---- Generate an identifierDepMap to track identifiers in item and make them reactive
// based on the dependencies from the array
// 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]),
// ]);
// Just wrap the item in an assignment expression to get all the identifiers
const itemWrapper = this.t.assignmentExpression('=', forUnit.item, this.t.objectExpression([]));
this.config.depMaskMap = new Map([
...this.config.depMaskMap,
...this.getIdentifiers(itemWrapper).map(id => [id, depBitmaps] as const),
]);
const forParticle: ForParticle = {
type: 'for',
item: forUnit.item,
array: {
value: forUnit.array,
depBitmaps: [],
depMask: depMask,
depBitmaps,
depMask: fullDepMask,
dependenciesNode,
},
children: forUnit.children.map(this.parseViewParticle.bind(this)),
key: forUnit.key,
};
// this.config.identifierDepMap = prevIdentifierDepMap;
this.config.depMaskMap = prevMap;
return forParticle;
}
@ -427,7 +424,7 @@ export class ReactivityParser {
private getDependencies(node: t.Expression | t.Statement) {
if (this.t.isFunctionExpression(node) || this.t.isArrowFunctionExpression(node)) {
return {
depMask: 0,
fullDepMask: 0,
depBitmaps: [],
dependenciesNode: this.t.arrayExpression([]),
};

View File

@ -104,4 +104,4 @@ export interface ReactivityParserConfig {
// TODO: unify with the types in babel-inula-next-core
export type Bitmap = number;
export type DepMaskMap = Map<string, Bitmap>;
export type DepMaskMap = Map<string, Bitmap | Bitmap[]>;