大家好,我卡颂。
要说React
有什么其他框架没有的、独一无二的特性,那一定是并发更新。围绕并发更新,存在两个很有意思的现象:
- 很多开发者听说过他
- 很少开发者直接使用过他
这两个现象看似矛盾,其实很好解释 —— React
18之后的新特性,主要是面向上层框架的(主要是Next.js
)。
换句话说,这些新特性(比如并发更新)主要是供框架集成,而不是开发者直接使用。
比如,并发更新的两个核心API
—— useTransition
和useDeferredValue
,都是针对视图切换的场景。
而在前端交互中,最主要的视图切换场景就是路由切换,所以包含路由功能的前端框架就会集成这两个API
。
而现在,一个试验性浏览器API
—— View Transitions API
将原生实现视图切换功能。
他到底有什么用?如果其他框架使用它,是不是能获得React
同样的并发更新能力?
欢迎围观朋友圈、加入人类高质量前端交流群,带飞
什么是视图切换?
不管是View Transitions API
,还是React
的useTransition
,都是为了提高视图切换场景下的用户体验。
什么是视图切换?以view-transitions Demo举例。这是个简单的相册Demo
,点击左边图片缩略图,右边会显示大图:
整个过程简单来说包括3个步骤:
- 点击缩略图
- 请求大图数据
- 大图请求成功后,显示大图
从步骤1到3的过程就是个典型的视图切换。整个过程有很多可以优化体验的地方,比如:
- 从旧图到新图的渐变过渡效果
- 点击缩略图发起图片请求后,大图区域可以先显示旧图(而不是立刻显示
loading
效果),待新图请求成功后再过渡到新图
这里解释下第二点,对于切换类的交互,相比于当视图切换时立刻显示loading效果,待新视图加载完成后过渡到新视图,当视图切换时先显示旧视图,待新视图加载完成后过渡到视图在延迟不高的情况下体验会更好。
除了上述这些体验优化的点,视图切换的实现还有很多细节需要考虑,比如:
- 如何处理新旧视图切换时的过渡效果?
- 如何处理新视图加载时的
loading
效果? - 当正在请求新视图数据时(此时视图处在旧视图中),用户又对旧视图产生交互怎么办?
- 视图切换时如何处理页面滚动位置、光标聚焦(
focus
)位置? - 对于使用屏幕阅读器的盲人,视图切换时阅读器会朗读什么?
除此之外,不同场景下的视图切换实现细节也不同。比如,如何在切换页面时优化视图切换效果?
在SPA
(单页应用)出现之前,网站通常是由多个页面组成。比如下面网站的每个Tab
栏,对应一个独立网页,其中:
bramus Tab
对应https://http203-playlist.netlify.app/with-bramus/
cassie Tab
对应https://http203-playlist.netlify.app/with-cassie/
在Tab
之间切换,浏览器会:
- 卸载之前的页面
- 请求新页面数据
- 加载新页面
从页面卸载到页面加载之间的白屏间隙会造成屏幕闪烁。
要优化这种场景下优化视图切换效果,当前唯一做法是利用history API
接管路由操作,将网页变成一个SPA
。
既然视图切换是如此常见的需求,且有这么多需要考虑的因素,那浏览器为什么不原生实现呢?
于是,View Transitions API
应运而生。
当前View Transitions API
不支持跨页面的视图切换,但未来会支持
View Transitions的使用
View Transitions API的使用很简单,只需要用document.startViewTransition
包裹视图切换后的回调函数即可。
对于上述相册示例,回调函数的逻辑是将img标签src属性更新为新图片地址:
const transition = document.startViewTransition(() => {
galleryImg.src = /* 新图片地址 */;
});
剩下所有跟过渡相关的实现,都由Transitions API
解决。
View Transitions实现原理
在视图切换时,存在2个概念:
- 切换前的旧视图
- 切换后的新视图
当使用View Transitions
后,会依次做:
- 对页面进行截图,作为旧视图
- 执行传递给
document.startViewTransition
的回调 DOM
更新后,对更新后的页面进行截图,作为新视图- 构造一棵代表过渡效果的伪元素树,挂载在根元素(
html
元素)下,结构类似如下:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
其中:
- 旧视图保存在
::view-transition-old(root)
中 - 新视图保存在
::view-transition-new(root)
中
对于上述相册示例,挂载的伪元素树结构如下:
之所以要挂载一棵伪元素树,主要是因为两个原因:
- 开发者可以对微元素应用
CSS
规则
比如,上述两个保存了新/旧视图的截图的伪元素,类似于img
标签,开发者可以对他们应用CSS动画
,当新/旧视图切换时,实现自定义的过渡效果。
- 方便对整个页面中不同视图切换分组
比如,在上述相册示例中,视图切换的元素包括两部分:
- 新/旧视图之间的切换(下图红框部分)
- 新/旧图片名称的切换(下图绿框部分)
相册对应的HTML
结构如下:
img
标签对应视图部分(下图红框部分)figcaption
标签对应图片名称部分(下图绿框部分)
当我们为figcaption
元素设置不同的view-transition-name
:
figcaption {
view-transition-name: figure-caption;
}
会得到一棵新的伪元素树,其中视图部分和图片名称部分伪元素是分离开的:
通过给页面中不同HTML
元素定义不同的view-transition-name
属性,就能独立控制这个元素是视图切换时的过渡效果。
与 React 的区别
浏览器原生的View Transitions API
与React
中的useTransition
相比,谁更强大呢?
毫无疑问,前者更强大。
这是因为,对于View Transitions API
,通过操作伪元素树,开发者可以自定义过渡效果(就像对img
元素使用CSS过渡动画
一样简单)。即使不使用CSS Transition
,使用JS Transition
也完全没问题。
document.startViewTransition
方法会返回一个transition
对象实例:
const transition = document.startViewTransition(() => {
galleryImg.src = /* 新图片地址 */;
});
该实例包含了一系列视图切换生命周期对应的promise
,比如:
ViewTransition.ready
:伪元素树构造完成,准备开始过渡时ViewTransition.finished
:过渡效果完成后,此时新视图已经可以响应用户交互
而在React
中,使用useTransition
后,与其说完成的是视图切换,不如说完成的是:
- 首先,完成状态的切换
React
内部再将状态变化映射到视图变化
本质来说,操作视图的是React
,而不是开发者。所以,开发者很难控制过渡效果。
动效库Framer
的作者认为,由于开发者很难控制并发更新的完整生命周期,所以很难在并发更新时表达animation
效果:
简单来说就是,开发者很难为并发更新定制过渡效果(用CSS
或JS
)。
总结
可以认为,View Transitions API
是比useTransition
抽象程度更高、开发者可控性更高的API
。useTransition
能实现的,他可以。useTransition
不能实现的,他也可以。
要说缺点,View Transitions API
是Web
平台独有的,而useTransition
依赖React
核心的并发更新能力,是跨端的。
当前,View Transitions API
的兼容性并不好:
但是,一旦他变成可以大规模使用的API
,那么其他前端框架只要接入他,就能轻松获得比React
耗费大量精力实现的useTransition
(以及底层的并发更新特性)更强大的视图切换能力。