mirror of https://github.com/vuejs/core.git
fix(custom-element): batch custom element prop patching (#13478)
close #12619
This commit is contained in:
parent
1df8990504
commit
c13e674fb9
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -499,6 +499,190 @@ describe('defineCustomElement', () => {
|
|||
'<div><span>1 is number</span><span>true is boolean</span></div>',
|
||||
)
|
||||
})
|
||||
|
||||
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(
|
||||
`<div><h1>default1</h1><h1>default2</h1><h2>defaultData1</h2><h2>defaultData2</h2></div>`,
|
||||
)
|
||||
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(
|
||||
`<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
|
||||
)
|
||||
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(
|
||||
`<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
|
||||
)
|
||||
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(
|
||||
`<div><h1>newValue3</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
|
||||
)
|
||||
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(
|
||||
`<div><h1>default1</h1><h1>default2</h1><h2>defaultData1</h2><h2>defaultData2</h2></div>`,
|
||||
)
|
||||
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(
|
||||
`<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
|
||||
)
|
||||
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(
|
||||
`<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
|
||||
)
|
||||
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(
|
||||
`<div><h1>newValue3</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
|
||||
)
|
||||
expect(prop1Calls).toBe(2)
|
||||
expect(prop2Calls).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('attrs', () => {
|
||||
|
|
|
|||
|
|
@ -229,6 +229,8 @@ export class VueElement
|
|||
|
||||
private _connected = false
|
||||
private _resolved = false
|
||||
private _patching = false
|
||||
private _dirty = false
|
||||
private _numberProps: Record<string, true> | null = null
|
||||
private _styleChildren = new WeakSet()
|
||||
private _pendingResolve: Promise<void> | undefined
|
||||
|
|
@ -468,11 +470,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)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -506,6 +508,7 @@ export class VueElement
|
|||
shouldUpdate = false,
|
||||
): void {
|
||||
if (val !== this._props[key]) {
|
||||
this._dirty = true
|
||||
if (val === REMOVAL) {
|
||||
delete this._props[key]
|
||||
} else {
|
||||
|
|
@ -697,6 +700,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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue