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,
|
shouldReflect?: boolean,
|
||||||
shouldUpdate?: boolean,
|
shouldUpdate?: boolean,
|
||||||
): void
|
): void
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_beginPatch(): void
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_endPatch(): void
|
||||||
/**
|
/**
|
||||||
* @internal attached by the nested Teleport when shadowRoot is false.
|
* @internal attached by the nested Teleport when shadowRoot is false.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -621,15 +621,27 @@ function baseCreateRenderer(
|
||||||
optimized,
|
optimized,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
patchElement(
|
const customElement = !!(n1.el && (n1.el as VueElement)._isVueCE)
|
||||||
n1,
|
? (n1.el as VueElement)
|
||||||
n2,
|
: null
|
||||||
parentComponent,
|
try {
|
||||||
parentSuspense,
|
if (customElement) {
|
||||||
namespace,
|
customElement._beginPatch()
|
||||||
slotScopeIds,
|
}
|
||||||
optimized,
|
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>',
|
'<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', () => {
|
describe('attrs', () => {
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,8 @@ export class VueElement
|
||||||
|
|
||||||
private _connected = false
|
private _connected = false
|
||||||
private _resolved = false
|
private _resolved = false
|
||||||
|
private _patching = false
|
||||||
|
private _dirty = false
|
||||||
private _numberProps: Record<string, true> | null = null
|
private _numberProps: Record<string, true> | null = null
|
||||||
private _styleChildren = new WeakSet()
|
private _styleChildren = new WeakSet()
|
||||||
private _pendingResolve: Promise<void> | undefined
|
private _pendingResolve: Promise<void> | undefined
|
||||||
|
|
@ -468,11 +470,11 @@ export class VueElement
|
||||||
// defining getter/setters on prototype
|
// defining getter/setters on prototype
|
||||||
for (const key of declaredPropKeys.map(camelize)) {
|
for (const key of declaredPropKeys.map(camelize)) {
|
||||||
Object.defineProperty(this, key, {
|
Object.defineProperty(this, key, {
|
||||||
get() {
|
get(this: VueElement) {
|
||||||
return this._getProp(key)
|
return this._getProp(key)
|
||||||
},
|
},
|
||||||
set(val) {
|
set(this: VueElement, val) {
|
||||||
this._setProp(key, val, true, true)
|
this._setProp(key, val, true, !this._patching)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -506,6 +508,7 @@ export class VueElement
|
||||||
shouldUpdate = false,
|
shouldUpdate = false,
|
||||||
): void {
|
): void {
|
||||||
if (val !== this._props[key]) {
|
if (val !== this._props[key]) {
|
||||||
|
this._dirty = true
|
||||||
if (val === REMOVAL) {
|
if (val === REMOVAL) {
|
||||||
delete this._props[key]
|
delete this._props[key]
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -697,6 +700,24 @@ export class VueElement
|
||||||
this._applyStyles(comp.styles, comp)
|
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
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue