Skip to content

Commit

Permalink
fix: attrs update not correctly mapped to props #833 (#835)
Browse files Browse the repository at this point in the history
Co-authored-by: neiyichao03 <nieyichao03@kuaishou.com>
  • Loading branch information
edwardnyc and neiyichao03 committed Oct 28, 2021
1 parent a5a68e0 commit 90b086b
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 31 deletions.
37 changes: 6 additions & 31 deletions src/mixin.ts
Expand Up @@ -18,20 +18,24 @@ import {
activateCurrentInstance,
resolveScopedSlots,
asVmProperty,
updateVmAttrs,
} from './utils/instance'
import {
getVueConstructor,
SetupContext,
toVue3ComponentInstance,
} from './runtimeContext'
import { createObserver, reactive } from './reactivity/reactive'
import { createObserver } from './reactivity/reactive'

export function mixin(Vue: VueConstructor) {
Vue.mixin({
beforeCreate: functionApiInit,
mounted(this: ComponentInstance) {
updateTemplateRef(this)
},
beforeUpdate() {
updateVmAttrs(this as ComponentInstance, this as SetupContext)
},
updated(this: ComponentInstance) {
updateTemplateRef(this)
if (this.$vnode?.context) {
Expand Down Expand Up @@ -212,7 +216,6 @@ export function mixin(Vue: VueConstructor) {
'isServer',
'ssrContext',
]
const propsReactiveProxy = ['attrs']
const methodReturnVoid = ['emit']

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

let propsProxy: any
propsReactiveProxy.forEach((key) => {
let srcKey = `$${key}`
proxy(ctx, key, {
get: () => {
if (propsProxy) return propsProxy
propsProxy = reactive({})
const source = vm[srcKey]

for (const attr of Object.keys(source)) {
proxy(propsProxy, attr, {
get: () => {
// to ensure it always return the latest value
return vm[srcKey][attr]
},
})
}

return propsProxy
},
set() {
__DEV__ &&
warn(
`Cannot assign to '${key}' because it is a read-only property`,
vm
)
},
})
})
updateVmAttrs(vm, ctx)

methodReturnVoid.forEach((key) => {
const srcKey = `$${key}`
Expand Down
38 changes: 38 additions & 0 deletions src/utils/instance.ts
Expand Up @@ -5,10 +5,12 @@ import {
getCurrentInstance,
ComponentInternalInstance,
InternalSlots,
SetupContext,
} from '../runtimeContext'
import { Ref, isRef, isReactive } from '../apis'
import { hasOwn, proxy, warn } from './utils'
import { createSlotProxy, resolveSlots } from './helper'
import { reactive } from '../reactivity/reactive'

export function asVmProperty(
vm: ComponentInstance,
Expand Down Expand Up @@ -101,6 +103,42 @@ export function updateTemplateRef(vm: ComponentInstance) {
vmStateManager.set(vm, 'refs', validNewKeys)
}

export function updateVmAttrs(vm: ComponentInstance, ctx: SetupContext) {
if (!vm || !ctx) {
return
}
let attrBindings = vmStateManager.get(vm, 'attrBindings')
if (!attrBindings) {
const observedData = reactive({})
vmStateManager.set(vm, 'attrBindings', observedData)
attrBindings = observedData
proxy(ctx, 'attrs', {
get: () => {
return attrBindings
},
set() {
__DEV__ &&
warn(
`Cannot assign to '$attrs' because it is a read-only property`,
vm
)
},
})
}

const source = vm.$attrs
for (const attr of Object.keys(source)) {
if (!hasOwn(attrBindings!, attr)) {
proxy(attrBindings, attr, {
get: () => {
// to ensure it always return the latest value
return vm.$attrs[attr]
},
})
}
}
}

export function resolveScopedSlots(
vm: ComponentInstance,
slotsProxy: InternalSlots
Expand Down
1 change: 1 addition & 0 deletions src/utils/vmStateManager.ts
Expand Up @@ -3,6 +3,7 @@ import { ComponentInstance, Data } from '../component'
export interface VfaState {
refs?: string[]
rawBindings?: Data
attrBindings?: Data
slots?: string[]
}

Expand Down
62 changes: 62 additions & 0 deletions test/setup.spec.js
Expand Up @@ -11,6 +11,7 @@ const {
toRaw,
nextTick,
isReactive,
watchEffect,
defineComponent,
onMounted,
set,
Expand Down Expand Up @@ -1242,4 +1243,65 @@ describe('setup', () => {
})
expect(spy).toHaveBeenCalledTimes(0)
})

// #833
it('attrs update not correctly mapped to props', async () => {
let propsFromAttrs = null
const Field = defineComponent({
props: ['firstName', 'lastName'],
setup(props, { attrs }) {
watchEffect(() => {
propsFromAttrs = props
})
return () => {
return h('div', [props.firstName, props.lastName])
}
},
})

const WrapperField = defineComponent({
setup(props, ctx) {
const { attrs } = ctx
return () => {
return h(Field, {
attrs: {
...attrs,
},
})
}
},
})

const App = defineComponent({
setup() {
let person = ref({
firstName: 'wang',
})
onMounted(async () => {
person.value = {
firstName: 'wang',
lastName: 'xiao',
}
})
return () => {
return h('div', [
h(WrapperField, {
attrs: {
...person.value,
},
}),
])
}
},
})
const vm = new Vue(App).$mount()

await sleep(100)
await vm.$nextTick()
expect(vm.$el.outerText === 'wangxiao')
expect(propsFromAttrs).toStrictEqual({
firstName: 'wang',
lastName: 'xiao',
})
})
})

0 comments on commit 90b086b

Please sign in to comment.