diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts
index f191c36df12..ea9b43bc324 100644
--- a/packages/runtime-core/src/component.ts
+++ b/packages/runtime-core/src/component.ts
@@ -1270,6 +1270,14 @@ export interface ComponentCustomElementInterface {
shouldReflect?: boolean,
shouldUpdate?: boolean,
): void
+ /**
+ * @internal
+ */
+ _beginPatch(): void
+ /**
+ * @internal
+ */
+ _endPatch(): void
/**
* @internal attached by the nested Teleport when shadowRoot is false.
*/
diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts
index a57be791a44..4e1c91b20d3 100644
--- a/packages/runtime-core/src/renderer.ts
+++ b/packages/runtime-core/src/renderer.ts
@@ -621,15 +621,27 @@ function baseCreateRenderer(
optimized,
)
} else {
- patchElement(
- n1,
- n2,
- parentComponent,
- parentSuspense,
- namespace,
- slotScopeIds,
- optimized,
- )
+ const customElement = !!(n1.el && (n1.el as VueElement)._isVueCE)
+ ? (n1.el as VueElement)
+ : null
+ try {
+ if (customElement) {
+ customElement._beginPatch()
+ }
+ patchElement(
+ n1,
+ n2,
+ parentComponent,
+ parentSuspense,
+ namespace,
+ slotScopeIds,
+ optimized,
+ )
+ } finally {
+ if (customElement) {
+ customElement._endPatch()
+ }
+ }
}
}
diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts
index c44840df5e3..fe7aef1d015 100644
--- a/packages/runtime-dom/__tests__/customElement.spec.ts
+++ b/packages/runtime-dom/__tests__/customElement.spec.ts
@@ -474,6 +474,190 @@ describe('defineCustomElement', () => {
'
1 is numbertrue is boolean
',
)
})
+
+ test('should patch all props together', async () => {
+ let prop1Calls = 0
+ let prop2Calls = 0
+ const E = defineCustomElement({
+ props: {
+ prop1: {
+ type: String,
+ default: 'default1',
+ },
+ prop2: {
+ type: String,
+ default: 'default2',
+ },
+ },
+ data() {
+ return {
+ data1: 'defaultData1',
+ data2: 'defaultData2',
+ }
+ },
+ watch: {
+ prop1(_) {
+ prop1Calls++
+ this.data2 = this.prop2
+ },
+ prop2(_) {
+ prop2Calls++
+ this.data1 = this.prop1
+ },
+ },
+ render() {
+ return h('div', [
+ h('h1', this.prop1),
+ h('h1', this.prop2),
+ h('h2', this.data1),
+ h('h2', this.data2),
+ ])
+ },
+ })
+ customElements.define('my-watch-element', E)
+
+ render(h('my-watch-element'), container)
+ const e = container.childNodes[0] as VueElement
+ expect(e).toBeInstanceOf(E)
+ expect(e._instance).toBeTruthy()
+ expect(e.shadowRoot!.innerHTML).toBe(
+ `default1
default2
defaultData1
defaultData2
`,
+ )
+ expect(prop1Calls).toBe(0)
+ expect(prop2Calls).toBe(0)
+
+ // patch props
+ render(
+ h('my-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
+ container,
+ )
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(
+ `newValue1
newValue2
newValue1
newValue2
`,
+ )
+ expect(prop1Calls).toBe(1)
+ expect(prop2Calls).toBe(1)
+
+ // same prop values
+ render(
+ h('my-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
+ container,
+ )
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(
+ `newValue1
newValue2
newValue1
newValue2
`,
+ )
+ expect(prop1Calls).toBe(1)
+ expect(prop2Calls).toBe(1)
+
+ // update only prop1
+ render(
+ h('my-watch-element', { prop1: 'newValue3', prop2: 'newValue2' }),
+ container,
+ )
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(
+ `newValue3
newValue2
newValue1
newValue2
`,
+ )
+ expect(prop1Calls).toBe(2)
+ expect(prop2Calls).toBe(1)
+ })
+
+ test('should patch all props together (async)', async () => {
+ let prop1Calls = 0
+ let prop2Calls = 0
+ const E = defineCustomElement(
+ defineAsyncComponent(() =>
+ Promise.resolve(
+ defineComponent({
+ props: {
+ prop1: {
+ type: String,
+ default: 'default1',
+ },
+ prop2: {
+ type: String,
+ default: 'default2',
+ },
+ },
+ data() {
+ return {
+ data1: 'defaultData1',
+ data2: 'defaultData2',
+ }
+ },
+ watch: {
+ prop1(_) {
+ prop1Calls++
+ this.data2 = this.prop2
+ },
+ prop2(_) {
+ prop2Calls++
+ this.data1 = this.prop1
+ },
+ },
+ render() {
+ return h('div', [
+ h('h1', this.prop1),
+ h('h1', this.prop2),
+ h('h2', this.data1),
+ h('h2', this.data2),
+ ])
+ },
+ }),
+ ),
+ ),
+ )
+ customElements.define('my-async-watch-element', E)
+
+ render(h('my-async-watch-element'), container)
+
+ await new Promise(r => setTimeout(r))
+ const e = container.childNodes[0] as VueElement
+ expect(e).toBeInstanceOf(E)
+ expect(e._instance).toBeTruthy()
+ expect(e.shadowRoot!.innerHTML).toBe(
+ `default1
default2
defaultData1
defaultData2
`,
+ )
+ expect(prop1Calls).toBe(0)
+ expect(prop2Calls).toBe(0)
+
+ // patch props
+ render(
+ h('my-async-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
+ container,
+ )
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(
+ `newValue1
newValue2
newValue1
newValue2
`,
+ )
+ expect(prop1Calls).toBe(1)
+ expect(prop2Calls).toBe(1)
+
+ // same prop values
+ render(
+ h('my-async-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
+ container,
+ )
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(
+ `newValue1
newValue2
newValue1
newValue2
`,
+ )
+ expect(prop1Calls).toBe(1)
+ expect(prop2Calls).toBe(1)
+
+ // update only prop1
+ render(
+ h('my-async-watch-element', { prop1: 'newValue3', prop2: 'newValue2' }),
+ container,
+ )
+ await nextTick()
+ expect(e.shadowRoot!.innerHTML).toBe(
+ `newValue3
newValue2
newValue1
newValue2
`,
+ )
+ expect(prop1Calls).toBe(2)
+ expect(prop2Calls).toBe(1)
+ })
})
describe('attrs', () => {
diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts
index edf7c431353..9cddccb4e46 100644
--- a/packages/runtime-dom/src/apiCustomElement.ts
+++ b/packages/runtime-dom/src/apiCustomElement.ts
@@ -228,6 +228,8 @@ export class VueElement
private _connected = false
private _resolved = false
+ private _patching = false
+ private _dirty = false
private _numberProps: Record | null = null
private _styleChildren = new WeakSet()
private _pendingResolve: Promise | undefined
@@ -457,11 +459,11 @@ export class VueElement
// defining getter/setters on prototype
for (const key of declaredPropKeys.map(camelize)) {
Object.defineProperty(this, key, {
- get() {
+ get(this: VueElement) {
return this._getProp(key)
},
- set(val) {
- this._setProp(key, val, true, true)
+ set(this: VueElement, val) {
+ this._setProp(key, val, true, !this._patching)
},
})
}
@@ -495,6 +497,7 @@ export class VueElement
shouldUpdate = false,
): void {
if (val !== this._props[key]) {
+ this._dirty = true
if (val === REMOVAL) {
delete this._props[key]
} else {
@@ -670,6 +673,24 @@ export class VueElement
this._applyStyles(comp.styles, comp)
}
+ /**
+ * @internal
+ */
+ _beginPatch(): void {
+ this._patching = true
+ this._dirty = false
+ }
+
+ /**
+ * @internal
+ */
+ _endPatch(): void {
+ this._patching = false
+ if (this._dirty && this._instance) {
+ this._update()
+ }
+ }
+
/**
* @internal
*/