]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(vapor): generate more efficient runtime code for specific interpolation patterns...
authorJohnson Chu <johnsoncodehk@gmail.com>
Thu, 10 Jul 2025 01:06:13 +0000 (09:06 +0800)
committerGitHub <noreply@github.com>
Thu, 10 Jul 2025 01:06:13 +0000 (18:06 -0700)
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
packages/compiler-vapor/src/generators/block.ts
packages/compiler-vapor/src/generators/expression.ts
packages/compiler-vapor/src/generators/for.ts
packages/compiler-vapor/src/generators/operation.ts
packages/runtime-vapor/__tests__/for.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
rollup.config.js

index e3631f158905bd5044d62611eee3f1a92e7981d7..141d3e410dccd93473750a80a1272f95fb9c7d59 100644 (file)
@@ -47,6 +47,21 @@ 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("<tr> </tr>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.rows), (_for_item0) => {
+    const n2 = t0()
+    const x2 = _child(n2)
+    _setText(x2, _toDisplayString(_for_item0.value.id + _for_item0.value.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("<div></div>", true)
@@ -130,6 +145,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("<tr> </tr>", 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("<tr></tr>", 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("<tr></tr>", 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("<tr></tr>", 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("<div> </div>", true)
index d22981c1e3008b851318d78040034b339b1b4269..7357ad84fefafef4805bfbeb57054f4724d9b6db 100644 (file)
@@ -67,6 +67,73 @@ describe('compiler: v-for', () => {
     ).lengthOf(1)
   })
 
+  test('key only binding pattern', () => {
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+          >
+            {{ row.id + row.id }}
+          </tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+  })
+
+  test('selector pattern', () => {
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+          >
+            {{ selected === row.id ? 'danger' : '' }}
+          </tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+            :class="selected === row.id ? 'danger' : ''"
+          ></tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+
+    // Should not be optimized because row.label is not from parent scope
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+            :class="row.label === row.id ? 'danger' : ''"
+          ></tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+
+    expect(
+      compileWithVFor(
+        `
+          <tr
+            v-for="row of rows"
+            :key="row.id"
+            :class="{ danger: row.id === selected }"
+          ></tr>
+      `,
+      ).code,
+    ).matchSnapshot()
+  })
+
   test('multi effect', () => {
     const { code } = compileWithVFor(
       `<div v-for="(item, index) of items" :item="item" :index="index" />`,
index ff240dd6eac05068f63d88df62be4e39d97ff13d..30347394756b90e7c63a385a91dc745597b43f4c 100644 (file)
@@ -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
@@ -70,7 +69,7 @@ export function genBlockContent(
   }
 
   push(...genOperations(operation, context))
-  push(...genEffects(effect, context))
+  push(...genEffects(effect, context, genEffectsExtraFrag))
 
   push(NEWLINE, `return `)
 
@@ -79,7 +78,7 @@ export function genBlockContent(
     returnNodes.length > 1
       ? genMulti(DELIMITERS_ARRAY, ...returnNodes)
       : [returnNodes[0] || 'null']
-  push(...(customReturns ? customReturns(returnsCode) : returnsCode))
+  push(...returnsCode)
 
   resetBlock()
   return frag
index a8fbc8f830071be6e588d49f27e75e09d3fb062d..aa7edf658f5228ae19b14051520423ef96599e1c 100644 (file)
@@ -233,6 +233,7 @@ function canPrefix(name: string) {
 type DeclarationResult = {
   ids: Record<string, string>
   frag: CodeFragment[]
+  varNames: string[]
 }
 type DeclarationValue = {
   name: string
@@ -246,6 +247,7 @@ type DeclarationValue = {
 export function processExpressions(
   context: CodegenContext,
   expressions: SimpleExpressionNode[],
+  shouldDeclare: boolean,
 ): DeclarationResult {
   // analyze variables
   const {
@@ -277,7 +279,11 @@ export function processExpressions(
     expToVariableMap,
   )
 
-  return genDeclarations([...varDeclarations, ...expDeclarations], context)
+  return genDeclarations(
+    [...varDeclarations, ...expDeclarations],
+    context,
+    shouldDeclare,
+  )
 }
 
 function analyzeExpressions(expressions: SimpleExpressionNode[]) {
@@ -592,15 +598,21 @@ function processRepeatedExpressions(
 function genDeclarations(
   declarations: DeclarationValue[],
   context: CodegenContext,
+  shouldDeclare: boolean,
 ): DeclarationResult {
   const [frag, push] = buildCodeFragment()
   const ids: Record<string, string> = Object.create(null)
+  const varNames = new Set<string>()
 
   // 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)
     }
   })
 
@@ -608,15 +620,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) {
index fbb72c61d476eda757c7ea4179b4efcac95a6e8f..40f002a8536870d411ea4218beb649ce71dc6ea8 100644 (file)
@@ -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<string, string | SimpleExpressionNode | null>,
+) {
+  const selectorPatterns: NonNullable<
+    ReturnType<typeof matchSelectorPattern>
+  >[] = []
+  const keyOnlyBindingPatterns: NonNullable<
+    ReturnType<typeof matchKeyOnlyBindingPattern>
+  >[] = []
+
+  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<string, string | SimpleExpressionNode | null>,
+):
+  | {
+      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<Identifier, Node[]>()
+        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<string, string | SimpleExpressionNode | null>,
+) {
+  let globals: string[] = []
+  let locals: string[] = []
+
+  const ids: Identifier[] = []
+  const parentStackMap = new Map<Identifier, Node[]>()
+  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
+}
index 563d72f1ee1c22c4ac0db8850e166631e3defc4a..13ce5477cc1705908d777c3d75ae18089a82c809 100644 (file)
@@ -98,15 +98,18 @@ export function genOperation(
 export function genEffects(
   effects: IREffect[],
   context: CodegenContext,
+  genExtraFrag?: () => CodeFragment[],
 ): CodeFragment[] {
   const { helper } = context
   const expressions = effects.flatMap(effect => effect.expressions)
   const [frag, push, unshift] = buildCodeFragment()
+  const shouldDeclare = genExtraFrag === undefined
   let operationsCount = 0
-  const { ids, frag: declarationFrags } = processExpressions(
-    context,
-    expressions,
-  )
+  const {
+    ids,
+    frag: declarationFrags,
+    varNames,
+  } = processExpressions(context, expressions, shouldDeclare)
   push(...declarationFrags)
   for (let i = 0; i < effects.length; i++) {
     const effect = effects[i]
@@ -123,6 +126,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) {
@@ -130,6 +136,14 @@ export function genEffects(
     push(`)`)
   }
 
+  if (!shouldDeclare && varNames.length) {
+    unshift(NEWLINE, `let `, varNames.join(', '))
+  }
+
+  if (genExtraFrag) {
+    push(...context.withId(genExtraFrag, ids))
+  }
+
   return frag
 }
 
index db91b6a62da9aa0cb7e172348d0b210b920faafa..dc247b6d4caa7158f9abfd47d018e6a82cd028e5 100644 (file)
@@ -101,7 +101,7 @@ describe('createFor', () => {
           })
           return span
         },
-        item => item.name,
+        item => item,
       )
       return n1
     }).render()
index 9b457f09b940f806128dbe91e056f3198c7d36a1..6ea0dfeb451b6ef4f642a7ed3444f0c572a7b71f 100644 (file)
@@ -9,6 +9,7 @@ import {
   shallowRef,
   toReactive,
   toReadonly,
+  watch,
 } from '@vue/reactivity'
 import { getSequence, isArray, isObject, isString } from '@vue/shared'
 import { createComment, createTextNode } from './dom/node'
@@ -86,12 +87,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()')
@@ -119,9 +126,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 = ''
@@ -361,9 +371,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) {
@@ -376,7 +395,59 @@ export const createFor = (
     insert(frag, _insertionParent, _insertionAnchor)
   }
 
+  // @ts-expect-error
+  frag.useSelector = useSelector
+
   return frag
+
+  function useSelector(source: () => any): (key: any, cb: () => void) => void {
+    let operMap = new Map<any, (() => void)[]>()
+    let activeKey = source()
+    let activeOpers: (() => void)[] | undefined
+
+    watch(source, 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(
index 7f2ecb8c8642b701152ec386a4a10204cd83e12b..1fa345f87fc926876c4f7787abde1a83e43db15b 100644 (file)
@@ -314,6 +314,7 @@ function createConfig(format, output, plugins = []) {
     const treeShakenDeps = [
       'source-map-js',
       '@babel/parser',
+      '@babel/types',
       'estree-walker',
       'entities/lib/decode.js',
     ]