]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: cache setProp prev value on element, simplify codegen
authorEvan You <evan@vuejs.org>
Wed, 11 Dec 2024 17:48:40 +0000 (01:48 +0800)
committerEvan You <evan@vuejs.org>
Thu, 12 Dec 2024 05:18:04 +0000 (13:18 +0800)
Also separate `setClass`/`setClassIncremental` and `setStyle`/
`setStyleIncremental`

23 files changed:
packages/compiler-vapor/__tests__/transforms/__snapshots__/vBind.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vBind.spec.ts
packages/compiler-vapor/__tests__/transforms/vOn.spec.ts
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/generators/expression.ts
packages/compiler-vapor/src/generators/html.ts
packages/compiler-vapor/src/generators/operation.ts
packages/compiler-vapor/src/generators/prop.ts
packages/compiler-vapor/src/generators/text.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transform.ts
packages/runtime-dom/src/index.ts
packages/runtime-dom/src/modules/props.ts
packages/runtime-dom/src/modules/style.ts
packages/runtime-dom/src/patchProp.ts
packages/runtime-vapor/__tests__/apiSetupContext.spec.ts
packages/runtime-vapor/__tests__/dom/prop.spec.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/dom/prop.ts
packages/runtime-vapor/src/index.ts
packages/shared/src/domAttrConfig.ts
packages/shared/src/normalizeProp.ts
scripts/trim-vapor-exports.js

index 5710d104e5d9868146a7861fae9aa14c6829c978..f536750ea2bed9aa421b66c57e724aa76f28d521 100644 (file)
@@ -330,6 +330,23 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler v-bind > MathML global attributes should set as attribute 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<math></math>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    _setAttr(n0, "autofucus", _ctx.autofucus)
+    _setAttr(n0, "dir", _ctx.dir)
+    _setAttr(n0, "displaystyle", _ctx.displaystyle)
+    _setAttr(n0, "mathcolor", _ctx.mathcolor)
+    _setAttr(n0, "tabindex", _ctx.tabindex)
+  })
+  return n0
+}"
+`;
+
 exports[`compiler v-bind > MathML global attributes should set as dom prop 1`] = `
 "import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<math></math>")
@@ -348,6 +365,21 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler v-bind > SVG global attributes should set as attribute 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<svg></svg>")
+
+export function render(_ctx) {
+  const n0 = t0()
+  _renderEffect(() => {
+    _setAttr(n0, "id", _ctx.id)
+    _setAttr(n0, "lang", _ctx.lang)
+    _setAttr(n0, "tabindex", _ctx.tabindex)
+  })
+  return n0
+}"
+`;
+
 exports[`compiler v-bind > SVG global attributes should set as dom prop 1`] = `
 "import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<svg></svg>")
index 87c7752aa3ce528855cb99a69e9b4d78bb4c0363..261b26a741be51b066b83bc4b5107594716450c5 100644 (file)
@@ -686,69 +686,6 @@ describe('compiler v-bind', () => {
     expect(code).contains(' _setAttr(n6, "width", _ctx.width)')
   })
 
-  test('HTML global attributes should set as dom prop', () => {
-    const { code } = compileWithVBind(`
-      <div :id="id" :title="title" :lang="lang" :dir="dir" :tabindex="tabindex" />
-    `)
-
-    expect(code).matchSnapshot()
-    expect(code).contains(
-      '_id !== _ctx.id && _setDOMProp(n0, "id", (_id = _ctx.id))',
-    )
-    expect(code).contains(
-      '_title !== _ctx.title && _setDOMProp(n0, "title", (_title = _ctx.title))',
-    )
-    expect(code).contains(
-      '_lang !== _ctx.lang && _setDOMProp(n0, "lang", (_lang = _ctx.lang))',
-    )
-    expect(code).contains(
-      '_dir !== _ctx.dir && _setDOMProp(n0, "dir", (_dir = _ctx.dir))',
-    )
-    expect(code).contains(
-      '_tabindex !== _ctx.tabindex && _setDOMProp(n0, "tabindex", (_tabindex = _ctx.tabindex))',
-    )
-  })
-
-  test('SVG global attributes should set as dom prop', () => {
-    const { code } = compileWithVBind(`
-      <svg :id="id" :lang="lang" :tabindex="tabindex" />
-    `)
-
-    expect(code).matchSnapshot()
-    expect(code).contains(
-      '_id !== _ctx.id && _setDOMProp(n0, "id", (_id = _ctx.id))',
-    )
-    expect(code).contains(
-      '_lang !== _ctx.lang && _setDOMProp(n0, "lang", (_lang = _ctx.lang))',
-    )
-    expect(code).contains(
-      '_tabindex !== _ctx.tabindex && _setDOMProp(n0, "tabindex", (_tabindex = _ctx.tabindex))',
-    )
-  })
-
-  test('MathML global attributes should set as dom prop', () => {
-    const { code } = compileWithVBind(`
-      <math :autofucus :dir :displaystyle :mathcolor :tabindex/>
-    `)
-
-    expect(code).matchSnapshot()
-    expect(code).contains(
-      '_autofucus !== _ctx.autofucus && _setDOMProp(n0, "autofucus", (_autofucus = _ctx.autofucus))',
-    )
-    expect(code).contains(
-      '_dir !== _ctx.dir && _setDOMProp(n0, "dir", (_dir = _ctx.dir))',
-    )
-    expect(code).contains(
-      '_displaystyle !== _ctx.displaystyle && _setDOMProp(n0, "displaystyle", (_displaystyle = _ctx.displaystyle))',
-    )
-    expect(code).contains(
-      '_mathcolor !== _ctx.mathcolor && _setDOMProp(n0, "mathcolor", (_mathcolor = _ctx.mathcolor))',
-    )
-    expect(code).contains(
-      '_tabindex !== _ctx.tabindex && _setDOMProp(n0, "tabindex", (_tabindex = _ctx.tabindex))',
-    )
-  })
-
   test(':innerHTML', () => {
     const { code } = compileWithVBind(`
       <div :innerHTML="foo"/>
index 0aac082d653b7290a19f2331f6c55fa44851dc30..3b744d0a7dabfecc3752f4d7258a5bcb8acece1c 100644 (file)
@@ -106,6 +106,8 @@ describe('v-on', () => {
       },
     })
 
+    console.log(code)
+
     expect(code).matchSnapshot()
   })
 
index bbcff0728c736eec98790dc4159e9d9a95d777a2..a134882351e2f3f95befce7e393cafca91babb3b 100644 (file)
@@ -2,13 +2,7 @@ import type {
   CodegenOptions as BaseCodegenOptions,
   BaseCodegenResult,
 } from '@vue/compiler-dom'
-import type {
-  BlockIRNode,
-  CoreHelper,
-  IREffect,
-  RootIRNode,
-  VaporHelper,
-} from './ir'
+import type { BlockIRNode, CoreHelper, RootIRNode, VaporHelper } from './ir'
 import { extend, remove } from '@vue/shared'
 import { genBlockContent } from './generators/block'
 import { genTemplates } from './generators/template'
@@ -37,15 +31,6 @@ export class CodegenContext {
 
   delegates: Set<string> = new Set<string>()
 
-  processingRenderEffect: IREffect | undefined = undefined
-  allRenderEffectSeenNames: Record<string, number> = Object.create(null)
-  shouldCacheRenderEffectDeps = (): boolean => {
-    // only need to generate effect deps when it's not nested in v-for
-    return !!(
-      this.processingRenderEffect && !this.processingRenderEffect.inVFor
-    )
-  }
-
   identifiers: Record<string, string[]> = Object.create(null)
 
   block: BlockIRNode
index c0f1c8212404e479ea3ac13f45f4db6fa0c059a8..69274aab56d6bcca8e74937ee3f21fba54baa404 100644 (file)
@@ -1,4 +1,4 @@
-import { isArray, isGloballyAllowed } from '@vue/shared'
+import { isGloballyAllowed } from '@vue/shared'
 import {
   BindingTypes,
   NewlineType,
@@ -95,14 +95,7 @@ export function genExpression(
         )
 
         if (i === ids.length - 1 && end < content.length) {
-          const rest = content.slice(end)
-          const last = frag[frag.length - 1]
-          if (hasMemberExpression && isArray(last)) {
-            // merge rest content into the last identifier's generated name
-            last[0] += rest
-          } else {
-            push([rest, NewlineType.Unknown])
-          }
+          push([content.slice(end), NewlineType.Unknown])
         }
       })
 
index 4eea9faa0b18ae5ed67566a7b5b517f4aa0c8b3c..72af699dd03d7bd04a927e725d0cfcc6639ebd06 100644 (file)
@@ -2,17 +2,15 @@ import type { CodegenContext } from '../generate'
 import type { SetHtmlIRNode } from '../ir'
 import { genExpression } from './expression'
 import { type CodeFragment, NEWLINE, genCall } from './utils'
-import { processValues } from './prop'
 
 export function genSetHtml(
   oper: SetHtmlIRNode,
   context: CodegenContext,
 ): CodeFragment[] {
-  const { helper, shouldCacheRenderEffectDeps } = context
+  const { helper } = context
   const { value, element } = oper
-  let html = genExpression(value, context)
-  if (shouldCacheRenderEffectDeps()) {
-    processValues(context, [html])
-  }
-  return [NEWLINE, ...genCall(helper('setHtml'), `n${element}`, html)]
+  return [
+    NEWLINE,
+    ...genCall(helper('setHtml'), `n${element}`, genExpression(value, context)),
+  ]
 }
index 6554b5e77769aeff89b8407db807ca4a49c47411..a82fc6e4d1b17cdcf73e10ede5d26a325b8d09ce 100644 (file)
@@ -78,15 +78,16 @@ export function genEffects(
 ): CodeFragment[] {
   const { helper } = context
   const [frag, push, unshift] = buildCodeFragment()
-  const declareNames = new Set<string>()
   let operationsCount = 0
   for (let i = 0; i < effects.length; i++) {
-    const effect = (context.processingRenderEffect = effects[i])
+    const effect = effects[i]
     operationsCount += effect.operations.length
-    const frags = genEffect(effect, context, declareNames)
-    const needSemi = frag[frag.length - 1] === ')' && frags[0] === '('
+    const frags = genEffect(effect, context)
     i > 0 && push(NEWLINE)
-    push(needSemi ? ';' : undefined, ...frags)
+    if (frag[frag.length - 1] === ')' && frags[0] === '(') {
+      push(';')
+    }
+    push(...frags)
   }
 
   const newLineCount = frag.filter(frag => frag === NEWLINE).length
@@ -100,60 +101,21 @@ export function genEffects(
     push(`)`)
   }
 
-  // declare variables: let _foo, _bar
-  if (declareNames.size) {
-    frag.splice(1, 0, `let ${[...declareNames].join(', ')}`, NEWLINE)
-  }
   return frag
 }
 
 export function genEffect(
   { operations }: IREffect,
   context: CodegenContext,
-  allDeclareNames: Set<string>,
 ): CodeFragment[] {
-  const { processingRenderEffect } = context
   const [frag, push] = buildCodeFragment()
-  const { declareNames, earlyCheckExps } = processingRenderEffect!
   const operationsExps = genOperations(operations, context)
-
-  if (declareNames.size) {
-    allDeclareNames.add([...declareNames].join(', '))
-  }
-
   const newlineCount = operationsExps.filter(frag => frag === NEWLINE).length
+
   if (newlineCount > 1) {
-    // multiline check expression: if (_foo !== _ctx.foo || _bar !== _ctx.bar) {
-    const checkExpsStart: CodeFragment[] =
-      earlyCheckExps.length > 0
-        ? [`if(`, ...earlyCheckExps.join(' || '), `) {`, INDENT_START]
-        : []
-    const checkExpsEnd: CodeFragment[] =
-      earlyCheckExps.length > 0 ? [INDENT_END, NEWLINE, '}'] : []
-    // assignment: _foo = _ctx.foo; _bar = _ctx.bar
-    const assignmentExps: CodeFragment[] =
-      earlyCheckExps.length > 0
-        ? [NEWLINE, ...earlyCheckExps.map(c => c.replace('!==', '=')).join(';')]
-        : []
-    push(
-      ...checkExpsStart,
-      ...operationsExps,
-      ...assignmentExps,
-      ...checkExpsEnd,
-    )
+    push(...operationsExps)
   } else {
-    // single line check expression: (_foo !== _ctx.foo || _bar !== _ctx.bar) &&
-    const multiple = earlyCheckExps.length > 1
-    const checkExps: CodeFragment[] =
-      earlyCheckExps.length > 0
-        ? [
-            multiple ? `(` : undefined,
-            ...earlyCheckExps.join(' || '),
-            multiple ? `)` : undefined,
-            ' && ',
-          ]
-        : []
-    push(...checkExps, ...operationsExps.filter(frag => frag !== NEWLINE))
+    push(...operationsExps.filter(frag => frag !== NEWLINE))
   }
 
   return frag
index fef5e09875715b04738638a2c2e47bba41277a32..b0137f97abc21b696fb1b38cc5e795058158fa37 100644 (file)
@@ -21,19 +21,31 @@ import {
   genMulti,
 } from './utils'
 import {
-  attributeCache,
   canSetValueDirectly,
-  isArray,
   isHTMLGlobalAttr,
-  isHTMLTag,
-  isMathMLGlobalAttr,
-  isMathMLTag,
-  isSVGTag,
-  isSvgGlobalAttr,
-  shouldSetAsAttr,
   toHandlerKey,
 } from '@vue/shared'
 
+export type HelperConfig = {
+  name: VaporHelper
+  needKey?: boolean
+  acceptRoot?: boolean
+}
+
+// this should be kept in sync with runtime-vapor/src/dom/prop.ts
+const helpers = {
+  setText: { name: 'setText' },
+  setHtml: { name: 'setHtml' },
+  setClass: { name: 'setClass' },
+  setClassIncremental: { name: 'setClassIncremental' },
+  setStyle: { name: 'setStyle' },
+  setStyleIncremental: { name: 'setStyleIncremental' },
+  setValue: { name: 'setValue' },
+  setAttr: { name: 'setAttr', needKey: true },
+  setDOMProp: { name: 'setDOMProp', needKey: true },
+  setDynamicProps: { name: 'setDynamicProps', acceptRoot: true },
+} as const satisfies Partial<Record<VaporHelper, HelperConfig>>
+
 // only the static key prop will reach here
 export function genSetProp(
   oper: SetPropIRNode,
@@ -43,31 +55,19 @@ export function genSetProp(
   const {
     prop: { key, values, modifier },
     tag,
+    root,
   } = oper
-  const { helperName, omitKey } = getRuntimeHelper(tag, key.content, modifier)
+  const resolvedHelper = getRuntimeHelper(tag, key.content, modifier, root)
   const propValue = genPropValue(values, context)
-  const { prevValueName, shouldWrapInParentheses } = processPropValues(
-    context,
-    helperName,
-    [propValue],
-  )
   return [
     NEWLINE,
-    ...(prevValueName
-      ? [shouldWrapInParentheses ? `(` : undefined, `${prevValueName} = `]
-      : []),
     ...genCall(
-      [helper(helperName), null],
+      [helper(resolvedHelper.name), null],
       `n${oper.element}`,
-      omitKey ? false : genExpression(key, context),
-      ...(prevValueName ? [`${prevValueName}`] : []),
+      resolvedHelper.needKey ? genExpression(key, context) : false,
       propValue,
-      // only `setClass` and `setStyle` need merge inherit attr
-      oper.root && (helperName === 'setClass' || helperName === 'setStyle')
-        ? 'true'
-        : undefined,
+      root && resolvedHelper.acceptRoot ? 'true' : undefined,
     ),
-    ...(prevValueName && shouldWrapInParentheses ? [`)`] : []),
   ]
 }
 
@@ -84,24 +84,14 @@ export function genDynamicProps(
         ? genLiteralObjectProps([props], context) // dynamic arg props
         : genExpression(props.value, context),
   ) // v-bind=""
-  const { prevValueName, shouldWrapInParentheses } = processPropValues(
-    context,
-    'setDynamicProps',
-    values,
-  )
   return [
     NEWLINE,
-    ...(prevValueName
-      ? [shouldWrapInParentheses ? `(` : undefined, `${prevValueName} = `]
-      : []),
     ...genCall(
       helper('setDynamicProps'),
       `n${oper.element}`,
-      ...(prevValueName ? [`${prevValueName}`] : []),
       genMulti(DELIMITERS_ARRAY, ...values),
       oper.root && 'true',
     ),
-    ...(prevValueName && shouldWrapInParentheses ? [`)`] : []),
   ]
 }
 
@@ -161,161 +151,75 @@ export function genPropValue(
   )
 }
 
+// TODO
+// - 1. textContent + innerHTML Known base dom properties in https://developer.mozilla.org/en-US/docs/Web/API/Element
+// - 2. special handling (class / style)
+// - 3. SVG: always attribute
+// - 4. Custom Elements
+//     - always properties unless known global attr or has hyphen (aria- / data-)
+// - 5. Normal Elements
+//   - 1. Known shared dom properties:
+//     - https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
+//   - 2. Each element's known dom properties
+//   - 3. Fallback to attribute
+
 function getRuntimeHelper(
   tag: string,
   keyName: string,
   modifier: '.' | '^' | undefined,
-) {
+  root: boolean,
+): HelperConfig {
   const tagName = tag.toUpperCase()
-  let helperName: VaporHelper
-  let omitKey = false
-
   if (modifier) {
     if (modifier === '.') {
-      const helper = getSpecialHelper(keyName, tagName)
-      if (helper) {
-        helperName = helper.name
-        omitKey = helper.omitKey
-      } else {
-        helperName = 'setDOMProp'
-        omitKey = false
-      }
+      return getSpecialHelper(keyName, tagName, root) || helpers.setDOMProp
     } else {
-      helperName = 'setAttr'
+      return helpers.setAttr
     }
   } else {
-    const attrCacheKey = `${tagName}_${keyName}`
-    const helper = getSpecialHelper(keyName, tagName)
+    const helper = getSpecialHelper(keyName, tagName, root)
     if (helper) {
-      helperName = helper.name
-      omitKey = helper.omitKey
-    } else if (
-      attributeCache[attrCacheKey] === undefined
-        ? (attributeCache[attrCacheKey] = shouldSetAsAttr(tagName, keyName))
-        : attributeCache[attrCacheKey]
-    ) {
-      helperName = 'setAttr'
-    } else if (
-      (isHTMLTag(tag) && isHTMLGlobalAttr(keyName)) ||
-      (isSVGTag(tag) && isSvgGlobalAttr(keyName)) ||
-      (isMathMLTag(tag) && isMathMLGlobalAttr(keyName))
-    ) {
-      helperName = 'setDOMProp'
+      return helper
+    } else if (tagName.includes('-')) {
+      // custom element
+      if (isHTMLGlobalAttr(keyName) || keyName.includes('-')) {
+        return helpers.setAttr
+      } else {
+        return helpers.setDOMProp
+      }
+    } else if (/[A-Z]/.test(keyName)) {
+      return helpers.setDOMProp
     } else {
-      helperName = 'setDynamicProp'
+      return helpers.setAttr
     }
   }
-  return { helperName, omitKey }
 }
 
-const specialHelpers: Record<string, { name: VaporHelper; omitKey: boolean }> =
-  {
-    class: { name: 'setClass', omitKey: true },
-    style: { name: 'setStyle', omitKey: true },
-    innerHTML: { name: 'setHtml', omitKey: true },
-    textContent: { name: 'setText', omitKey: true },
-  }
-
 const getSpecialHelper = (
   keyName: string,
   tagName: string,
-): { name: VaporHelper; omitKey: boolean } | null => {
+  root: boolean,
+): HelperConfig | undefined => {
   // special case for 'value' property
   if (keyName === 'value' && canSetValueDirectly(tagName)) {
-    return { name: 'setValue', omitKey: true }
-  }
-
-  return specialHelpers[keyName] || null
-}
-
-// those runtime helpers will return the prevValue
-const helpersNeedCachedReturnValue = [
-  'setStyle',
-  'setDynamicProp',
-  'setDynamicProps',
-]
-
-function processPropValues(
-  context: CodegenContext,
-  helperName: string,
-  values: CodeFragment[][],
-): { prevValueName: string | undefined; shouldWrapInParentheses: boolean } {
-  const { shouldCacheRenderEffectDeps, processingRenderEffect } = context
-  // single-line render effect and the operation needs cache return a value,
-  // the expression needs to be wrapped in parentheses.
-  // e.g. _foo === _ctx.foo && (_foo = _setStyle(...))
-  let shouldWrapInParentheses: boolean = false
-  let prevValueName
-  if (shouldCacheRenderEffectDeps()) {
-    const needReturnValue = helpersNeedCachedReturnValue.includes(helperName)
-    processValues(context, values, !needReturnValue)
-    const { declareNames } = processingRenderEffect!
-    // if the operation needs to cache the return value and has multiple declareNames,
-    // combine them into a single name as the return value name.
-    if (declareNames.size > 0 && needReturnValue) {
-      prevValueName = [...declareNames].join('')
-      declareNames.add(prevValueName)
-    }
-    shouldWrapInParentheses = processingRenderEffect!.operations.length === 1
+    return helpers.setValue
   }
-  return { prevValueName, shouldWrapInParentheses }
-}
-
-export function processValues(
-  context: CodegenContext,
-  values: CodeFragment[][],
-  needRewrite: boolean = true,
-): string[] {
-  const allCheckExps: string[] = []
-  values.forEach(value => {
-    const checkExps = processValue(context, value, needRewrite)
-    if (checkExps) allCheckExps.push(...checkExps, ' && ')
-  })
-
-  return allCheckExps.length > 0
-    ? (context.processingRenderEffect!.earlyCheckExps = [
-        ...new Set(allCheckExps),
-      ])
-    : []
-}
-
-function processValue(
-  context: CodegenContext,
-  values: CodeFragment[],
-  needRewrite: boolean = true,
-): string[] | undefined {
-  const { processingRenderEffect, allRenderEffectSeenNames } = context
-  const { declareNames, rewrittenNames, earlyCheckExps, operations } =
-    processingRenderEffect!
-
-  const isSingleLine = operations.length === 1
-  for (const frag of values) {
-    if (!isArray(frag)) continue
-    // [code, newlineIndex, loc, name] -> [(_name = code), newlineIndex, loc, name]
-    const [newName, , , rawName] = frag
-    if (rawName) {
-      let name = rawName.replace(/[^\w]/g, '_')
-      if (rewrittenNames.has(name)) continue
-      rewrittenNames.add(name)
 
-      name = `_${name}`
-      if (declareNames.has(name)) continue
-
-      if (allRenderEffectSeenNames[name] === undefined)
-        allRenderEffectSeenNames[name] = 0
-      else name += ++allRenderEffectSeenNames[name]
-
-      declareNames.add(name)
-      earlyCheckExps.push(`${name} !== ${newName}`)
-
-      if (needRewrite && isSingleLine) {
-        // replace the original code fragment with the assignment expression
-        frag[0] = `(${name} = ${newName})`
-      }
+  if (root) {
+    if (keyName === 'class') {
+      return helpers.setClassIncremental
+    } else if (keyName === 'style') {
+      return helpers.setStyleIncremental
     }
   }
 
-  if (earlyCheckExps.length > 0) {
-    return [[...new Set(earlyCheckExps)].join(' && ')]
+  if (keyName === 'class') {
+    return helpers.setClass
+  } else if (keyName === 'style') {
+    return helpers.setStyle
+  } else if (keyName === 'innerHTML') {
+    return helpers.setHtml
+  } else if (keyName === 'textContent') {
+    return helpers.setText
   }
 }
index 30ace31a7bc963e2dc9e8b943ed4fd2265fdcf0c..8d8b849c80ac7df5659787bcfccc1f1656543c82 100644 (file)
@@ -8,18 +8,14 @@ import {
   genCall,
   genMulti,
 } from './utils'
-import { processValues } from './prop'
 
 export function genSetText(
   oper: SetTextIRNode,
   context: CodegenContext,
 ): CodeFragment[] {
-  const { helper, shouldCacheRenderEffectDeps } = context
+  const { helper } = context
   const { element, values } = oper
   const texts = values.map(value => genExpression(value, context))
-  if (shouldCacheRenderEffectDeps()) {
-    processValues(context, texts)
-  }
   return [NEWLINE, ...genCall(helper('setText'), `n${element}`, ...texts)]
 }
 
index 7dabd0dc2e0727902beb6966b57990e720a19834..f1ca98325e4e7ef9e355375cdfe7b4658b67b36b 100644 (file)
@@ -260,12 +260,7 @@ export interface IRDynamicInfo {
 
 export interface IREffect {
   expressions: SimpleExpressionNode[]
-  identifiers: string[]
   operations: OperationNode[]
-  declareNames: Set<string>
-  rewrittenNames: Set<string>
-  earlyCheckExps: string[]
-  inVFor: boolean
 }
 
 type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> &
index 0b4f71f8d7e1c671f9c72bada0f56197b287f627..69e681745de363f6cd52bcfd98d5694430374821 100644 (file)
@@ -5,7 +5,6 @@ import {
   type CompilerCompatOptions,
   type ElementNode,
   ElementTypes,
-  type ExpressionNode,
   NodeTypes,
   type RootNode,
   type SimpleExpressionNode,
@@ -13,16 +12,8 @@ import {
   defaultOnError,
   defaultOnWarn,
   isVSlot,
-  walkIdentifiers,
 } from '@vue/compiler-dom'
-import {
-  EMPTY_OBJ,
-  NOOP,
-  extend,
-  isArray,
-  isString,
-  looseEqual,
-} from '@vue/shared'
+import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
 import {
   type BlockIRNode,
   DynamicFlag,
@@ -151,10 +142,8 @@ export class TransformContext<T extends AllNode = AllNode> {
     if (this.inVOnce || expressions.length === 0) {
       return this.registerOperation(...operations)
     }
-    const ids = new Set<string>()
-    expressions.forEach(exp => extractIdentifiers(ids, exp))
     const existing = this.block.effect.find(e =>
-      looseEqual(e.identifiers, Array.from(ids)),
+      isSameExpression(e.expressions, expressions),
     )
     if (existing) {
       existing.operations.push(...operations)
@@ -162,14 +151,18 @@ export class TransformContext<T extends AllNode = AllNode> {
       this.block.effect.push({
         expressions,
         operations,
-        earlyCheckExps: [],
-        declareNames: new Set<string>(),
-        rewrittenNames: new Set<string>(),
-        inVFor: this.inVFor > 0,
-        identifiers: Array.from(ids),
       })
     }
+
+    function isSameExpression(
+      a: SimpleExpressionNode[],
+      b: SimpleExpressionNode[],
+    ) {
+      if (a.length !== b.length) return false
+      return a.every((exp, i) => exp.content === b[i].content)
+    }
   }
+
   registerOperation(...node: OperationNode[]): void {
     this.block.operation.push(...node)
   }
@@ -304,11 +297,3 @@ export function createStructuralDirectiveTransform(
     }
   }
 }
-
-function extractIdentifiers(ids: Set<string>, node: ExpressionNode) {
-  if (node.ast) {
-    walkIdentifiers(node.ast, n => ids.add(n.name), true)
-  } else if (node.ast === null) {
-    ids.add((node as SimpleExpressionNode).content)
-  }
-}
index 4f59f1f16aed3a697022df6961275e63473c167e..60c32ce5d309b600f1e8fc582ffb9f086a45a0d3 100644 (file)
@@ -311,3 +311,14 @@ export const initDirectivesForSSR: () => void = __SSR__
 export * from '@vue/runtime-core'
 
 export * from './jsx'
+
+// VAPOR -----------------------------------------------------------------------
+
+/**
+ * @internal
+ */
+export { patchStyle } from './modules/style'
+/**
+ * @internal
+ */
+export { shouldSetAsProp } from './patchProp'
index 98608831a9a650a388dc994006269d254cacdb8c..a28290acf4a3b88fe9b06bbe160e9a39659ac00c 100644 (file)
@@ -1,5 +1,5 @@
 import { DeprecationTypes, compatUtils, warn } from '@vue/runtime-core'
-import { includeBooleanAttr } from '@vue/shared'
+import { canSetValueDirectly, includeBooleanAttr } from '@vue/shared'
 import { unsafeToTrustedHTML } from '../nodeOps'
 
 // functions. The user is responsible for using them with only trusted content.
@@ -24,12 +24,7 @@ export function patchDOMProp(
 
   const tag = el.tagName
 
-  if (
-    key === 'value' &&
-    tag !== 'PROGRESS' &&
-    // custom elements may use _value internally
-    !tag.includes('-')
-  ) {
+  if (key === 'value' && canSetValueDirectly(tag)) {
     // #4956: <option> value will fallback to its text content so we need to
     // compare against its attribute value instead.
     const oldValue =
index 383628a6ad0492d926d536fe9006a14dc9d9e717..ac679e0e33139c3968c25052909b6149140753d4 100644 (file)
@@ -7,7 +7,7 @@ import {
 } from '../directives/vShow'
 import { CSS_VAR_TEXT } from '../helpers/useCssVars'
 
-type Style = string | Record<string, string | string[]> | null
+type Style = string | Record<string, string | string[]> | null | undefined
 
 const displayRE = /(^|;)\s*display\s*:/
 
index ef2108d7b1dd4af654240a7f5ab79070f27333a5..dfa5974ebe94b7397f0753063521445cccb21689 100644 (file)
@@ -10,6 +10,7 @@ import {
   isNativeOn,
   isOn,
   isString,
+  shouldSetAsAttr,
 } from '@vue/shared'
 import type { RendererOptions } from '@vue/runtime-core'
 import type { VueElement } from './apiCustomElement'
@@ -71,12 +72,12 @@ export const patchProp: DOMRendererOptions['patchProp'] = (
   }
 }
 
-function shouldSetAsProp(
+export function shouldSetAsProp(
   el: Element,
   key: string,
   value: unknown,
   isSVG: boolean,
-) {
+): boolean {
   if (isSVG) {
     // most keys must be set as attribute on svg elements to work
     // ...except innerHTML & textContent
@@ -90,45 +91,10 @@ function shouldSetAsProp(
     return false
   }
 
-  // these are enumerated attrs, however their corresponding DOM properties
-  // are actually booleans - this leads to setting it with a string "false"
-  // value leading it to be coerced to `true`, so we need to always treat
-  // them as attributes.
-  // Note that `contentEditable` doesn't have this problem: its DOM
-  // property is also enumerated string values.
-  if (key === 'spellcheck' || key === 'draggable' || key === 'translate') {
+  if (shouldSetAsAttr(el.tagName, key)) {
     return false
   }
 
-  // #1787, #2840 form property on form elements is readonly and must be set as
-  // attribute.
-  if (key === 'form') {
-    return false
-  }
-
-  // #1526 <input list> must be set as attribute
-  if (key === 'list' && el.tagName === 'INPUT') {
-    return false
-  }
-
-  // #2766 <textarea type> must be set as attribute
-  if (key === 'type' && el.tagName === 'TEXTAREA') {
-    return false
-  }
-
-  // #8780 the width or height of embedded tags must be set as attribute
-  if (key === 'width' || key === 'height') {
-    const tag = el.tagName
-    if (
-      tag === 'IMG' ||
-      tag === 'VIDEO' ||
-      tag === 'CANVAS' ||
-      tag === 'SOURCE'
-    ) {
-      return false
-    }
-  }
-
   // native onclick with string value, must be set as attribute
   if (isNativeOn(key) && isString(value)) {
     return false
index 266d1cfa2e88f6332d89946daa85d8b56b38f640..7eb8a0cf0ea25c7c3b447c98e3f0236dd102ee3e 100644 (file)
@@ -74,8 +74,7 @@ describe('api: setup context', () => {
       inheritAttrs: false,
       setup(props, { attrs }) {
         const el = document.createElement('div')
-        let prev: any
-        renderEffect(() => (prev = setDynamicProps(el, prev, [attrs])))
+        renderEffect(() => setDynamicProps(el, [attrs]))
         return el
       },
     })
@@ -111,10 +110,7 @@ describe('api: setup context', () => {
         const n0 = createComponent(Wrapper, null, {
           default: () => {
             const n0 = template('<div>')() as HTMLDivElement
-            let prev: any
-            renderEffect(
-              () => (prev = setDynamicProps(n0, prev, [attrs], true)),
-            )
+            renderEffect(() => setDynamicProps(n0, [attrs], true))
             return n0
           },
         })
index d0fa57c4ea6612289bbcfc7795bd07b4f3da7980..95569f7ad1fc5c28fe6ee3056ddaa45ba458ad6f 100644 (file)
@@ -241,8 +241,9 @@ describe('patchProp', () => {
   describe('setDOMProp', () => {
     test('should be boolean prop', () => {
       const el = document.createElement('select')
-      setDOMProp(el, 'multiple', '')
-      expect(el.multiple).toBe(true)
+      // In vapor static attrs are part of the template and this never happens
+      // setDOMProp(el, 'multiple', '')
+      // expect(el.multiple).toBe(true)
       setDOMProp(el, 'multiple', null)
       expect(el.multiple).toBe(false)
       setDOMProp(el, 'multiple', true)
@@ -261,33 +262,29 @@ describe('patchProp', () => {
 
     test('should remove attribute when value is falsy', () => {
       const el = document.createElement('div')
-      setDOMProp(el, 'id', '')
-      expect(el.hasAttribute('id')).toBe(true)
+      el.setAttribute('id', '')
       setDOMProp(el, 'id', null)
       expect(el.hasAttribute('id')).toBe(false)
 
-      setDOMProp(el, 'id', '')
-      expect(el.hasAttribute('id')).toBe(true)
+      el.setAttribute('id', '')
       setDOMProp(el, 'id', undefined)
       expect(el.hasAttribute('id')).toBe(false)
 
       setDOMProp(el, 'id', '')
-      expect(el.hasAttribute('id')).toBe(true)
+      expect(el.hasAttribute('id')).toBe(false)
 
       const img = document.createElement('img')
-      setDOMProp(img, 'width', '')
-      expect(img.hasAttribute('width')).toBe(false)
       setDOMProp(img, 'width', 0)
-      expect(img.hasAttribute('width')).toBe(true)
+      expect(img.hasAttribute('width')).toBe(false) // skipped
 
       setDOMProp(img, 'width', null)
       expect(img.hasAttribute('width')).toBe(false)
-      setDOMProp(img, 'width', 0)
+      setDOMProp(img, 'width', 1)
       expect(img.hasAttribute('width')).toBe(true)
 
       setDOMProp(img, 'width', undefined)
       expect(img.hasAttribute('width')).toBe(false)
-      setDOMProp(img, 'width', 0)
+      setDOMProp(img, 'width', 1)
       expect(img.hasAttribute('width')).toBe(true)
     })
 
@@ -371,25 +368,24 @@ describe('patchProp', () => {
   describe('setDynamicProps', () => {
     test('basic set dynamic props', () => {
       const el = document.createElement('div')
-      setDynamicProps(el, null, [{ foo: 'val' }, { bar: 'val' }])
+      setDynamicProps(el, [{ foo: 'val' }, { bar: 'val' }])
       expect(el.getAttribute('foo')).toBe('val')
       expect(el.getAttribute('bar')).toBe('val')
     })
 
     test('should merge props', () => {
       const el = document.createElement('div')
-      setDynamicProps(el, null, [{ foo: 'val' }, { foo: 'newVal' }])
+      setDynamicProps(el, [{ foo: 'val' }, { foo: 'newVal' }])
       expect(el.getAttribute('foo')).toBe('newVal')
     })
 
     test('should reset old props', () => {
       const el = document.createElement('div')
-      let prev: any
-      prev = setDynamicProps(el, prev, [{ foo: 'val' }])
+      setDynamicProps(el, [{ foo: 'val' }])
       expect(el.attributes.length).toBe(1)
       expect(el.getAttribute('foo')).toBe('val')
 
-      prev = setDynamicProps(el, prev, [{ bar: 'val' }])
+      setDynamicProps(el, [{ bar: 'val' }])
       expect(el.attributes.length).toBe(1)
       expect(el.getAttribute('bar')).toBe('val')
       expect(el.getAttribute('foo')).toBeNull()
@@ -398,19 +394,18 @@ describe('patchProp', () => {
     test('should reset old modifier props', () => {
       const el = document.createElement('div')
 
-      let prev: any
-      prev = setDynamicProps(el, prev, [{ ['.foo']: 'val' }])
+      setDynamicProps(el, [{ ['.foo']: 'val' }])
       expect((el as any).foo).toBe('val')
 
-      prev = setDynamicProps(el, prev, [{ ['.bar']: 'val' }])
+      setDynamicProps(el, [{ ['.bar']: 'val' }])
       expect((el as any).bar).toBe('val')
       expect((el as any).foo).toBe('')
 
-      prev = setDynamicProps(el, prev, [{ ['^foo']: 'val' }])
+      setDynamicProps(el, [{ ['^foo']: 'val' }])
       expect(el.attributes.length).toBe(1)
       expect(el.getAttribute('foo')).toBe('val')
 
-      prev = setDynamicProps(el, prev, [{ ['^bar']: 'val' }])
+      setDynamicProps(el, [{ ['^bar']: 'val' }])
       expect(el.attributes.length).toBe(1)
       expect(el.getAttribute('bar')).toBe('val')
       expect(el.getAttribute('foo')).toBeNull()
index 612916394ab94e8c0c09469b2c29b7b869431d94..069bb88b68415bdca8fed4b0babbd2b2959d4293 100644 (file)
@@ -32,13 +32,7 @@ import {
   proxyRefs,
   resetTracking,
 } from '@vue/reactivity'
-import {
-  EMPTY_OBJ,
-  extend,
-  invokeArrayFns,
-  isFunction,
-  isString,
-} from '@vue/shared'
+import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
 import {
   type DynamicPropsSource,
   type RawProps,
@@ -50,8 +44,7 @@ import {
 } from './componentProps'
 import { renderEffect } from './renderEffect'
 import { emit, normalizeEmitsOptions } from './componentEmits'
-import { setStyle } from './dom/style'
-import { setClass, setDynamicProp, setDynamicProps } from './dom/prop'
+import { setDynamicProps } from './dom/prop'
 import {
   type DynamicSlotSource,
   type RawSlots,
@@ -215,11 +208,8 @@ export function createComponent(
     instance.block instanceof Element &&
     Object.keys(instance.attrs).length
   ) {
-    let prevProps: any
     renderEffect(() => {
-      setDynamicProps(instance.block as Element, prevProps, [
-        (prevProps = extend({}, instance.attrs)),
-      ])
+      setDynamicProps(instance.block as Element, [instance.attrs])
     })
   }
 
@@ -429,23 +419,8 @@ export function createComponentWithFallback(
   const el = document.createElement(comp)
 
   if (rawProps) {
-    let prevProps: any, prevStyle: any
     renderEffect(() => {
-      let classes: unknown[] | undefined
-      let styles: unknown[] | undefined
-      const resolved = resolveDynamicProps(rawProps)
-      for (const key in resolved) {
-        const value = resolved[key]
-        if (key === 'class') {
-          ;(classes ||= []).push(value)
-        } else if (key === 'style') {
-          ;(styles ||= []).push(value)
-        } else if (value !== prevProps) {
-          setDynamicProp(el, key, prevProps, (prevProps = value))
-        }
-      }
-      if (classes) setClass(el, classes, isSingleRoot)
-      if (styles) setStyle(el, prevStyle, (prevStyle = styles), isSingleRoot)
+      setDynamicProps(el, [resolveDynamicProps(rawProps)])
     })
   }
 
index 192d14e393e62e71ff288107d5cd3e74cd1d61bb..652e1db36eb8e72df32c09f964f60b506cc2dfeb 100644 (file)
 import {
-  attributeCache,
+  type NormalizedStyle,
   canSetValueDirectly,
-  includeBooleanAttr,
-  isArray,
-  isFunction,
-  isNativeOn,
   isOn,
   isString,
   normalizeClass,
   normalizeStyle,
-  shouldSetAsAttr,
+  parseStringStyle,
   toDisplayString,
 } from '@vue/shared'
-import { setStyle } from './style'
 import { on } from './event'
-import { currentInstance } from '../component'
-import { warn } from '@vue/runtime-dom'
+import {
+  mergeProps,
+  patchStyle as setStyle,
+  shouldSetAsProp,
+  warn,
+} from '@vue/runtime-dom'
+
+type TargetElement = Element & {
+  $html?: string
+  $cls?: string
+  $clsi?: string
+  $sty?: NormalizedStyle
+  $styi?: NormalizedStyle
+  $dprops?: Record<string, any>
+}
 
-export function mergeInheritAttr(key: string, value: any): unknown {
-  const instance = currentInstance!
-  return mergeProp(key, instance.attrs[key], value)
+export function setText(el: Node & { $txt?: string }, ...values: any[]): void {
+  const value = values.map(v => toDisplayString(v)).join('')
+  if (el.$txt !== value) {
+    el.textContent = el.$txt = value
+  }
 }
 
-export function setClass(el: Element, value: any, root?: boolean): void {
-  el.className = normalizeClass(root ? mergeInheritAttr('class', value) : value)
+export function setHtml(el: TargetElement, value: any): void {
+  value = value == null ? '' : value
+  if (el.$html !== value) {
+    el.innerHTML = el.$html = value
+  }
 }
 
-export function setAttr(el: Element, key: string, value: any): void {
-  if (value != null) {
-    el.setAttribute(key, value)
-  } else {
-    el.removeAttribute(key)
+export function setClass(el: TargetElement, value: any): void {
+  if ((value = normalizeClass(value)) !== el.$cls) {
+    el.className = el.$cls = value
+  }
+}
+
+/**
+ * A version of setClass that does not overwrite pre-existing classes.
+ * Used on single root elements so it can patch class independent of fallthrough
+ * attributes.
+ */
+export function setClassIncremental(el: TargetElement, value: any): void {
+  const prev = el.$clsi
+  if ((value = normalizeClass(value)) !== prev) {
+    el.$clsi = value
+    const nextList = value.split(/\s+/)
+    el.classList.add(...nextList)
+    if (prev) {
+      for (const cls of prev.split(/\s+/)) {
+        if (!nextList.includes(cls)) el.classList.remove(cls)
+      }
+    }
+  }
+}
+
+/**
+ * Reuse from runtime-dom
+ */
+export { setStyle }
+
+/**
+ * A version of setStyle that does not overwrite pre-existing styles.
+ * Used on single root elements so it can patch class independent of fallthrough
+ * attributes.
+ */
+export function setStyleIncremental(el: TargetElement, value: any): void {
+  const prev = el.$styi
+  value = el.$styi = isString(value)
+    ? parseStringStyle(value)
+    : ((normalizeStyle(value) || {}) as NormalizedStyle)
+  setStyle(el, prev, value)
+}
+
+export function setAttr(el: any, key: string, value: any): void {
+  if (value !== el[`$${key}`]) {
+    el[`$${key}`] = value
+    if (value != null) {
+      el.setAttribute(key, value)
+    } else {
+      el.removeAttribute(key)
+    }
   }
 }
 
-export function setValue(el: any, value: any): void {
+export function setValue(
+  el: Element & { value?: string; _value?: any },
+  value: any,
+): void {
   // store value as _value as well since
   // non-string values will be stringified.
   el._value = value
@@ -51,13 +113,15 @@ export function setValue(el: any, value: any): void {
 }
 
 export function setDOMProp(el: any, key: string, value: any): void {
+  const prev = el[key]
+  if (value === prev) {
+    return
+  }
+
   let needRemove = false
   if (value === '' || value == null) {
-    const type = typeof el[key]
-    if (type === 'boolean') {
-      // e.g. <select multiple> compiles to { multiple: '' }
-      value = includeBooleanAttr(value)
-    } else if (value == null && type === 'string') {
+    const type = typeof prev
+    if (value == null && type === 'string') {
       // e.g. <div :id="null">
       value = ''
       needRemove = true
@@ -86,60 +150,13 @@ export function setDOMProp(el: any, key: string, value: any): void {
   needRemove && el.removeAttribute(key)
 }
 
-export function setDynamicProp(
-  el: Element,
-  key: string,
-  prev: any,
-  value: any,
-): any {
-  // TODO
-  const isSVG = false
-  if (key === 'class') {
-    setClass(el, value)
-  } else if (key === 'style') {
-    return setStyle(el as HTMLElement, prev, value)
-  } else if (isOn(key)) {
-    on(el, key[2].toLowerCase() + key.slice(3), () => value, { effect: true })
-  } else if (
-    key[0] === '.'
-      ? ((key = key.slice(1)), true)
-      : key[0] === '^'
-        ? ((key = key.slice(1)), false)
-        : shouldSetAsProp(el, key, value, isSVG)
-  ) {
-    if (key === 'innerHTML') {
-      setHtml(el, value)
-      return
-    }
-
-    if (key === 'textContent') {
-      setText(el, value)
-      return
-    }
-
-    const tag = el.tagName
-    if (key === 'value' && canSetValueDirectly(tag)) {
-      setValue(el, value)
-      return
-    }
-
-    setDOMProp(el, key, value)
-  } else {
-    // TODO special case for <input v-model type="checkbox">
-    setAttr(el, key, value)
-  }
-}
-
 export function setDynamicProps(
-  el: Element,
-  oldProps: any,
+  el: TargetElement,
   args: any[],
-  root?: boolean,
+  root = false,
 ): void {
-  if (root) {
-    args.unshift(currentInstance!.attrs)
-  }
   const props = args.length > 1 ? mergeProps(...args) : args[0]
+  const oldProps = el.$dprops
 
   if (oldProps) {
     for (const key in oldProps) {
@@ -151,103 +168,67 @@ export function setDynamicProps(
       const oldValue = oldProps[key]
       const hasNewValue = props[key] || props['.' + key] || props['^' + key]
       if (oldValue && !hasNewValue) {
-        setDynamicProp(el, key, oldValue, null)
+        setDynamicProp(el, key, oldValue, null, root)
       }
     }
   }
 
-  const prev = Object.create(null)
+  const prev = (el.$dprops = Object.create(null))
   for (const key in props) {
     setDynamicProp(
       el,
       key,
       oldProps ? oldProps[key] : undefined,
       (prev[key] = props[key]),
+      root,
     )
   }
-
-  return prev
 }
 
-export function mergeProp(
+/**
+ * @internal
+ */
+export function setDynamicProp(
+  el: TargetElement,
   key: string,
-  existing: unknown,
-  incoming: unknown,
-): unknown {
+  prev: any,
+  value: any,
+  root?: boolean,
+): void {
+  // TODO
+  const isSVG = false
   if (key === 'class') {
-    if (existing !== incoming) {
-      return normalizeClass([existing, incoming])
+    if (root) {
+      setClassIncremental(el, value)
+    } else {
+      setClass(el, value)
     }
   } else if (key === 'style') {
-    return normalizeStyle([existing, incoming])
-  } else if (isOn(key)) {
-    if (
-      incoming &&
-      existing !== incoming &&
-      !(isArray(existing) && existing.includes(incoming))
-    ) {
-      return existing ? [].concat(existing as any, incoming as any) : incoming
-    }
-  }
-  return incoming
-}
-
-type Data = Record<string, any>
-
-export function mergeProps(...args: Data[]): Data {
-  const ret: Data = {}
-  for (let i = 0; i < args.length; i++) {
-    const toMerge = args[i]
-    for (const key in toMerge) {
-      if (key !== '') {
-        ret[key] = mergeProp(key, ret[key], toMerge[key])
-      }
-    }
-  }
-  return ret
-}
-
-export function setText(el: Node, ...values: any[]): void {
-  el.textContent = values.map(v => toDisplayString(v)).join('')
-}
-
-export function setHtml(el: Element, value: any): void {
-  el.innerHTML = value == null ? '' : value
-}
-
-// TODO copied from runtime-dom
-function shouldSetAsProp(
-  el: Element,
-  key: string,
-  value: unknown,
-  isSVG: boolean,
-) {
-  if (isSVG) {
-    // most keys must be set as attribute on svg elements to work
-    // ...except innerHTML & textContent
-    if (key === 'innerHTML' || key === 'textContent') {
-      return true
-    }
-    // or native onclick with function values
-    if (key in el && isNativeOn(key) && isFunction(value)) {
-      return true
+    if (root) {
+      setStyleIncremental(el, value)
+    } else {
+      setStyle(el, prev, value)
     }
-    return false
-  }
-
-  const attrCacheKey = `${el.tagName}_${key}`
-  if (
-    attributeCache[attrCacheKey] === undefined
-      ? (attributeCache[attrCacheKey] = shouldSetAsAttr(el.tagName, key))
-      : attributeCache[attrCacheKey]
+  } else if (isOn(key)) {
+    on(el, key[2].toLowerCase() + key.slice(3), () => value, { effect: true })
+  } else if (
+    key[0] === '.'
+      ? ((key = key.slice(1)), true)
+      : key[0] === '^'
+        ? ((key = key.slice(1)), false)
+        : shouldSetAsProp(el, key, value, isSVG)
   ) {
-    return false
-  }
-
-  // native onclick with string value, must be set as attribute
-  if (isNativeOn(key) && isString(value)) {
-    return false
+    if (key === 'innerHTML') {
+      setHtml(el, value)
+    } else if (key === 'textContent') {
+      setText(el, value)
+    } else if (key === 'value' && canSetValueDirectly(el.tagName)) {
+      setValue(el, value)
+    } else {
+      setDOMProp(el, key, value)
+    }
+  } else {
+    // TODO special case for <input v-model type="checkbox">
+    setAttr(el, key, value)
   }
-
-  return key in el
 }
index ae7ea7cdb5054d791c5a2bb17de68f4596ccc234..3aeb3055ec3d04f4e1fa2ecbcfc6b5935b977f49 100644 (file)
@@ -9,15 +9,16 @@ export { renderEffect } from './renderEffect'
 export { createSlot, createForSlots } from './componentSlots'
 export { template, children, next } from './dom/template'
 export { createTextNode } from './dom/node'
-export { setStyle } from './dom/style'
 export {
   setText,
   setHtml,
   setClass,
+  setStyle,
+  setClassIncremental,
+  setStyleIncremental,
   setAttr,
   setValue,
   setDOMProp,
-  setDynamicProp,
   setDynamicProps,
 } from './dom/prop'
 export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
index b13df275769fcdf0ed5cf29298d86380fb56f5ce..7905a3bc2ee01019c31ea27960f6083bc18cbc33 100644 (file)
@@ -180,11 +180,6 @@ export function isRenderableAttrValue(value: unknown): boolean {
   return type === 'string' || type === 'number' || type === 'boolean'
 }
 
-/**
- * cache seen attributes which must be set as attribute
- */
-export const attributeCache: Record<string, boolean> = Object.create(null)
-
 /*
  * The following attributes must be set as attribute
  */
index ef598a03ced79d4862991ebadec82fd1bb2c61c7..223f6f53d4b1f70810cbba0798418c656a655a88 100644 (file)
@@ -1,6 +1,6 @@
 import { hyphenate, isArray, isObject, isString } from './general'
 
-export type NormalizedStyle = Record<string, string | number>
+export type NormalizedStyle = Record<string, string>
 
 export function normalizeStyle(
   value: unknown,
index 9bfe6e58ca1d769648103fc07256e62d59598c94..672192fc6c4a5a624d3ca41ca345c150dfb9642e 100644 (file)
@@ -22,7 +22,10 @@ export function trimVaporExportsPlugin(format, pkgName) {
       {
         name: 'trim-vapor-exports',
         transform(code, id) {
-          if (id.endsWith('runtime-core/src/index.ts')) {
+          if (
+            id.endsWith('runtime-core/src/index.ts') ||
+            id.endsWith('runtime-dom/src/index.ts')
+          ) {
             const index = code.lastIndexOf('// VAPOR ---')
             return code.slice(0, index)
           }