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, index = 1,
bitPositionToRemoveInParent: number[] = [] bitPositionToRemoveInParent: number[] = []
) { ) {
// dfs the component tree
// To store the bitmap of the properties
const bitMap = new Map<string, number>();
const bitPositionToRemove: number[] = [...bitPositionToRemoveInParent]; const bitPositionToRemove: number[] = [...bitPositionToRemoveInParent];
// dfs the component tree
comp.variables.forEach(v => { comp.variables.forEach(v => {
if (v.type === 'reactive') { if (v.type === 'reactive') {
// get the origin bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100 // get the origin bit, computed should keep the highest bit, etc. 0b0111 -> 0b0100
@ -42,9 +40,7 @@ export function pruneUnusedBit(
if (comp.usedBit & originBit) { if (comp.usedBit & originBit) {
v.bit = 1 << (index - bitPositionToRemove.length - 1); v.bit = 1 << (index - bitPositionToRemove.length - 1);
bitMap.set(v.name, v.bit);
if (v.dependency) { if (v.dependency) {
// 去掉最高位
v.dependency.depMask = getDepMask(v.dependency.depBitmaps, bitPositionToRemove); v.dependency.depMask = getDepMask(v.dependency.depBitmaps, bitPositionToRemove);
} }
} else { } else {
@ -91,11 +87,19 @@ function pruneBitmap(depMask: Bitmap, bitPositionToRemove: number[]) {
return parseInt(result, 2); 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[]) { function getDepMask(depBitmaps: Bitmap[], bitPositionToRemove: number[]) {
// prune each dependency bitmap and combine them // prune each dependency bitmap and combine them
return depBitmaps.reduce((acc, cur) => { return depBitmaps.reduce((acc, cur) => {
const a = pruneBitmap(cur, bitPositionToRemove); // computed should keep the highest bit, others should be pruned
return keepHighestBit(a) | acc; return keepHighestBit(pruneBitmap(cur, bitPositionToRemove)) | acc;
}, 0); }, 0);
} }

View File

@ -56,4 +56,21 @@ describe('viewAnalyze', () => {
expect(div.children[0].content.depMask).toEqual(0b1); expect(div.children[0].content.depMask).toEqual(0b1);
expect(genCode(div.children[0].content.dependenciesNode)).toMatchInlineSnapshot(`"[info?.firstName]"`); 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) { private pareFor(node: t.JSXElement) {
// ---- Get array // ---- Get array
const arrayContainer = this.findProp(node, 'array'); const arrayContainer = this.findProp(node, 'each');
if (!arrayContainer) throw new Error('Missing [array] prop in for loop'); if (!arrayContainer) throw new Error('Missing [each] prop in for loop');
if (!this.t.isJSXExpressionContainer(arrayContainer.value)) if (!this.t.isJSXExpressionContainer(arrayContainer.value))
throw new Error('Expected expression container for [array] prop'); throw new Error('Expected expression container for [array] prop');
const array = arrayContainer.value.expression; const array = arrayContainer.value.expression;
@ -554,24 +554,31 @@ export class ViewParser {
} }
// ---- Get Item // ---- Get Item
const itemProp = this.findProp(node, 'item'); // <for each={data}>
if (!itemProp) throw new Error('Missing [item] prop in for loop'); // {(item, idx)=><Comp_$id1$item={item}idx={idx}/>)}
if (!this.t.isJSXExpressionContainer(itemProp.value)) // </for>
throw new Error('Expected expression container for [item] prop'); const jsxChildren = node.children;
const item = itemProp.value.expression; if (jsxChildren.length !== 1) throw new Error('Expected 1 child');
if (this.t.isJSXEmptyExpression(item)) throw new Error('Expected [item] expression not empty'); if (jsxChildren[0].type !== 'JSXExpressionContainer') throw new Error('Expected expression container');
// ---- ObjectExpression to ObjectPattern / ArrayExpression to ArrayPattern const itemFnNode = jsxChildren[0].expression;
this.traverse(this.wrapWithFile(item), { if (this.t.isJSXEmptyExpression(itemFnNode)) throw new Error('Expected expression not empty');
ObjectExpression: path => {
path.node.type = 'ObjectPattern' as any;
},
ArrayExpression: path => {
path.node.type = 'ArrayPattern' as any;
},
});
// ---- Get children let children;
const children = this.t.jsxFragment(this.t.jsxOpeningFragment(), this.t.jsxClosingFragment(), node.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({ this.viewUnits.push({
type: 'for', type: 'for',

View File

@ -1,11 +1,10 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { parse } from './mock'; import { parse } from './mock';
describe('ForUnit', () => { describe('ForUnit', () => {
it('should identify for unit', () => { 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.length).toBe(1);
expect(viewUnits[0].type).toBe('for'); expect(viewUnits[0].type).toBe('for');
}); });
}); });

View File

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

View File

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

View File

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