前端知库

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,用户正在看,界面布局变了,要把它移到侧边栏:

JavaScript
sidebar.insertBefore(player, null);

视频会从头开始,因为 iframe 被移除后重新插入,浏览器会重新加载它的内容。MDN 列出了用 insertBefore 这类方法移动时会被重置、用 moveBefore 能保留的状态:

  • 动画和过渡的进度
  • <iframe> 的加载状态
  • 交互状态,比如 :focus 和 :active
  • 全屏元素的状态
  • popover 的打开状态
  • <dialog> 的模态状态,模态对话框不会被关闭

<video> 和 <audio> 的播放状态不在列表里,因为这两个元素被移除再插入时本来就会保留播放状态,不管用哪种方法移动。

改用 moveBefore

把上面那行换成 moveBefore,视频就能接着播放:

JavaScript
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(),浏览器只调用它,不再走断开和连接:

JavaScript
class VideoCard extends HTMLElement {
  connectedCallback() {
    this.startPlayer();
  }
  disconnectedCallback() {
    this.stopPlayer();
  }
  connectedMoveCallback() {
    // 只是换了位置,播放器不用停也不用重建
  }
}

MutationObserver 那边没有新的记录类型。用 moveBefore 移动的节点,仍然记录成一次移除加一次添加,所以已有的监听代码不用改。

常见追问

MutationObserver 能区分 moveBefore 和普通的移除插入吗?

不能。按 MDN 的说明,moveBefore 移动的节点同样记录成一次移除和一次添加。需要区分时,自定义元素可以用 connectedMoveCallback()。

iframe 还有哪些常见问题?

加载开销、跨域通信这些问题见《iframe 有哪些优缺点?父页面和 iframe 之间怎样通信?》。