diff --git a/packages/compiler-core/src/options.ts b/packages/compiler-core/src/options.ts
index bb59f4940..43d019b75 100644
--- a/packages/compiler-core/src/options.ts
+++ b/packages/compiler-core/src/options.ts
@@ -83,7 +83,11 @@ export const enum BindingTypes {
/**
* a const binding that may be a ref.
*/
- SETUP_CONST_REF = 'setup-const-ref',
+ SETUP_MAYBE_REF = 'setup-maybe-ref',
+ /**
+ * bindings that are guaranteed to be refs
+ */
+ SETUP_REF = 'setup-ref',
/**
* declared by other options, e.g. computed, inject
*/
@@ -91,7 +95,7 @@ export const enum BindingTypes {
}
export interface BindingMetadata {
- [key: string]: BindingTypes
+ [key: string]: BindingTypes | undefined
}
interface SharedTransformCodegenOptions {
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index e3989a807..1d4a46a13 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -272,7 +272,8 @@ export function resolveComponentType(
}
const tagFromSetup =
checkType(BindingTypes.SETUP_LET) ||
- checkType(BindingTypes.SETUP_CONST_REF)
+ checkType(BindingTypes.SETUP_REF) ||
+ checkType(BindingTypes.SETUP_MAYBE_REF)
if (tagFromSetup) {
return context.inline
? // setup scope bindings that may be refs need to be unrefed
diff --git a/packages/compiler-core/src/transforms/transformExpression.ts b/packages/compiler-core/src/transforms/transformExpression.ts
index 9c33cf67b..edee2f52e 100644
--- a/packages/compiler-core/src/transforms/transformExpression.ts
+++ b/packages/compiler-core/src/transforms/transformExpression.ts
@@ -21,14 +21,22 @@ import {
isGloballyWhitelisted,
makeMap,
babelParserDefaultPlugins,
- hasOwn
+ hasOwn,
+ isString
} from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
-import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
+import {
+ Node,
+ Function,
+ Identifier,
+ ObjectProperty,
+ AssignmentExpression,
+ UpdateExpression
+} from '@babel/types'
import { validateBrowserExpression } from '../validateExpression'
import { parse } from '@babel/parser'
import { walk } from 'estree-walker'
-import { UNREF } from '../runtimeHelpers'
+import { IS_REF, UNREF } from '../runtimeHelpers'
import { BindingTypes } from '../options'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@@ -100,28 +108,81 @@ export function processExpression(
}
const { inline, bindingMetadata } = context
- const prefix = (raw: string) => {
+ const rewriteIdentifier = (raw: string, parent?: Node, id?: Identifier) => {
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
if (inline) {
+ const isAssignmentLVal =
+ parent && parent.type === 'AssignmentExpression' && parent.left === id
+ const isUpdateArg =
+ parent && parent.type === 'UpdateExpression' && parent.argument === id
// setup inline mode
if (type === BindingTypes.SETUP_CONST) {
return raw
+ } else if (type === BindingTypes.SETUP_REF) {
+ return isAssignmentLVal || isUpdateArg
+ ? `${raw}.value`
+ : `${context.helperString(UNREF)}(${raw})`
} else if (
- type === BindingTypes.SETUP_CONST_REF ||
+ type === BindingTypes.SETUP_MAYBE_REF ||
type === BindingTypes.SETUP_LET
) {
- return `${context.helperString(UNREF)}(${raw})`
+ if (isAssignmentLVal) {
+ if (type === BindingTypes.SETUP_MAYBE_REF) {
+ // const binding that may or may not be ref
+ // if it's not a ref, then the assignment doesn't make sense so
+ // just no-op it
+ // x = y ---> !isRef(x) ? null : x.value = y
+ return `!${context.helperString(
+ IS_REF
+ )}(${raw}) ? null : ${raw}.value`
+ } else {
+ // let binding.
+ // this is a bit more tricky as we need to cover the case where
+ // let is a local non-ref value, and we need to replicate the
+ // right hand side value.
+ // x = y --> isRef(x) ? x.value = y : x = y
+ const rVal = (parent as AssignmentExpression).right
+ const rExp = rawExp.slice(rVal.start! - 1, rVal.end! - 1)
+ const rExpString = stringifyExpression(
+ processExpression(createSimpleExpression(rExp, false), context)
+ )
+ return `${context.helperString(IS_REF)}(${raw})${
+ context.isTS ? ` //@ts-ignore\n` : ``
+ } ? ${raw}.value = ${rExpString} : ${raw}`
+ }
+ } else if (isUpdateArg) {
+ // make id replace parent in the code range so the raw update operator
+ // is removed
+ id!.start = parent!.start
+ id!.end = parent!.end
+ const { prefix: isPrefix, operator } = parent as UpdateExpression
+ const prefix = isPrefix ? operator : ``
+ const postfix = isPrefix ? `` : operator
+ if (type === BindingTypes.SETUP_MAYBE_REF) {
+ // const binding that may or may not be ref
+ // if it's not a ref, then the assignment doesn't make sense so
+ // just no-op it
+ // x++ ---> !isRef(x) ? null : x.value++
+ return `!${context.helperString(
+ IS_REF
+ )}(${raw}) ? null : ${prefix}${raw}.value${postfix}`
+ } else {
+ // let binding.
+ // x++ --> isRef(a) ? a.value++ : a++
+ return `${context.helperString(IS_REF)}(${raw})${
+ context.isTS ? ` //@ts-ignore\n` : ``
+ } ? ${prefix}${raw}.value${postfix} : ${prefix}${raw}${postfix}`
+ }
+ } else {
+ return `${context.helperString(UNREF)}(${raw})`
+ }
} else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode
// it gets correct type
return `__props.${raw}`
}
} else {
- if (
- type === BindingTypes.SETUP_LET ||
- type === BindingTypes.SETUP_CONST ||
- type === BindingTypes.SETUP_CONST_REF
- ) {
+ if (type && type.startsWith('setup')) {
// setup bindings in non-inline mode
return `$setup.${raw}`
} else if (type) {
@@ -149,7 +210,7 @@ export function processExpression(
!isGloballyWhitelisted(rawExp) &&
!isLiteralWhitelisted(rawExp)
) {
- node.content = prefix(rawExp)
+ node.content = rewriteIdentifier(rawExp)
} else if (!context.identifiers[rawExp] && !bailConstant) {
// mark node constant for hoisting unless it's referring a scope variable
node.isConstant = true
@@ -199,7 +260,7 @@ export function processExpression(
// we rewrite the value
node.prefix = `${node.name}: `
}
- node.name = prefix(node.name)
+ node.name = rewriteIdentifier(node.name, parent, node)
ids.push(node)
} else if (!isStaticPropertyKey(node, parent)) {
// The identifier is considered constant unless it's pointing to a
@@ -373,3 +434,15 @@ function shouldPrefix(id: Identifier, parent: Node) {
return true
}
+
+function stringifyExpression(exp: ExpressionNode | string): string {
+ if (isString(exp)) {
+ return exp
+ } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
+ return exp.content
+ } else {
+ return (exp.children as (ExpressionNode | string)[])
+ .map(stringifyExpression)
+ .join('')
+ }
+}
diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts
index 0274b642e..7bdffd81c 100644
--- a/packages/compiler-core/src/transforms/vModel.ts
+++ b/packages/compiler-core/src/transforms/vModel.ts
@@ -5,7 +5,8 @@ import {
createCompoundExpression,
NodeTypes,
Property,
- ElementTypes
+ ElementTypes,
+ ExpressionNode
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
@@ -14,7 +15,8 @@ import {
hasScopeRef,
isStaticExp
} from '../utils'
-import { helperNameMap, IS_REF, UNREF } from '../runtimeHelpers'
+import { IS_REF } from '../runtimeHelpers'
+import { BindingTypes } from '../options'
export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir
@@ -31,10 +33,14 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
// im SFC
+
+
`,
{ inlineTemplate: true }
)
+ // known const ref: set value
+ expect(content).toMatch(`count.value = $event`)
+ // const but maybe ref: only assign after check
+ expect(content).toMatch(`_isRef(maybe) ? maybe.value = $event : null`)
+ // let: handle both cases
+ expect(content).toMatch(
+ `_isRef(lett) ? lett.value = $event : lett = $event`
+ )
+ assertCode(content)
+ })
+
+ test('template assignment expression codegen', () => {
+ const { content } = compile(
+ `
+
+
+
+
+
+ `,
+ { inlineTemplate: true }
+ )
+ // known const ref: set value
+ expect(content).toMatch(`count.value = 1`)
+ // const but maybe ref: only assign after check
+ expect(content).toMatch(
+ `!_isRef(maybe) ? null : maybe.value = _unref(count)`
+ )
+ // let: handle both cases
+ expect(content).toMatch(
+ `_isRef(lett) ? lett.value = _unref(count) : lett = _unref(count)`
+ )
+ assertCode(content)
+ })
+
+ test('template update expression codegen', () => {
+ const { content } = compile(
+ `
+
+
+
+
+
+
+
+
+ `,
+ { inlineTemplate: true }
+ )
+ // known const ref: set value
+ expect(content).toMatch(`count.value++`)
+ expect(content).toMatch(`--count.value`)
+ // const but maybe ref: only assign after check
+ expect(content).toMatch(`!_isRef(maybe) ? null : maybe.value++`)
+ expect(content).toMatch(`!_isRef(maybe) ? null : --maybe.value`)
+ // let: handle both cases
+ expect(content).toMatch(`_isRef(lett) ? lett.value++ : lett++`)
+ expect(content).toMatch(`_isRef(lett) ? --lett.value : --lett`)
assertCode(content)
})
})
@@ -381,9 +453,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`let d`)
assertCode(content)
expect(bindings).toStrictEqual({
- foo: BindingTypes.SETUP_CONST_REF,
- a: BindingTypes.SETUP_CONST_REF,
- b: BindingTypes.SETUP_CONST_REF,
+ foo: BindingTypes.SETUP_REF,
+ a: BindingTypes.SETUP_REF,
+ b: BindingTypes.SETUP_REF,
c: BindingTypes.SETUP_LET,
d: BindingTypes.SETUP_LET
})
@@ -403,9 +475,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`return { a, b, c }`)
assertCode(content)
expect(bindings).toStrictEqual({
- a: BindingTypes.SETUP_CONST_REF,
- b: BindingTypes.SETUP_CONST_REF,
- c: BindingTypes.SETUP_CONST_REF
+ a: BindingTypes.SETUP_REF,
+ b: BindingTypes.SETUP_REF,
+ c: BindingTypes.SETUP_REF
})
})
@@ -495,12 +567,12 @@ const { props, emit } = defineOptions({
)
expect(content).toMatch(`return { n, a, c, d, f, g }`)
expect(bindings).toStrictEqual({
- n: BindingTypes.SETUP_CONST_REF,
- a: BindingTypes.SETUP_CONST_REF,
- c: BindingTypes.SETUP_CONST_REF,
- d: BindingTypes.SETUP_CONST_REF,
- f: BindingTypes.SETUP_CONST_REF,
- g: BindingTypes.SETUP_CONST_REF
+ n: BindingTypes.SETUP_REF,
+ a: BindingTypes.SETUP_REF,
+ c: BindingTypes.SETUP_REF,
+ d: BindingTypes.SETUP_REF,
+ f: BindingTypes.SETUP_REF,
+ g: BindingTypes.SETUP_REF
})
assertCode(content)
})
@@ -519,10 +591,10 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`console.log(n.value, a.value, b.value, c.value)`)
expect(content).toMatch(`return { n, a, b, c }`)
expect(bindings).toStrictEqual({
- n: BindingTypes.SETUP_CONST_REF,
- a: BindingTypes.SETUP_CONST_REF,
- b: BindingTypes.SETUP_CONST_REF,
- c: BindingTypes.SETUP_CONST_REF
+ n: BindingTypes.SETUP_REF,
+ a: BindingTypes.SETUP_REF,
+ b: BindingTypes.SETUP_REF,
+ c: BindingTypes.SETUP_REF
})
assertCode(content)
})
@@ -542,9 +614,9 @@ const { props, emit } = defineOptions({
expect(content).toMatch(`\nconst e = _ref(__e);`)
expect(content).toMatch(`return { b, d, e }`)
expect(bindings).toStrictEqual({
- b: BindingTypes.SETUP_CONST_REF,
- d: BindingTypes.SETUP_CONST_REF,
- e: BindingTypes.SETUP_CONST_REF
+ b: BindingTypes.SETUP_REF,
+ d: BindingTypes.SETUP_REF,
+ e: BindingTypes.SETUP_REF
})
assertCode(content)
})
@@ -728,8 +800,8 @@ describe('SFC analyze
`)
expect(bindings).toStrictEqual({
- foo: BindingTypes.SETUP_CONST_REF,
- bar: BindingTypes.SETUP_CONST_REF
+ foo: BindingTypes.SETUP_MAYBE_REF,
+ bar: BindingTypes.SETUP_MAYBE_REF
})
})
@@ -748,8 +820,8 @@ describe('SFC analyze
`)
expect(bindings).toStrictEqual({
- foo: BindingTypes.SETUP_CONST_REF,
- bar: BindingTypes.SETUP_CONST_REF
+ foo: BindingTypes.SETUP_MAYBE_REF,
+ bar: BindingTypes.SETUP_MAYBE_REF
})
})
@@ -867,7 +939,7 @@ describe('SFC analyze
`)
+
expect(bindings).toStrictEqual({
+ r: BindingTypes.SETUP_CONST,
+ a: BindingTypes.SETUP_REF,
+ b: BindingTypes.SETUP_LET,
+ c: BindingTypes.SETUP_CONST,
+ d: BindingTypes.SETUP_MAYBE_REF,
+ e: BindingTypes.SETUP_LET,
foo: BindingTypes.PROPS
})
})
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index bdbd0b1d3..4e1d1288d 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -20,7 +20,8 @@ import {
Statement,
Expression,
LabeledStatement,
- TSUnionType
+ TSUnionType,
+ CallExpression
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
@@ -159,6 +160,7 @@ export function compileScript(
source: string
}
> = Object.create(null)
+ const userImportAlias: Record = Object.create(null)
const setupBindings: Record = Object.create(null)
const refBindings: Record = Object.create(null)
const refIdentifiers: Set = new Set()
@@ -220,12 +222,22 @@ export function compileScript(
)
}
+ function registerUserImport(
+ source: string,
+ local: string,
+ imported: string | false
+ ) {
+ if (source === 'vue' && imported) {
+ userImportAlias[imported] = local
+ }
+ userImports[local] = {
+ imported: imported || null,
+ source
+ }
+ }
+
function processDefineOptions(node: Node): boolean {
- if (
- node.type === 'CallExpression' &&
- node.callee.type === 'Identifier' &&
- node.callee.name === DEFINE_OPTIONS
- ) {
+ if (isCallOf(node, DEFINE_OPTIONS)) {
if (hasOptionsCall) {
error(`duplicate ${DEFINE_OPTIONS}() call`, node)
}
@@ -308,7 +320,7 @@ export function compileScript(
if (id.name[0] === '$') {
error(`ref variable identifiers cannot start with $.`, id)
}
- refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_CONST_REF
+ refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP_REF
refIdentifiers.add(id)
}
@@ -409,15 +421,11 @@ export function compileScript(
if (node.type === 'ImportDeclaration') {
// record imports for dedupe
for (const specifier of node.specifiers) {
- const name = specifier.local.name
const imported =
specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name
- userImports[name] = {
- imported: imported || null,
- source: node.source.value
- }
+ registerUserImport(node.source.value, specifier.local.name, imported)
}
} else if (node.type === 'ExportDefaultDeclaration') {
// export default
@@ -567,10 +575,7 @@ export function compileScript(
error(`different imports aliased to same local name.`, specifier)
}
} else {
- userImports[local] = {
- imported: imported || null,
- source: node.source.value
- }
+ registerUserImport(source, local, imported)
}
}
if (removed === node.specifiers.length) {
@@ -605,7 +610,7 @@ export function compileScript(
node.type === 'ClassDeclaration') &&
!node.declare
) {
- walkDeclaration(node, setupBindings)
+ walkDeclaration(node, setupBindings, userImportAlias)
}
// Type declarations
@@ -783,9 +788,10 @@ export function compileScript(
Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg))
}
for (const [key, { source }] of Object.entries(userImports)) {
- bindingMetadata[key] = source.endsWith('.vue')
- ? BindingTypes.SETUP_CONST
- : BindingTypes.SETUP_CONST_REF
+ bindingMetadata[key] =
+ source.endsWith('.vue') || source === 'vue'
+ ? BindingTypes.SETUP_CONST
+ : BindingTypes.SETUP_MAYBE_REF
}
for (const key in setupBindings) {
bindingMetadata[key] = setupBindings[key]
@@ -941,32 +947,34 @@ export function compileScript(
function walkDeclaration(
node: Declaration,
- bindings: Record
+ bindings: Record,
+ userImportAlias: Record
) {
if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const'
// export const foo = ...
for (const { id, init } of node.declarations) {
- const isUseOptionsCall = !!(
- isConst &&
- init &&
- init.type === 'CallExpression' &&
- init.callee.type === 'Identifier' &&
- init.callee.name === DEFINE_OPTIONS
- )
+ const isUseOptionsCall = !!(isConst && isCallOf(init, DEFINE_OPTIONS))
if (id.type === 'Identifier') {
- bindings[id.name] =
+ let bindingType
+ if (
// if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it
isUseOptionsCall ||
(isConst &&
- init!.type !== 'Identifier' && // const a = b
- init!.type !== 'CallExpression' && // const a = ref()
- init!.type !== 'MemberExpression') // const a = b.c
- ? BindingTypes.SETUP_CONST
- : isConst
- ? BindingTypes.SETUP_CONST_REF
- : BindingTypes.SETUP_LET
+ canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive'))
+ ) {
+ bindingType = BindingTypes.SETUP_CONST
+ } else if (isConst) {
+ if (isCallOf(init, userImportAlias['ref'] || 'ref')) {
+ bindingType = BindingTypes.SETUP_REF
+ } else {
+ bindingType = BindingTypes.SETUP_MAYBE_REF
+ }
+ } else {
+ bindingType = BindingTypes.SETUP_LET
+ }
+ bindings[id.name] = bindingType
} else if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst, isUseOptionsCall)
} else if (id.type === 'ArrayPattern') {
@@ -998,7 +1006,7 @@ function walkObjectPattern(
bindings[p.key.name] = isUseOptionsCall
? BindingTypes.SETUP_CONST
: isConst
- ? BindingTypes.SETUP_CONST_REF
+ ? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
} else {
walkPattern(p.value, bindings, isConst, isUseOptionsCall)
@@ -1035,7 +1043,7 @@ function walkPattern(
bindings[node.name] = isUseOptionsCall
? BindingTypes.SETUP_CONST
: isConst
- ? BindingTypes.SETUP_CONST_REF
+ ? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
} else if (node.type === 'RestElement') {
// argument can only be identifer when destructuring
@@ -1051,7 +1059,7 @@ function walkPattern(
bindings[node.left.name] = isUseOptionsCall
? BindingTypes.SETUP_CONST
: isConst
- ? BindingTypes.SETUP_CONST_REF
+ ? BindingTypes.SETUP_MAYBE_REF
: BindingTypes.SETUP_LET
} else {
walkPattern(node.left, bindings, isConst)
@@ -1419,6 +1427,43 @@ function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
return []
}
+function isCallOf(node: Node | null, name: string): node is CallExpression {
+ return !!(
+ node &&
+ node.type === 'CallExpression' &&
+ node.callee.type === 'Identifier' &&
+ node.callee.name === name
+ )
+}
+
+function canNeverBeRef(node: Node, userReactiveImport: string): boolean {
+ if (isCallOf(node, userReactiveImport)) {
+ return true
+ }
+ switch (node.type) {
+ case 'UnaryExpression':
+ case 'BinaryExpression':
+ case 'ArrayExpression':
+ case 'ObjectExpression':
+ case 'FunctionExpression':
+ case 'ArrowFunctionExpression':
+ case 'UpdateExpression':
+ case 'ClassExpression':
+ case 'TaggedTemplateExpression':
+ return true
+ case 'SequenceExpression':
+ return canNeverBeRef(
+ node.expressions[node.expressions.length - 1],
+ userReactiveImport
+ )
+ default:
+ if (node.type.endsWith('Literal')) {
+ return true
+ }
+ return false
+ }
+}
+
/**
* Analyze bindings in normal `