From 8ddc8f6a63d6a0033205edfb04d6db97076e0f44 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 5 May 2025 13:26:00 +0800 Subject: [PATCH] wip --- .../__snapshots__/vFor.spec.ts.snap | 90 +++++ .../__tests__/transforms/vFor.spec.ts | 67 ++++ .../compiler-vapor/src/generators/block.ts | 9 +- .../src/generators/expression.ts | 24 +- packages/compiler-vapor/src/generators/for.ts | 302 ++++++++++++++- .../src/generators/operation.ts | 22 +- packages/runtime-vapor/__tests__/for.spec.ts | 2 +- packages/runtime-vapor/src/apiCreateFor.ts | 351 +++++++++++------- rollup.config.js | 1 + 9 files changed, 706 insertions(+), 162 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap index cb14f56afdb..c87a37ba5d2 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -47,6 +47,27 @@ export function render(_ctx) { }" `; +exports[`compiler: v-for > key only binding pattern 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template(" ", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { + const n2 = t0() + const x2 = _child(n2) + let _row, _row_id + { + _row = _for_item0.value + _row_id = _row.id + + } + _setText(x2, _toDisplayString(_row_id + _row_id)) + return n2 + }, (row) => (row.id)) + return n0 +}" +`; + exports[`compiler: v-for > multi effect 1`] = ` "import { setProp as _setProp, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; const t0 = _template("
", true) @@ -115,6 +136,75 @@ export function render(_ctx) { }" `; +exports[`compiler: v-for > selector pattern 1`] = ` +"import { child as _child, toDisplayString as _toDisplayString, setText as _setText, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template(" ", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { + const n2 = t0() + const x2 = _child(n2) + _selector0_0(() => { + _setText(x2, _toDisplayString(_ctx.selected === _for_item0.value.id ? 'danger' : '')) + }) + return n2 + }, (row) => (row.id)) + const _selector0_0 = n0.useSelector(() => _ctx.selected) + return n0 +}" +`; + +exports[`compiler: v-for > selector pattern 2`] = ` +"import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { + const n2 = t0() + _selector0_0(() => { + _setClass(n2, _ctx.selected === _for_item0.value.id ? 'danger' : '') + }) + return n2 + }, (row) => (row.id)) + const _selector0_0 = n0.useSelector(() => _ctx.selected) + return n0 +}" +`; + +exports[`compiler: v-for > selector pattern 3`] = ` +"import { setClass as _setClass, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { + const n2 = t0() + _renderEffect(() => { + const _row = _for_item0.value + _setClass(n2, _row.label === _row.id ? 'danger' : '') + }) + return n2 + }, (row) => (row.id)) + return n0 +}" +`; + +exports[`compiler: v-for > selector pattern 4`] = ` +"import { setClass as _setClass, createFor as _createFor, template as _template } from 'vue'; +const t0 = _template("", true) + +export function render(_ctx) { + const n0 = _createFor(() => (_ctx.rows), (_for_item0) => { + const n2 = t0() + _selector0_0(() => { + _setClass(n2, { danger: _for_item0.value.id === _ctx.selected }) + }) + return n2 + }, (row) => (row.id)) + const _selector0_0 = n0.useSelector(() => _ctx.selected) + return n0 +}" +`; + exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = ` "import { getDefaultValue as _getDefaultValue, child as _child, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue'; const t0 = _template("
", true) diff --git a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts index 0008df7f4c7..91b8526b329 100644 --- a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts @@ -67,6 +67,73 @@ describe('compiler: v-for', () => { ).lengthOf(1) }) + test('key only binding pattern', () => { + expect( + compileWithVFor( + ` + + {{ row.id + row.id }} + + `, + ).code, + ).matchSnapshot() + }) + + test('selector pattern', () => { + expect( + compileWithVFor( + ` + + {{ selected === row.id ? 'danger' : '' }} + + `, + ).code, + ).matchSnapshot() + + expect( + compileWithVFor( + ` + + `, + ).code, + ).matchSnapshot() + + // Should not be optimized because row.label is not from parent scope + expect( + compileWithVFor( + ` + + `, + ).code, + ).matchSnapshot() + + expect( + compileWithVFor( + ` + + `, + ).code, + ).matchSnapshot() + }) + test('multi effect', () => { const { code } = compileWithVFor( `
`, diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts index b161b8f45d1..66b57c58378 100644 --- a/packages/compiler-vapor/src/generators/block.ts +++ b/packages/compiler-vapor/src/generators/block.ts @@ -19,14 +19,13 @@ export function genBlock( context: CodegenContext, args: CodeFragment[] = [], root?: boolean, - customReturns?: (returns: CodeFragment[]) => CodeFragment[], ): CodeFragment[] { return [ '(', ...args, ') => {', INDENT_START, - ...genBlockContent(oper, context, root, customReturns), + ...genBlockContent(oper, context, root), INDENT_END, NEWLINE, '}', @@ -37,7 +36,7 @@ export function genBlockContent( block: BlockIRNode, context: CodegenContext, root?: boolean, - customReturns?: (returns: CodeFragment[]) => CodeFragment[], + genEffectsExtraFrag?: () => CodeFragment[], ): CodeFragment[] { const [frag, push] = buildCodeFragment() const { dynamic, effect, operation, returns } = block @@ -56,7 +55,7 @@ export function genBlockContent( } push(...genOperations(operation, context)) - push(...genEffects(effect, context)) + push(...genEffects(effect, context, genEffectsExtraFrag)) push(NEWLINE, `return `) @@ -65,7 +64,7 @@ export function genBlockContent( returnNodes.length > 1 ? genMulti(DELIMITERS_ARRAY, ...returnNodes) : [returnNodes[0] || 'null'] - push(...(customReturns ? customReturns(returnsCode) : returnsCode)) + push(...returnsCode) resetBlock() return frag diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts index eedaeeb380a..ab7254d6d79 100644 --- a/packages/compiler-vapor/src/generators/expression.ts +++ b/packages/compiler-vapor/src/generators/expression.ts @@ -230,6 +230,7 @@ function canPrefix(name: string) { type DeclarationResult = { ids: Record frag: CodeFragment[] + varNames: string[] } type DeclarationValue = { name: string @@ -243,6 +244,7 @@ type DeclarationValue = { export function processExpressions( context: CodegenContext, expressions: SimpleExpressionNode[], + shouldDeclare: boolean, ): DeclarationResult { // analyze variables const { seenVariable, variableToExpMap, expToVariableMap, seenIdentifier } = @@ -266,7 +268,11 @@ export function processExpressions( varDeclarations, ) - return genDeclarations([...varDeclarations, ...expDeclarations], context) + return genDeclarations( + [...varDeclarations, ...expDeclarations], + context, + shouldDeclare, + ) } function analyzeExpressions(expressions: SimpleExpressionNode[]) { @@ -507,15 +513,21 @@ function processRepeatedExpressions( function genDeclarations( declarations: DeclarationValue[], context: CodegenContext, + shouldDeclare: boolean, ): DeclarationResult { const [frag, push] = buildCodeFragment() const ids: Record = Object.create(null) + const varNames = new Set() // process identifiers first as expressions may rely on them declarations.forEach(({ name, isIdentifier, value }) => { if (isIdentifier) { const varName = (ids[name] = `_${name}`) - push(`const ${varName} = `, ...genExpression(value, context), NEWLINE) + varNames.add(varName) + if (shouldDeclare) { + push(`const `) + } + push(`${varName} = `, ...genExpression(value, context), NEWLINE) } }) @@ -523,15 +535,19 @@ function genDeclarations( declarations.forEach(({ name, isIdentifier, value }) => { if (!isIdentifier) { const varName = (ids[name] = `_${name}`) + varNames.add(varName) + if (shouldDeclare) { + push(`const `) + } push( - `const ${varName} = `, + `${varName} = `, ...context.withId(() => genExpression(value, context), ids), NEWLINE, ) } }) - return { ids, frag } + return { ids, frag, varNames: [...varNames] } } function escapeRegExp(string: string) { diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts index fbb72c61d47..40f002a8536 100644 --- a/packages/compiler-vapor/src/generators/for.ts +++ b/packages/compiler-vapor/src/generators/for.ts @@ -1,16 +1,32 @@ import { type SimpleExpressionNode, createSimpleExpression, + isStaticNode, walkIdentifiers, } from '@vue/compiler-dom' -import { genBlock } from './block' +import { genBlockContent } from './block' import { genExpression } from './expression' import type { CodegenContext } from '../generate' -import type { ForIRNode } from '../ir' -import { type CodeFragment, NEWLINE, genCall, genMulti } from './utils' -import type { Identifier } from '@babel/types' +import type { BlockIRNode, ForIRNode, IREffect } from '../ir' +import { + type CodeFragment, + INDENT_END, + INDENT_START, + NEWLINE, + genCall, + genMulti, +} from './utils' +import { + type Expression, + type Identifier, + type Node, + isNodesEquivalent, +} from '@babel/types' import { parseExpression } from '@babel/parser' import { VaporVForFlags } from '../../../shared/src/vaporFlags' +import { walk } from 'estree-walker' +import { genOperation } from './operation' +import { extend, isGloballyAllowed } from '@vue/shared' export function genFor( oper: ForIRNode, @@ -78,7 +94,62 @@ export function genFor( idMap[indexVar] = null } - const blockFn = context.withId(() => genBlock(render, context, args), idMap) + const { selectorPatterns, keyOnlyBindingPatterns } = matchPatterns( + render, + keyProp, + idMap, + ) + const patternFrag: CodeFragment[] = [] + + for (let i = 0; i < selectorPatterns.length; i++) { + const { selector } = selectorPatterns[i] + const selectorName = `_selector${id}_${i}` + patternFrag.push( + NEWLINE, + `const ${selectorName} = `, + ...genCall(`n${id}.useSelector`, [ + `() => `, + ...genExpression(selector, context), + ]), + ) + } + + const blockFn = context.withId(() => { + const frag: CodeFragment[] = [] + frag.push('(', ...args, ') => {', INDENT_START) + if (selectorPatterns.length || keyOnlyBindingPatterns.length) { + frag.push( + ...genBlockContent(render, context, false, () => { + const patternFrag: CodeFragment[] = [] + + for (let i = 0; i < selectorPatterns.length; i++) { + const { effect } = selectorPatterns[i] + patternFrag.push( + NEWLINE, + `_selector${id}_${i}(() => {`, + INDENT_START, + ) + for (const oper of effect.operations) { + patternFrag.push(...genOperation(oper, context)) + } + patternFrag.push(INDENT_END, NEWLINE, `})`) + } + + for (const { effect } of keyOnlyBindingPatterns) { + for (const oper of effect.operations) { + patternFrag.push(...genOperation(oper, context)) + } + } + + return patternFrag + }), + ) + } else { + frag.push(...genBlockContent(render, context)) + } + frag.push(INDENT_END, NEWLINE, '}') + return frag + }, idMap) exitScope() let flags = 0 @@ -103,6 +174,7 @@ export function genFor( flags ? String(flags) : undefined, // todo: hydrationNode ), + ...patternFrag, ] // construct a id -> accessor path map. @@ -234,3 +306,223 @@ export function genFor( return idMap } } + +function matchPatterns( + render: BlockIRNode, + keyProp: SimpleExpressionNode | undefined, + idMap: Record, +) { + const selectorPatterns: NonNullable< + ReturnType + >[] = [] + const keyOnlyBindingPatterns: NonNullable< + ReturnType + >[] = [] + + render.effect = render.effect.filter(effect => { + if (keyProp !== undefined) { + const selector = matchSelectorPattern(effect, keyProp.ast, idMap) + if (selector) { + selectorPatterns.push(selector) + return false + } + const keyOnly = matchKeyOnlyBindingPattern(effect, keyProp.ast) + if (keyOnly) { + keyOnlyBindingPatterns.push(keyOnly) + return false + } + } + + return true + }) + + return { + keyOnlyBindingPatterns, + selectorPatterns, + } +} + +function matchKeyOnlyBindingPattern( + effect: IREffect, + keyAst: any, +): + | { + effect: IREffect + } + | undefined { + // TODO: expressions can be multiple? + if (effect.expressions.length === 1) { + const ast = effect.expressions[0].ast + if (typeof ast === 'object' && ast !== null) { + if (isKeyOnlyBinding(ast, keyAst)) { + return { effect } + } + } + } +} + +function matchSelectorPattern( + effect: IREffect, + keyAst: any, + idMap: Record, +): + | { + effect: IREffect + selector: SimpleExpressionNode + } + | undefined { + // TODO: expressions can be multiple? + if (effect.expressions.length === 1) { + const ast = effect.expressions[0].ast + if (typeof ast === 'object' && ast) { + const matcheds: [key: Expression, selector: Expression][] = [] + + walk(ast, { + enter(node) { + if ( + typeof node === 'object' && + node && + node.type === 'BinaryExpression' && + node.operator === '===' && + node.left.type !== 'PrivateName' + ) { + const { left, right } = node + for (const [a, b] of [ + [left, right], + [right, left], + ]) { + const aIsKey = isKeyOnlyBinding(a, keyAst) + const bIsKey = isKeyOnlyBinding(b, keyAst) + const bVars = analyzeVariableScopes(b, idMap) + if (aIsKey && !bIsKey && !bVars.locals.length) { + matcheds.push([a, b]) + } + } + } + }, + }) + + if (matcheds.length === 1) { + const [key, selector] = matcheds[0] + const content = effect.expressions[0].content + + let hasExtraId = false + const parentStackMap = new Map() + const parentStack: Node[] = [] + walkIdentifiers( + ast, + id => { + if (id.start !== key.start && id.start !== selector.start) { + hasExtraId = true + } + parentStackMap.set(id, parentStack.slice()) + }, + false, + parentStack, + ) + + if (!hasExtraId) { + const name = content.slice(selector.start! - 1, selector.end! - 1) + return { + effect, + // @ts-expect-error + selector: { + content: name, + ast: extend({}, selector, { + start: 1, + end: name.length + 1, + }), + loc: selector.loc as any, + isStatic: false, + }, + } + } + } + } + + const content = effect.expressions[0].content + if ( + typeof ast === 'object' && + ast && + ast.type === 'ConditionalExpression' && + ast.test.type === 'BinaryExpression' && + ast.test.operator === '===' && + ast.test.left.type !== 'PrivateName' && + isStaticNode(ast.consequent) && + isStaticNode(ast.alternate) + ) { + const left = ast.test.left + const right = ast.test.right + for (const [a, b] of [ + [left, right], + [right, left], + ]) { + const aIsKey = isKeyOnlyBinding(a, keyAst) + const bIsKey = isKeyOnlyBinding(b, keyAst) + const bVars = analyzeVariableScopes(b, idMap) + if (aIsKey && !bIsKey && !bVars.locals.length) { + return { + effect, + // @ts-expect-error + selector: { + content: content.slice(b.start! - 1, b.end! - 1), + ast: b, + loc: b.loc as any, + isStatic: false, + }, + } + } + } + } + } +} + +function analyzeVariableScopes( + ast: Node, + idMap: Record, +) { + let globals: string[] = [] + let locals: string[] = [] + + const ids: Identifier[] = [] + const parentStackMap = new Map() + const parentStack: Node[] = [] + walkIdentifiers( + ast, + id => { + ids.push(id) + parentStackMap.set(id, parentStack.slice()) + }, + false, + parentStack, + ) + + for (const id of ids) { + if (isGloballyAllowed(id.name)) { + continue + } + if (idMap[id.name]) { + locals.push(id.name) + } else { + globals.push(id.name) + } + } + + return { globals, locals } +} + +function isKeyOnlyBinding(expr: Node, keyAst: any) { + let only = true + walk(expr, { + enter(node) { + if (isNodesEquivalent(node, keyAst)) { + this.skip() + return + } + if (node.type === 'Identifier') { + only = false + } + }, + }) + return only +} diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts index 4247bc6feca..b5f99f8ed07 100644 --- a/packages/compiler-vapor/src/generators/operation.ts +++ b/packages/compiler-vapor/src/generators/operation.ts @@ -98,17 +98,20 @@ export function genOperation( export function genEffects( effects: IREffect[], context: CodegenContext, + genExtraFrag?: () => CodeFragment[], ): CodeFragment[] { const { helper, block: { expressions }, } = context const [frag, push, unshift] = buildCodeFragment() + const shouldDeclareConst = genExtraFrag === undefined let operationsCount = 0 - const { ids, frag: declarationFrags } = processExpressions( - context, - expressions, - ) + const { + ids, + frag: declarationFrags, + varNames, + } = processExpressions(context, expressions, shouldDeclareConst) push(...declarationFrags) for (let i = 0; i < effects.length; i++) { const effect = effects[i] @@ -125,6 +128,9 @@ export function genEffects( if (newLineCount > 1 || operationsCount > 1 || declarationFrags.length > 0) { unshift(`{`, INDENT_START, NEWLINE) push(INDENT_END, NEWLINE, '}') + if (!effects.length) { + unshift(NEWLINE) + } } if (effects.length) { @@ -132,6 +138,14 @@ export function genEffects( push(`)`) } + if (!shouldDeclareConst && varNames.length) { + unshift(NEWLINE, `let `, varNames.join(', ')) + } + + if (genExtraFrag) { + push(...context.withId(genExtraFrag, ids)) + } + return frag } diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts index 7ba6023b1e9..02120002607 100644 --- a/packages/runtime-vapor/__tests__/for.spec.ts +++ b/packages/runtime-vapor/__tests__/for.spec.ts @@ -94,7 +94,7 @@ describe('createFor', () => { }) return span }, - item => item.name, + item => item, ) return n1 }).render() diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 0cd8317532f..b2000306f51 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -8,8 +8,9 @@ import { shallowReadArray, shallowRef, toReactive, + watch, } from '@vue/reactivity' -import { getSequence, isArray, isObject, isString } from '@vue/shared' +import { isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode } from './dom/node' import { type Block, @@ -78,12 +79,18 @@ export const createFor = ( let oldBlocks: ForBlock[] = [] let newBlocks: ForBlock[] let parent: ParentNode | undefined | null + // useSelector only + let currentKey: any // TODO handle this in hydration const parentAnchor = __DEV__ ? createComment('for') : createTextNode() const frag = new VaporFragment(oldBlocks) const instance = currentInstance! - const canUseFastRemove = flags & VaporVForFlags.FAST_REMOVE - const isComponent = flags & VaporVForFlags.IS_COMPONENT + const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE) + const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT) + const selectors: { + deregister: (key: any) => void + cleanup: () => void + }[] = [] if (__DEV__ && !instance) { warn('createFor() can only be used inside setup()') @@ -111,9 +118,12 @@ export const createFor = ( } } else if (!newLength) { // fast path for clearing all + for (const selector of selectors) { + selector.cleanup() + } const doRemove = !canUseFastRemove for (let i = 0; i < oldLength; i++) { - unmount(oldBlocks[i], doRemove) + unmount(oldBlocks[i], doRemove, false) } if (canUseFastRemove) { parent!.textContent = '' @@ -132,149 +142,148 @@ export const createFor = ( unmount(oldBlocks[i]) } } else { - let i = 0 - let e1 = oldLength - 1 // prev ending index - let e2 = newLength - 1 // next ending index - - // 1. sync from start - // (a b) c - // (a b) d e - while (i <= e1 && i <= e2) { - if (tryPatchIndex(source, i)) { - i++ - } else { - break + const commonLength = Math.min(oldLength, newLength) + const oldKeyToIndex: [any, number][] = new Array(oldLength) + const pendingNews: [ + index: number, + item: ReturnType, + key: any, + ][] = new Array(newLength) + + let defaultAnchor: Node = parentAnchor + let right = 0 + let left = 0 + let l1 = 0 + let l2 = 0 + + while (right < commonLength) { + const index = newLength - right - 1 + const item = getItem(source, index) + const key = getKey(...item) + const block = oldBlocks[oldLength - right - 1] + if (block.key === key) { + update(block, ...item) + newBlocks[index] = block + right++ + continue + } + if (right !== 0) { + defaultAnchor = normalizeAnchor(newBlocks[index + 1].nodes) } + break } - // 2. sync from end - // a (b c) - // d e (b c) - while (i <= e1 && i <= e2) { - if (tryPatchIndex(source, i)) { - e1-- - e2-- + while (left < commonLength - right) { + const item = getItem(source, left) + const key = getKey(...item) + const oldBlock = oldBlocks[left] + const oldKey = oldBlock.key + if (oldKey === key) { + update((newBlocks[left] = oldBlock), item[0]) } else { - break + pendingNews[l1++] = [left, item, key] + oldKeyToIndex[l2++] = [oldKey, left] } + left++ } - // 3. common sequence + mount - // (a b) - // (a b) c - // i = 2, e1 = 1, e2 = 2 - // (a b) - // c (a b) - // i = 0, e1 = -1, e2 = 0 - if (i > e1) { - if (i <= e2) { - const nextPos = e2 + 1 - const anchor = - nextPos < newLength - ? normalizeAnchor(newBlocks[nextPos].nodes) - : parentAnchor - while (i <= e2) { - mount(source, i, anchor) - i++ - } - } + for (let i = left; i < oldLength - right; i++) { + oldKeyToIndex[l2++] = [oldBlocks[i].key, i] } - // 4. common sequence + unmount - // (a b) c - // (a b) - // i = 2, e1 = 2, e2 = 1 - // a (b c) - // (b c) - // i = 0, e1 = 0, e2 = -1 - else if (i > e2) { - while (i <= e1) { - unmount(oldBlocks[i]) - i++ - } + const prepareLength = Math.min(newLength - right, commonLength) + for (let i = left; i < prepareLength; i++) { + const item = getItem(source, i) + const key = getKey(...item) + pendingNews[l1++] = [i, item, key] } - // 5. unknown sequence - // [i ... e1 + 1]: a b [c d e] f g - // [i ... e2 + 1]: a b [e d c h] f g - // i = 2, e1 = 4, e2 = 5 - else { - const s1 = i // prev starting index - const s2 = i // next starting index - - // 5.1 build key:index map for newChildren - const keyToNewIndexMap = new Map() - for (i = s2; i <= e2; i++) { - keyToNewIndexMap.set(getKey(...getItem(source, i)), i) + if (!l1 && !l2) { + for (let i = prepareLength; i < newLength - right; i++) { + const item = getItem(source, i) + const key = getKey(...item) + mount(source, i, item, key, defaultAnchor) } - - // 5.2 loop through old children left to be patched and try to patch - // matching nodes & remove nodes that are no longer present - let j - let patched = 0 - const toBePatched = e2 - s2 + 1 - let moved = false - // used to track whether any node has moved - let maxNewIndexSoFar = 0 - // works as Map - // Note that oldIndex is offset by +1 - // and oldIndex = 0 is a special value indicating the new node has - // no corresponding old node. - // used for determining longest stable subsequence - const newIndexToOldIndexMap = new Array(toBePatched).fill(0) - - for (i = s1; i <= e1; i++) { - const prevBlock = oldBlocks[i] - if (patched >= toBePatched) { - // all new children have been patched so this can only be a removal - unmount(prevBlock) + } else { + pendingNews.length = l1 + oldKeyToIndex.length = l2 + + const oldKeyToIndexMap = new Map(oldKeyToIndex) + const pendingMounts: [ + index: number, + item: ReturnType, + key: any, + anchorIndex: number, + ][] = [] + const moveOrMount = ( + index: number, + item: ReturnType, + key: any, + anchorIndex: number, + ) => { + const oldIndex = oldKeyToIndexMap.get(key) + if (oldIndex !== undefined) { + const block = (newBlocks[index] = oldBlocks[oldIndex]) + update(block, ...item) + insert( + block, + parent!, + anchorIndex === -1 + ? defaultAnchor + : normalizeAnchor(newBlocks[anchorIndex].nodes), + ) + oldKeyToIndexMap.delete(key) } else { - const newIndex = keyToNewIndexMap.get(prevBlock.key) - if (newIndex == null) { - unmount(prevBlock) - } else { - newIndexToOldIndexMap[newIndex - s2] = i + 1 - if (newIndex >= maxNewIndexSoFar) { - maxNewIndexSoFar = newIndex - } else { - moved = true - } - update( - (newBlocks[newIndex] = prevBlock), - ...getItem(source, newIndex), - ) - patched++ - } + pendingMounts.push([index, item, key, anchorIndex]) } } - // 5.3 move and mount - // generate longest stable subsequence only when nodes have moved - const increasingNewIndexSequence = moved - ? getSequence(newIndexToOldIndexMap) - : [] - j = increasingNewIndexSequence.length - 1 - // looping backwards so that we can use last patched node as anchor - for (i = toBePatched - 1; i >= 0; i--) { - const nextIndex = s2 + i - const anchor = - nextIndex + 1 < newLength - ? normalizeAnchor(newBlocks[nextIndex + 1].nodes) - : parentAnchor - if (newIndexToOldIndexMap[i] === 0) { - // mount new - mount(source, nextIndex, anchor) - } else if (moved) { - // move if: - // There is no stable subsequence (e.g. a reverse) - // OR current node is not among the stable sequence - if (j < 0 || i !== increasingNewIndexSequence[j]) { - insert(newBlocks[nextIndex].nodes, parent!, anchor) - } else { - j-- - } + for (let i = pendingNews.length - 1; i >= 0; i--) { + const [index, item, key] = pendingNews[i] + moveOrMount( + index, + item, + key, + index < prepareLength - 1 ? index + 1 : -1, + ) + } + + for (let i = prepareLength; i < newLength - right; i++) { + const item = getItem(source, i) + const key = getKey(...item) + moveOrMount(i, item, key, -1) + } + + const shouldUseFastRemove = pendingMounts.length === newLength + + for (const i of oldKeyToIndexMap.values()) { + unmount( + oldBlocks[i], + !(shouldUseFastRemove && canUseFastRemove), + !shouldUseFastRemove, + ) + } + if (shouldUseFastRemove) { + for (const selector of selectors) { + selector.cleanup() + } + if (canUseFastRemove) { + parent!.textContent = '' + parent!.appendChild(parentAnchor) } } + + for (const [index, item, key, anchorIndex] of pendingMounts) { + mount( + source, + index, + item, + key, + anchorIndex === -1 + ? defaultAnchor + : normalizeAnchor(newBlocks[anchorIndex].nodes), + ) + } } } } @@ -293,14 +302,16 @@ export const createFor = ( const mount = ( source: ResolvedSource, idx: number, + [item, key, index] = getItem(source, idx), + key2 = getKey && getKey(item, key, index), anchor: Node | undefined = parentAnchor, ): ForBlock => { - const [item, key, index] = getItem(source, idx) const itemRef = shallowRef(item) // avoid creating refs if the render fn doesn't need it const keyRef = needKey ? shallowRef(key) : undefined const indexRef = needIndex ? shallowRef(index) : undefined + currentKey = key2 let nodes: Block let scope: EffectScope | undefined if (isComponent) { @@ -319,7 +330,7 @@ export const createFor = ( itemRef, keyRef, indexRef, - getKey && getKey(item, key, index), + key2, )) if (parent) insert(block.nodes, parent, anchor) @@ -327,15 +338,6 @@ export const createFor = ( return block } - const tryPatchIndex = (source: any, idx: number) => { - const block = oldBlocks[idx] - const [item, key, index] = getItem(source, idx) - if (block.key === getKey!(item, key, index)) { - update((newBlocks[idx] = block), item) - return true - } - } - const update = ( { itemRef, keyRef, indexRef }: ForBlock, newItem: any, @@ -353,9 +355,18 @@ export const createFor = ( } } - const unmount = ({ nodes, scope }: ForBlock, doRemove = true) => { - scope && scope.stop() - doRemove && removeBlock(nodes, parent!) + const unmount = (block: ForBlock, doRemove = true, doDeregister = true) => { + if (!isComponent) { + block.scope!.stop() + } + if (doRemove) { + removeBlock(block.nodes, parent!) + } + if (doDeregister) { + for (const selector of selectors) { + selector.deregister(block.key) + } + } } if (flags & VaporVForFlags.ONCE) { @@ -368,7 +379,61 @@ export const createFor = ( insert(frag, _insertionParent, _insertionAnchor) } + // @ts-expect-error + frag.useSelector = useSelector + return frag + + function useSelector( + getActiveKey: () => any, + ): (key: any, cb: () => void) => void { + let operMap = new Map void)[]>() + let activeKey = getActiveKey() + let activeOpers: (() => void)[] | undefined + + watch(getActiveKey, newValue => { + if (activeOpers !== undefined) { + for (const oper of activeOpers) { + oper() + } + } + activeOpers = operMap.get(newValue) + if (activeOpers !== undefined) { + for (const oper of activeOpers) { + oper() + } + } + }) + + selectors.push({ deregister, cleanup }) + return register + + function cleanup() { + operMap = new Map() + activeOpers = undefined + } + + function register(oper: () => void) { + oper() + let opers = operMap.get(currentKey) + if (opers !== undefined) { + opers.push(oper) + } else { + opers = [oper] + operMap.set(currentKey, opers) + if (currentKey === activeKey) { + activeOpers = opers + } + } + } + + function deregister(key: any) { + operMap.delete(key) + if (key === activeKey) { + activeOpers = undefined + } + } + } } export function createForSlots( diff --git a/rollup.config.js b/rollup.config.js index 7f2ecb8c864..1fa345f87fc 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -314,6 +314,7 @@ function createConfig(format, output, plugins = []) { const treeShakenDeps = [ 'source-map-js', '@babel/parser', + '@babel/types', 'estree-walker', 'entities/lib/decode.js', ]