From 90b086bb9dddbb34d110310272982fc164c471fd Mon Sep 17 00:00:00 2001 From: edwardnie <31278178+edwardnyc@users.noreply.github.com> Date: Thu, 28 Oct 2021 22:47:33 +0800 Subject: [PATCH] fix: attrs update not correctly mapped to props #833 (#835) Co-authored-by: neiyichao03 --- src/mixin.ts | 37 ++++------------------ src/utils/instance.ts | 38 +++++++++++++++++++++++ src/utils/vmStateManager.ts | 1 + test/setup.spec.js | 62 +++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 31 deletions(-) diff --git a/src/mixin.ts b/src/mixin.ts index d94c9be2..e560796c 100644 --- a/src/mixin.ts +++ b/src/mixin.ts @@ -18,13 +18,14 @@ 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({ @@ -32,6 +33,9 @@ export function mixin(Vue: VueConstructor) { mounted(this: ComponentInstance) { updateTemplateRef(this) }, + beforeUpdate() { + updateVmAttrs(this as ComponentInstance, this as SetupContext) + }, updated(this: ComponentInstance) { updateTemplateRef(this) if (this.$vnode?.context) { @@ -212,7 +216,6 @@ export function mixin(Vue: VueConstructor) { 'isServer', 'ssrContext', ] - const propsReactiveProxy = ['attrs'] const methodReturnVoid = ['emit'] propsPlain.forEach((key) => { @@ -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}` diff --git a/src/utils/instance.ts b/src/utils/instance.ts index 8a294fd5..63540a1f 100644 --- a/src/utils/instance.ts +++ b/src/utils/instance.ts @@ -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, @@ -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 diff --git a/src/utils/vmStateManager.ts b/src/utils/vmStateManager.ts index 6a2e2875..5a9d80f4 100644 --- a/src/utils/vmStateManager.ts +++ b/src/utils/vmStateManager.ts @@ -3,6 +3,7 @@ import { ComponentInstance, Data } from '../component' export interface VfaState { refs?: string[] rawBindings?: Data + attrBindings?: Data slots?: string[] } diff --git a/test/setup.spec.js b/test/setup.spec.js index 64f457d5..5ae0e6be 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -11,6 +11,7 @@ const { toRaw, nextTick, isReactive, + watchEffect, defineComponent, onMounted, set, @@ -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', + }) + }) })