fix(sfc): ensure `<script setup>` binding behavior consistency on `this` between prod and dev

close #6248
This commit is contained in:
Evan You 2022-11-10 16:02:45 +08:00
parent 15e889afaf
commit f73925d76a
2 changed files with 37 additions and 24 deletions

View File

@ -4,7 +4,8 @@ import {
getCurrentInstance, getCurrentInstance,
nodeOps, nodeOps,
createApp, createApp,
shallowReadonly shallowReadonly,
defineComponent
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { ComponentInternalInstance, ComponentOptions } from '../src/component' import { ComponentInternalInstance, ComponentOptions } from '../src/component'
@ -458,4 +459,24 @@ describe('component: proxy', () => {
)} was accessed during render ` + `but is not defined on instance.` )} was accessed during render ` + `but is not defined on instance.`
).toHaveBeenWarned() ).toHaveBeenWarned()
}) })
test('should prevent mutating script setup bindings', () => {
const Comp = defineComponent({
render() {},
setup() {
return {
__isScriptSetup: true,
foo: 1
}
},
mounted() {
expect('foo' in this).toBe(false)
try {
this.foo = 123
} catch (e) {}
}
})
render(h(Comp), nodeOps.createElement('div'))
expect(`Cannot mutate <script setup> binding "foo"`).toHaveBeenWarned()
})
}) })

View File

@ -270,6 +270,9 @@ export interface ComponentRenderContext {
export const isReservedPrefix = (key: string) => key === '_' || key === '$' export const isReservedPrefix = (key: string) => key === '_' || key === '$'
const hasSetupBinding = (state: Data, key: string) =>
state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key)
export const PublicInstanceProxyHandlers: ProxyHandler<any> = { export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) { get({ _: instance }: ComponentRenderContext, key: string) {
const { ctx, setupState, data, props, accessCache, type, appContext } = const { ctx, setupState, data, props, accessCache, type, appContext } =
@ -280,19 +283,6 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
return true return true
} }
// prioritize <script setup> bindings during dev.
// this allows even properties that start with _ or $ to be used - so that
// it aligns with the production behavior where the render fn is inlined and
// indeed has access to all declared variables.
if (
__DEV__ &&
setupState !== EMPTY_OBJ &&
setupState.__isScriptSetup &&
hasOwn(setupState, key)
) {
return setupState[key]
}
// data / props / ctx // data / props / ctx
// This getter gets called for every property access on the render context // This getter gets called for every property access on the render context
// during render and is a major hotspot. The most expensive part of this // during render and is a major hotspot. The most expensive part of this
@ -314,7 +304,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
return props![key] return props![key]
// default: just fallthrough // default: just fallthrough
} }
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { } else if (hasSetupBinding(setupState, key)) {
accessCache![key] = AccessTypes.SETUP accessCache![key] = AccessTypes.SETUP
return setupState[key] return setupState[key]
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) { } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
@ -403,26 +393,28 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
value: any value: any
): boolean { ): boolean {
const { data, setupState, ctx } = instance const { data, setupState, ctx } = instance
if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { if (hasSetupBinding(setupState, key)) {
setupState[key] = value setupState[key] = value
return true return true
} else if (
__DEV__ &&
setupState.__isScriptSetup &&
hasOwn(setupState, key)
) {
warn(`Cannot mutate <script setup> binding "${key}" from Options API.`)
return false
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) { } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
data[key] = value data[key] = value
return true return true
} else if (hasOwn(instance.props, key)) { } else if (hasOwn(instance.props, key)) {
__DEV__ && __DEV__ && warn(`Attempting to mutate prop "${key}". Props are readonly.`)
warn(
`Attempting to mutate prop "${key}". Props are readonly.`,
instance
)
return false return false
} }
if (key[0] === '$' && key.slice(1) in instance) { if (key[0] === '$' && key.slice(1) in instance) {
__DEV__ && __DEV__ &&
warn( warn(
`Attempting to mutate public property "${key}". ` + `Attempting to mutate public property "${key}". ` +
`Properties starting with $ are reserved and readonly.`, `Properties starting with $ are reserved and readonly.`
instance
) )
return false return false
} else { } else {
@ -449,7 +441,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
return ( return (
!!accessCache![key] || !!accessCache![key] ||
(data !== EMPTY_OBJ && hasOwn(data, key)) || (data !== EMPTY_OBJ && hasOwn(data, key)) ||
(setupState !== EMPTY_OBJ && hasOwn(setupState, key)) || hasSetupBinding(setupState, key) ||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) || ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
hasOwn(ctx, key) || hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) || hasOwn(publicPropertiesMap, key) ||