登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

View Transition API 如何为列表重排添加过渡

来源:17golang原创

时间:2026-10-09 01:56:05 158浏览 收藏

热门推荐
漫画APP
动画内容聚合,热门资源快捷查看
立即下载

列表排序、筛选和拖动重排最容易出现的体验问题,是数据已经更新,但卡片瞬间跳到新位置。View Transition API 可以把“更新前的视觉状态”和“更新后的视觉状态”交给浏览器匹配,让同一个列表项从旧位置平滑移动到新位置。实现重点并不是堆叠动画库,而是保证列表项具有稳定身份,并把真正的 DOM 重排放进 document.startViewTransition() 的更新回调。

适用范围:本文讨论单页内的同文档列表重排;跨页面导航使用的是另一套启用方式。浏览器不支持 API、页面处于隐藏状态或用户开启“减少动态效果”时,列表仍应立即完成排序,不能把过渡当成业务功能的前置条件。

明确列表重排的目标和边界

一个可复用的重排方案需要同时满足四个条件:数据顺序正确、DOM 节点与数据项一一对应、过渡不可用时功能仍然成立、动画不会盖住真实交互反馈。View Transition API 只负责视觉状态之间的过渡,不替代排序算法,也不替代列表的状态管理。

如果只把整个页面交给默认过渡,浏览器通常会对根视图做整体淡入淡出。要得到“每张卡片移动到新位置”的效果,就必须让每个列表项成为独立的过渡参与者。现代浏览器可以通过 view-transition-name: match-element 为同文档中的元素建立独立身份;需要精细控制时,也可以为元素分配稳定且唯一的自定义名称。

理解同文档过渡中的对象关系

调用 document.startViewTransition(update) 后,浏览器会围绕更新回调管理旧视图快照、新视图快照和对应的过渡伪元素。开发者负责在回调里同步更新 DOM,浏览器负责把匹配元素的旧状态与新状态放入 ::view-transition-group()、::view-transition-old() 和 ::view-transition-new() 等伪元素树中。

View Transition API 调用入口、DOM 更新、视图快照与过渡伪元素的静态对象关系图
图1:同文档过渡的对象关系。连线表示参与和归属关系,不表示执行时序。

这套机制的价值在于,业务代码仍然只做一次正常的 DOM 更新。无需手工读取每张卡片的旧坐标、计算位移再回写 transform;浏览器会根据匹配到的视觉状态生成过渡。若更新回调抛错,或者参与过渡的自定义名称发生冲突,过渡会被跳过,但业务代码仍应保持可恢复。

给每个列表项稳定身份

列表重排能否产生位移动画,取决于浏览器能否认出“更新前的卡片 A”和“更新后的卡片 A”是同一个对象。最简洁的 CSS 是:

/* 为同文档中的每张卡片自动建立独立的过渡身份 */
.task-card {
  view-transition-name: match-element;
}

match-element 依据渲染元素自身的身份进行匹配,因此重排时应移动现有 DOM 节点,而不是先清空列表,再为同一批数据重新创建全部节点。后者虽然最终内容相同,但元素身份已经变化,浏览器无法稳定匹配。

数据 ID、稳定 DOM 节点、match-element 与列表卡片之间的静态身份关系图
图2:稳定身份关系。数据 ID 管理业务对象,持久 DOM 节点保持元素身份,match-element 为每张卡片建立独立过渡名称。

如果项目需要针对某个列表项选择伪元素,可以改用自定义名称,例如 task-42。此时要保证当前渲染树中每个名称唯一;重复名称会使过渡准备阶段失败。对于普通排序场景,优先使用 match-element,可以少维护一层 CSS 标识。

把 DOM 重排放进 startViewTransition

下面的示例用一个 Map 保存数据 ID 与 DOM 节点的映射。第一次创建节点后,后续渲染只把同一批节点追加到新的顺序中;DOM 的 append() 会移动已有节点,不会复制它们。

// 示例数据:id 必须稳定,排序字段可以变化
const tasks = [
  { id: "a", title: "整理需求", priority: 2 },
  { id: "b", title: "实现列表", priority: 1 },
  { id: "c", title: "补充测试", priority: 3 }
];

const list = document.querySelector("#task-list");

// 每个数据项只创建一次 DOM 节点,后续重排继续复用
const taskNodes = new Map(
  tasks.map((task) => {
    const card = document.createElement("li");
    card.className = "task-card";
    card.dataset.id = task.id;
    card.textContent = `${task.title} · P${task.priority}`;
    return [task.id, card];
  })
);

