1. Do not use virtual parent optimization if the mutation targets have iframe elements as children. This will cause some performance regression but will be easy to add and ship.
2. If an iframe element has already been a child of a virtual parent, add the virtual parent back to the dom.
This commit is contained in:
Lucky Feng
2026-04-01 12:00:00 +08:00
committed by GitHub
parent f0c2258371
commit 424044ede3
2 changed files with 54 additions and 19 deletions

View File

@@ -166,23 +166,9 @@ export class Replayer {
this.emitter.on(ReplayerEvents.Flush, () => {
const { scrollMap, inputMap } = this.treeIndex.flush();
for (const [frag, parent] of this.fragmentParentMap.entries()) {
mirror.map[parent.__sn.id] = parent;
/**
* If we have already set value attribute on textarea,
* then we could not apply text content as default value any more.
*/
if (
parent.__sn.type === NodeType.Element &&
parent.__sn.tagName === 'textarea' &&
frag.textContent
) {
((parent as unknown) as HTMLTextAreaElement).value = frag.textContent;
}
parent.appendChild(frag);
// restore state of elements after they are mounted
this.restoreState(parent);
}
this.fragmentParentMap.forEach((parent, frag) =>
this.restoreRealParent(frag, parent),
);
this.fragmentParentMap.clear();
this.elementStateMap.clear();
@@ -627,6 +613,20 @@ export class Replayer {
iframeEl: HTMLIFrameElement,
) {
const collected: AppendedIframe[] = [];
// If iframeEl is detached from dom, iframeEl.contentDocument is null.
if (!iframeEl.contentDocument) {
let parent = iframeEl.parentNode;
while (parent) {
// The parent of iframeEl is virtual parent and we need to mount it on the dom.
if (this.fragmentParentMap.has((parent as unknown) as INode)) {
const frag = (parent as unknown) as INode;
const realParent = this.fragmentParentMap.get(frag)!;
this.restoreRealParent(frag, realParent);
break;
}
parent = parent.parentNode;
}
}
buildNodeWithSN(mutation.node, {
doc: iframeEl.contentDocument!,
map: mirror.map,
@@ -1139,8 +1139,19 @@ export class Replayer {
parentInDocument = this.iframe.contentDocument.body.contains(parent);
}
// if parent element is an iframe, iframe document can't be appended to virtual parent
if (useVirtualParent && parentInDocument && !isIframeINode(parent)) {
const hasIframeChild =
((parent as unknown) as HTMLElement).getElementsByTagName?.('iframe')
.length > 0;
/**
* Why !isIframeINode(parent)? If parent element is an iframe, iframe document can't be appended to virtual parent.
* Why !hasIframeChild? If we move iframe elements from dom to fragment document, we will lose the contentDocument of iframe. So we need to disable the virtual dom optimization if a parent node contains iframe elements.
*/
if (
useVirtualParent &&
parentInDocument &&
!isIframeINode(parent) &&
!hasIframeChild
) {
const virtualParent = (document.createDocumentFragment() as unknown) as INode;
mirror.map[mutation.parentId] = virtualParent;
this.fragmentParentMap.set(virtualParent, parent);
@@ -1529,6 +1540,29 @@ export class Replayer {
});
}
/**
* Replace the virtual parent with the real parent.
* @param frag fragment document, the virtual parent
* @param parent real parent element
*/
private restoreRealParent(frag: INode, parent: INode) {
mirror.map[parent.__sn.id] = parent;
/**
* If we have already set value attribute on textarea,
* then we could not apply text content as default value any more.
*/
if (
parent.__sn.type === NodeType.Element &&
parent.__sn.tagName === 'textarea' &&
frag.textContent
) {
((parent as unknown) as HTMLTextAreaElement).value = frag.textContent;
}
parent.appendChild(frag);
// restore state of elements after they are mounted
this.restoreState(parent);
}
/**
* store state of elements before unmounted from dom recursively
* the state should be restored in the handler of event ReplayerEvents.Flush

View File

@@ -55,6 +55,7 @@ export declare class Replayer {
private hoverElements;
private isUserInteraction;
private backToNormal;
private restoreRealParent;
private storeState;
private restoreState;
private warnNodeNotFound;