diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index c9847c1ea..ce1a2f977 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,6 +1,6 @@ import { OperationTypes } from './operations' import { Dep, targetMap } from './reactive' -import { EMPTY_OBJ } from '@vue/shared' +import { EMPTY_OBJ, extend } from '@vue/shared' export interface ReactiveEffect { (): any @@ -203,7 +203,7 @@ function scheduleRun( ) { if (__DEV__ && effect.onTrigger) { effect.onTrigger( - Object.assign( + extend( { effect, target, diff --git a/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts index a150c58bd..711e8109e 100644 --- a/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/vdomAttrsFallthrough.spec.ts @@ -5,7 +5,8 @@ import { nextTick, mergeProps, ref, - onUpdated + onUpdated, + createComponent } from '@vue/runtime-dom' describe('attribute fallthrough', () => { @@ -74,154 +75,156 @@ describe('attribute fallthrough', () => { expect(node.style.fontWeight).toBe('bold') }) - // it('should separate in attrs when component has declared props', async () => { - // const click = jest.fn() - // const childUpdated = jest.fn() + it('should separate in attrs when component has declared props', async () => { + const click = jest.fn() + const childUpdated = jest.fn() - // class Hello extends Component { - // count = 0 - // inc() { - // this.count++ - // click() - // } - // render() { - // return h(Child, { - // foo: 123, - // id: 'test', - // class: 'c' + this.count, - // style: { color: this.count ? 'red' : 'green' }, - // onClick: this.inc - // }) - // } - // } + const Hello = { + setup() { + const count = ref(0) - // class Child extends Component<{ [key: string]: any; foo: number }> { - // static props = { - // foo: Number - // } - // updated() { - // childUpdated() - // } - // render() { - // return cloneVNode( - // h( - // 'div', - // { - // class: 'c2', - // style: { fontWeight: 'bold' } - // }, - // this.$props.foo - // ), - // this.$attrs - // ) - // } - // } + function inc() { + count.value++ + click() + } - // const root = document.createElement('div') - // document.body.appendChild(root) - // await render(h(Hello), root) + return () => + h(Child, { + foo: 1, + id: 'test', + class: 'c' + count.value, + style: { color: count.value ? 'red' : 'green' }, + onClick: inc + }) + } + } - // const node = root.children[0] as HTMLElement + const Child = createComponent({ + props: { + foo: Number + }, + setup(props, { attrs }) { + onUpdated(childUpdated) + return () => + h( + 'div', + mergeProps( + { + class: 'c2', + style: { fontWeight: 'bold' } + }, + attrs + ), + props.foo + ) + } + }) - // // with declared props, any parent attr that isn't a prop falls through - // expect(node.getAttribute('id')).toBe('test') - // expect(node.getAttribute('class')).toBe('c2 c0') - // expect(node.style.color).toBe('green') - // expect(node.style.fontWeight).toBe('bold') - // node.dispatchEvent(new CustomEvent('click')) - // expect(click).toHaveBeenCalled() + const root = document.createElement('div') + document.body.appendChild(root) + render(h(Hello), root) - // // ...while declared ones remain props - // expect(node.hasAttribute('foo')).toBe(false) + const node = root.children[0] as HTMLElement - // await nextTick() - // expect(childUpdated).toHaveBeenCalled() - // expect(node.getAttribute('id')).toBe('test') - // expect(node.getAttribute('class')).toBe('c2 c1') - // expect(node.style.color).toBe('red') - // expect(node.style.fontWeight).toBe('bold') + // with declared props, any parent attr that isn't a prop falls through + expect(node.getAttribute('id')).toBe('test') + expect(node.getAttribute('class')).toBe('c2 c0') + expect(node.style.color).toBe('green') + expect(node.style.fontWeight).toBe('bold') + node.dispatchEvent(new CustomEvent('click')) + expect(click).toHaveBeenCalled() - // expect(node.hasAttribute('foo')).toBe(false) - // }) + // ...while declared ones remain props + expect(node.hasAttribute('foo')).toBe(false) - // it('should fallthrough on multi-nested components', async () => { - // const click = jest.fn() - // const childUpdated = jest.fn() - // const grandChildUpdated = jest.fn() + await nextTick() + expect(childUpdated).toHaveBeenCalled() + expect(node.getAttribute('id')).toBe('test') + expect(node.getAttribute('class')).toBe('c2 c1') + expect(node.style.color).toBe('red') + expect(node.style.fontWeight).toBe('bold') - // class Hello extends Component { - // count = 0 - // inc() { - // this.count++ - // click() - // } - // render() { - // return h(Child, { - // foo: 1, - // id: 'test', - // class: 'c' + this.count, - // style: { color: this.count ? 'red' : 'green' }, - // onClick: this.inc - // }) - // } - // } + expect(node.hasAttribute('foo')).toBe(false) + }) - // class Child extends Component<{ [key: string]: any; foo: number }> { - // updated() { - // childUpdated() - // } - // render() { - // return h(GrandChild, this.$props) - // } - // } + it('should fallthrough on multi-nested components', async () => { + const click = jest.fn() + const childUpdated = jest.fn() + const grandChildUpdated = jest.fn() - // class GrandChild extends Component<{ [key: string]: any; foo: number }> { - // static props = { - // foo: Number - // } - // updated() { - // grandChildUpdated() - // } - // render(props: any) { - // return cloneVNode( - // h( - // 'div', - // { - // class: 'c2', - // style: { fontWeight: 'bold' } - // }, - // props.foo - // ), - // this.$attrs - // ) - // } - // } + const Hello = { + setup() { + const count = ref(0) - // const root = document.createElement('div') - // document.body.appendChild(root) - // await render(h(Hello), root) + function inc() { + count.value++ + click() + } - // const node = root.children[0] as HTMLElement + return () => + h(Child, { + foo: 1, + id: 'test', + class: 'c' + count.value, + style: { color: count.value ? 'red' : 'green' }, + onClick: inc + }) + } + } - // // with declared props, any parent attr that isn't a prop falls through - // expect(node.getAttribute('id')).toBe('test') - // expect(node.getAttribute('class')).toBe('c2 c0') - // expect(node.style.color).toBe('green') - // expect(node.style.fontWeight).toBe('bold') - // node.dispatchEvent(new CustomEvent('click')) - // expect(click).toHaveBeenCalled() + const Child = { + setup(props: any) { + onUpdated(childUpdated) + return () => h(GrandChild, props) + } + } - // // ...while declared ones remain props - // expect(node.hasAttribute('foo')).toBe(false) + const GrandChild = createComponent({ + props: { + foo: Number + }, + setup(props, { attrs }) { + onUpdated(grandChildUpdated) + return () => + h( + 'div', + mergeProps( + { + class: 'c2', + style: { fontWeight: 'bold' } + }, + attrs + ), + props.foo + ) + } + }) - // await nextTick() - // expect(childUpdated).toHaveBeenCalled() - // expect(grandChildUpdated).toHaveBeenCalled() - // expect(node.getAttribute('id')).toBe('test') - // expect(node.getAttribute('class')).toBe('c2 c1') - // expect(node.style.color).toBe('red') - // expect(node.style.fontWeight).toBe('bold') + const root = document.createElement('div') + document.body.appendChild(root) + render(h(Hello), root) - // expect(node.hasAttribute('foo')).toBe(false) - // }) + const node = root.children[0] as HTMLElement + + // with declared props, any parent attr that isn't a prop falls through + expect(node.getAttribute('id')).toBe('test') + expect(node.getAttribute('class')).toBe('c2 c0') + expect(node.style.color).toBe('green') + expect(node.style.fontWeight).toBe('bold') + node.dispatchEvent(new CustomEvent('click')) + expect(click).toHaveBeenCalled() + + // ...while declared ones remain props + expect(node.hasAttribute('foo')).toBe(false) + + await nextTick() + expect(childUpdated).toHaveBeenCalled() + expect(grandChildUpdated).toHaveBeenCalled() + expect(node.getAttribute('id')).toBe('test') + expect(node.getAttribute('class')).toBe('c2 c1') + expect(node.style.color).toBe('red') + expect(node.style.fontWeight).toBe('bold') + + expect(node.hasAttribute('foo')).toBe(false) + }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 9353681c8..2982bb4aa 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -262,12 +262,19 @@ export function setupStatefulComponent(instance: ComponentInstance) { } } +// used to identify a setup context proxy +export const SetupProxySymbol = Symbol() + const SetupProxyHandlers: { [key: string]: ProxyHandler } = {} ;['attrs', 'slots', 'refs'].forEach((type: string) => { SetupProxyHandlers[type] = { - get: (instance: any, key: string) => (instance[type] as any)[key], - has: (instance: any, key: string) => key in (instance[type] as any), - ownKeys: (instance: any) => Object.keys(instance[type] as any), + get: (instance, key) => (instance[type] as any)[key], + has: (instance, key) => + key === SetupProxySymbol || key in (instance[type] as any), + ownKeys: instance => Reflect.ownKeys(instance[type] as any), + // this is necessary for ownKeys to work properly + getOwnPropertyDescriptor: (instance, key) => + Reflect.getOwnPropertyDescriptor(instance[type], key), set: () => false, deleteProperty: () => false } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index f14ed425a..13021e783 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -1,9 +1,17 @@ -import { isArray, isFunction, isString, isObject, EMPTY_ARR } from '@vue/shared' -import { ComponentInstance, Data } from './component' +import { + isArray, + isFunction, + isString, + isObject, + EMPTY_ARR, + extend +} from '@vue/shared' +import { ComponentInstance, Data, SetupProxySymbol } from './component' import { HostNode } from './createRenderer' import { RawSlots } from './componentSlots' import { PatchFlags } from './patchFlags' import { ShapeFlags } from './shapeFlags' +import { isReactive } from '@vue/reactivity' export const Fragment = Symbol('Fragment') export const Text = Symbol('Text') @@ -100,6 +108,28 @@ export function createVNode( // Allow passing 0 for props, this can save bytes on generated code. props = props || null + // class & style normalization. + if (props !== null) { + // for reactive or proxy objects, we need to clone it to enable mutation. + if (isReactive(props) || SetupProxySymbol in props) { + props = extend({}, props) + } + // class normalization only needed if the vnode isn't generated by + // compiler-optimized code + if (props.class != null && !(patchFlag & PatchFlags.CLASS)) { + props.class = normalizeClass(props.class) + } + let { style } = props + if (style != null) { + // reactive state objects need to be cloned since they are likely to be + // mutated + if (isReactive(style) && !isArray(style)) { + style = extend({}, style) + } + props.style = normalizeStyle(style) + } + } + // encode the vnode type information into a bitmap const shapeFlag = isString(type) ? ShapeFlags.ELEMENT @@ -127,18 +157,6 @@ export function createVNode( normalizeChildren(vnode, children) - // class & style normalization. - if (props !== null) { - // class normalization only needed if the vnode isn't generated by - // compiler-optimized code - if (props.class != null && !(patchFlag & PatchFlags.CLASS)) { - props.class = normalizeClass(props.class) - } - if (props.style != null) { - props.style = normalizeStyle(props.style) - } - } - // presence of a patch flag indicates this node is dynamic // component nodes also should always be tracked, because even if the // component doesn't need to update, it needs to persist the instance on to @@ -257,9 +275,7 @@ const handlersRE = /^on|^vnode/ export function mergeProps(...args: Data[]) { const ret: Data = {} - for (const key in args[0]) { - ret[key] = args[0][key] - } + extend(ret, args[0]) for (let i = 1; i < args.length; i++) { const toMerge = args[i] for (const key in toMerge) { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 3e670db31..9bace428c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -7,6 +7,16 @@ export const reservedPropRE = /^(?:key|ref|slots)$|^vnode/ export const isOn = (key: string) => key[0] === 'o' && key[1] === 'n' +export const extend = ( + a: T, + b: U +): T & U => { + for (const key in b) { + ;(a as any)[key] = b[key] + } + return a as any +} + export const isArray = Array.isArray export const isFunction = (val: any): val is Function => typeof val === 'function'