function renderByOrder(ids) {
  // DocumentFragment 用于一次性移动现有节点,避免逐项重绘
  const fragment = document.createDocumentFragment();
  ids.forEach((id) => fragment.append(taskNodes.get(id)));
  list.replaceChildren(fragment);
}

// 首次渲染不需要过渡
renderByOrder(tasks.map((task) => task.id));

排序按钮只负责计算下一组 ID 顺序。真正更新 DOM 的函数既可以直接调用,也可以作为 View Transition 的回调:

// 用户主动减少动画时,直接完成排序,避免强制播放位移动效
const reduceMotion = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
);

function reorder(nextIds) {
  const updateDOM = () => renderByOrder(nextIds);

  // API 不可用或用户偏好减少动态效果时,保留完整功能回退
  if (!document.startViewTransition || reduceMotion.matches) {
    updateDOM();
    return;
  }

  // 浏览器在回调前后捕获状态,并匹配每张稳定卡片
  document.startViewTransition(updateDOM);
}

document.querySelector("#sort-priority").addEventListener("click", () => {
  // 生成新顺序,但不重新创建任务对象和卡片节点
  const nextIds = [...tasks]
    .sort((left, right) => left.priority - right.priority)
    .map((task) => task.id);

  reorder(nextIds);
});

排序逻辑最好在调用过渡之前完成,更新回调只承担确定性的 DOM 变更。这样可以缩短快照之间的等待,也便于在 API 不可用时复用同一个 updateDOM。若必须在更新前请求远程数据,应先等待数据就绪,再启动过渡,而不是在更新回调中放入不可控的长耗时请求。

定制位移动画与无障碍回退

列表项的几何变化由过渡组承载,因此可以统一调整持续时间和缓动。不要把持续时间拉得太长;排序是高频操作,约 200~350 毫秒通常更容易让用户看清位置变化,又不会阻塞下一次操作。

/* 统一设置所有命名过渡组的节奏,让位置变化更平滑 */
::view-transition-group(*) {
  animation-duration: 320ms;
  animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}

/* 尊重系统的减少动态效果设置,把动画压缩为近乎即时 */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) {
    animation-duration: 1ms;
  }
}

如果重排同时包含新增和删除,已有卡片可以继续通过稳定身份移动;新增项只有新视图,删除项只有旧视图。此时再分别为 ::view-transition-new() 与 ::view-transition-old() 定制淡入淡出即可。先把纯重排做稳定,再扩展进入和离开动画,排错会更简单。

常见误区

  • 每次排序都用 innerHTML 重建列表:数据 ID 没变,但 DOM 元素已经变成新对象,match-element 无法延续身份。应缓存节点或使用能保留 keyed 节点的框架渲染。
  • 给多张卡片写同一个自定义名称:同一过渡中名称必须唯一。若需要统一样式,使用通配伪元素选择器,而不是复用名称。
  • 只写 startViewTransition,不给子项命名:通常只会看到整个根视图交叉淡化,不会得到独立卡片位移。
  • 把过渡成功当成业务成功:过渡可能因页面不可见、样式冲突或平台能力被跳过。数据和 DOM 更新必须独立正确。
  • 把跨文档配置混入同文档排序:@view-transition { navigation: auto; } 面向页面导航;单页列表重排只需要 JavaScript 启动同文档过渡。

实施速查表

检查点推荐做法失败表现
业务身份每条数据具有稳定 ID排序后内容与节点对应错误
DOM 身份移动已有节点,不整批重建卡片只淡入淡出,不产生位移
过渡命名优先 match-element,自定义名称必须唯一准备阶段跳过过渡
更新边界回调只做确定性的 DOM 变更快照等待过长或动画不连贯
能力回退检测 document.startViewTransition旧浏览器无法排序
无障碍尊重 prefers-reduced-motion用户被强制观看高频动画

官方参考

  • MDN View Transition API 使用指南:https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using
  • MDN Document.startViewTransition():https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition
  • MDN view-transition-name:https://developer.mozilla.org/en-US/docs/Web/CSS/view-transition-name

把方案压缩成一句话:稳定数据 ID,复用同一批 DOM 节点,用 match-element 建立视觉身份,再把一次确定性的重排交给 startViewTransition。只要功能回退始终成立,过渡就能作为渐进增强安全落地。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>