moveBefore() 和 insertBefore 有什么区别?为什么能保留 iframe 和焦点状态?
insertBefore 移动一个已在页面上的节点时,实际是先移除再插入,iframe 会重新加载,焦点、动画、打开的 popover 和模态 dialog 都会被重置。moveBefore 把节点原地移过去,不经过移除和插入,所以这些状态都能保留。它要求节点和目标在同一个文档里、都连接在页面上,否则抛错,需要回退到 insertBefore。
简答
parent.moveBefore(node, ref)把node移动到parent里、ref前面,ref传null就放到最后。参数和insertBefore一样,区别在于它不会先把节点移除再插入。用
insertBefore、append移动一个已经在页面上的节点,浏览器内部是先移除、再插入,节点的一部分状态会被重置。最明显的是 iframe 会重新加载,正在播放的嵌入视频回到开头。焦点也会丢,CSS 动画和过渡重新开始,打开的 popover 被关掉,模态 dialog 也会关闭。
moveBefore是真正的移动,这些状态都能保留。所以拖拽排序、把播放器挪到另一个区域、框架在列表重排时移动 DOM,都可以用它避免状态丢失。自定义元素用它移动时,会调用connectedMoveCallback(),而不是先后调用disconnectedCallback()和connectedCallback()。要注意它有限制。节点和目标必须在同一个文档里,并且要么都连接在页面上、要么都不在,否则抛出
HierarchyRequestError。兼容性上 Chrome 133、Firefox 144 起支持,Safari 还不支持。所以要用try...catch回退到insertBefore。
用 insertBefore 移动会丢什么
假设页面上有一个嵌入视频的 iframe,用户正在看,界面布局变了,要把它移到侧边栏:
sidebar.insertBefore(player, null);
视频会从头开始,因为 iframe 被移除后重新插入,浏览器会重新加载它的内容。MDN 列出了用 insertBefore 这类方法移动时会被重置、用 moveBefore 能保留的状态:
- 动画和过渡的进度
<iframe>的加载状态- 交互状态,比如
:focus和:active - 全屏元素的状态
- popover 的打开状态
<dialog>的模态状态,模态对话框不会被关闭
<video> 和 <audio> 的播放状态不在列表里,因为这两个元素被移除再插入时本来就会保留播放状态,不管用哪种方法移动。
改用 moveBefore
把上面那行换成 moveBefore,视频就能接着播放:
function moveNode(parent, node, ref) {
try {
parent.moveBefore(node, ref);
} catch {
parent.insertBefore(node, ref);
}
}
moveNode(sidebar, player, null);
为什么要包一层 try...catch?一方面不支持的浏览器里没有这个方法,调用会抛 TypeError;另一方面就算支持,下面几种情况也会抛 HierarchyRequestError:
- 节点不在页面上,却要移到页面上的元素里,或者反过来
- 节点是调用者的祖先
- 节点和目标属于不同的文档
- 节点不是
Element或CharacterData节点
另外,ref 不是调用者的子节点时抛 NotFoundError,第二个参数没传时抛 TypeError。回退到 insertBefore 就是接受状态被重置,至少位置能移过去。
自定义元素和 MutationObserver 能感知到吗
自定义元素用 insertBefore 移动时,会先调用 disconnectedCallback() 再调用 connectedCallback(),组件可能会在这两步里做清理和重新初始化。用 moveBefore 移动时,如果元素定义了 connectedMoveCallback(),浏览器只调用它,不再走断开和连接:
class VideoCard extends HTMLElement {
connectedCallback() {
this.startPlayer();
}
disconnectedCallback() {
this.stopPlayer();
}
connectedMoveCallback() {
// 只是换了位置,播放器不用停也不用重建
}
}
MutationObserver 那边没有新的记录类型。用 moveBefore 移动的节点,仍然记录成一次移除加一次添加,所以已有的监听代码不用改。
常见追问
MutationObserver 能区分 moveBefore 和普通的移除插入吗?
不能。按 MDN 的说明,
moveBefore移动的节点同样记录成一次移除和一次添加。需要区分时,自定义元素可以用connectedMoveCallback()。
iframe 还有哪些常见问题?
加载开销、跨域通信这些问题见《iframe 有哪些优缺点?父页面和 iframe 之间怎样通信?》。