Skip to content

Commit 90b086b

Browse files
edwardnycneiyichao03
andauthored
fix: attrs update not correctly mapped to props #833 (#835)
Co-authored-by: neiyichao03 <[email protected]>
1 parent a5a68e0 commit 90b086b

File tree

4 files changed

+107
-31
lines changed

4 files changed

+107
-31
lines changed

src/mixin.ts

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,24 @@ import {
1818
activateCurrentInstance,
1919
resolveScopedSlots,
2020
asVmProperty,
21+
updateVmAttrs,
2122
} from './utils/instance'
2223
import {
2324
getVueConstructor,
2425
SetupContext,
2526
toVue3ComponentInstance,
2627
} from './runtimeContext'
27-
import { createObserver, reactive } from './reactivity/reactive'
28+
import { createObserver } from './reactivity/reactive'
2829

2930
export function mixin(Vue: VueConstructor) {
3031
Vue.mixin({
3132
beforeCreate: functionApiInit,
3233
mounted(this: ComponentInstance) {
3334
updateTemplateRef(this)
3435
},
36+
beforeUpdate() {
37+
updateVmAttrs(this as ComponentInstance, this as SetupContext)
38+
},
3539
updated(this: ComponentInstance) {
3640
updateTemplateRef(this)
3741
if (this.$vnode?.context) {
@@ -212,7 +216,6 @@ export function mixin(Vue: VueConstructor) {
212216
'isServer',
213217
'ssrContext',
214218
]
215-
const propsReactiveProxy = ['attrs']
216219
const methodReturnVoid = ['emit']
217220

218221
propsPlain.forEach((key) => {
@@ -229,35 +232,7 @@ export function mixin(Vue: VueConstructor) {
229232
})
230233
})
231234

232-
let propsProxy: any
233-
propsReactiveProxy.forEach((key) => {
234-
let srcKey = `$${key}`
235-
proxy(ctx, key, {
236-
get: () => {
237-
if (propsProxy) return propsProxy
238-
propsProxy = reactive({})
239-
const source = vm[srcKey]
240-
241-
for (const attr of Object.keys(source)) {
242-
proxy(propsProxy, attr, {
243-
get: () => {
244-
// to ensure it always return the latest value
245-
return vm[srcKey][attr]
246-
},
247-
})
248-
}
249-
250-
return propsProxy
251-
},
252-
set() {
253-
__DEV__ &&
254-
warn(
255-
`Cannot assign to '${key}' because it is a read-only property`,
256-
vm
257-
)
258-
},
259-
})
260-
})
235+
updateVmAttrs(vm, ctx)
261236

262237
methodReturnVoid.forEach((key) => {
263238
const srcKey = `$${key}`

src/utils/instance.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import {
55
getCurrentInstance,
66
ComponentInternalInstance,
77
InternalSlots,
8+
SetupContext,
89
} from '../runtimeContext'
910
import { Ref, isRef, isReactive } from '../apis'
1011
import { hasOwn, proxy, warn } from './utils'
1112
import { createSlotProxy, resolveSlots } from './helper'
13+
import { reactive } from '../reactivity/reactive'
1214

1315
export function asVmProperty(
1416
vm: ComponentInstance,
@@ -101,6 +103,42 @@ export function updateTemplateRef(vm: ComponentInstance) {
101103
vmStateManager.set(vm, 'refs', validNewKeys)
102104
}
103105

106+
export function updateVmAttrs(vm: ComponentInstance, ctx: SetupContext) {
107+
if (!vm || !ctx) {
108+
return
109+
}
110+
let attrBindings = vmStateManager.get(vm, 'attrBindings')
111+
if (!attrBindings) {
112+
const observedData = reactive({})
113+
vmStateManager.set(vm, 'attrBindings', observedData)
114+
attrBindings = observedData
115+
proxy(ctx, 'attrs', {
116+
get: () => {
117+
return attrBindings
118+
},
119+
set() {
120+
__DEV__ &&
121+
warn(
122+
`Cannot assign to '$attrs' because it is a read-only property`,
123+
vm
124+
)
125+
},
126+
})
127+
}
128+
129+
const source = vm.$attrs
130+
for (const attr of Object.keys(source)) {
131+
if (!hasOwn(attrBindings!, attr)) {
132+
proxy(attrBindings, attr, {
133+
get: () => {
134+
// to ensure it always return the latest value
135+
return vm.$attrs[attr]
136+
},
137+
})
138+
}
139+
}
140+
}
141+
104142
export function resolveScopedSlots(
105143
vm: ComponentInstance,
106144
slotsProxy: InternalSlots

src/utils/vmStateManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ComponentInstance, Data } from '../component'
33
export interface VfaState {
44
refs?: string[]
55
rawBindings?: Data
6+
attrBindings?: Data
67
slots?: string[]
78
}
89

test/setup.spec.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
toRaw,
1212
nextTick,
1313
isReactive,
14+
watchEffect,
1415
defineComponent,
1516
onMounted,
1617
set,
@@ -1242,4 +1243,65 @@ describe('setup', () => {
12421243
})
12431244
expect(spy).toHaveBeenCalledTimes(0)
12441245
})
1246+
1247+
// #833
1248+
it('attrs update not correctly mapped to props', async () => {
1249+
let propsFromAttrs = null
1250+
const Field = defineComponent({
1251+
props: ['firstName', 'lastName'],
1252+
setup(props, { attrs }) {
1253+
watchEffect(() => {
1254+
propsFromAttrs = props
1255+
})
1256+
return () => {
1257+
return h('div', [props.firstName, props.lastName])
1258+
}
1259+
},
1260+
})
1261+
1262+
const WrapperField = defineComponent({
1263+
setup(props, ctx) {
1264+
const { attrs } = ctx
1265+
return () => {
1266+
return h(Field, {
1267+
attrs: {
1268+
...attrs,
1269+
},
1270+
})
1271+
}
1272+
},
1273+
})
1274+
1275+
const App = defineComponent({
1276+
setup() {
1277+
let person = ref({
1278+
firstName: 'wang',
1279+
})
1280+
onMounted(async () => {
1281+
person.value = {
1282+
firstName: 'wang',
1283+
lastName: 'xiao',
1284+
}
1285+
})
1286+
return () => {
1287+
return h('div', [
1288+
h(WrapperField, {
1289+
attrs: {
1290+
...person.value,
1291+
},
1292+
}),
1293+
])
1294+
}
1295+
},
1296+
})
1297+
const vm = new Vue(App).$mount()
1298+
1299+
await sleep(100)
1300+
await vm.$nextTick()
1301+
expect(vm.$el.outerText === 'wangxiao')
1302+
expect(propsFromAttrs).toStrictEqual({
1303+
firstName: 'wang',
1304+
lastName: 'xiao',
1305+
})
1306+
})
12451307
})

0 commit comments

Comments
 (0)