framesync.js 是什么?
约定:
- 前一帧、当前帧、下一帧:指代 上一个 requestAnimationFrame,当前待执行的 requestAnimationFrame,与下一个 requestAnimationFrame。
framesync.js 是对浏览器原生 requestAnimationFrame 函数的封装,它作为 Popmotion.js 动画库的一部分,作为一个独立包分发。
它是一个很简单的库,它暴露出来的接口只有下面的几个:
js
import sync,{flushSync,cancelSync,getFrameData} from "framesync"
这些接口本质上是一些提供订阅监听的对象:
- sync:用来添加订阅
- cancelSync:用来取消订阅
- flushSync:用来发布订阅
这些对象都具有五个属性,分别是:read、update、preRender、render、postRender。它们的类型如下:
ts
type T = {
read:function,
update:function,
preRender:function,
render:function,
postRender:function
}
read、update、preRender、render、postRender,本质上是一组按顺序执行的生命周期函数,sync.read 先于 sync.update 之前执行。
这些接口实际上实现的是一个订阅发布模式,不过它与 requestAnimationFrame 关联到一起了,在当前帧的第一次调用 sync.xxx 进行订阅的时候,它会在 requestAnimationFrame 上添加发布函数的回调。由此,sync.xxx 的调用等同于 requestAnimationFrame 的调用。
目前为止,它解决了直接使用 requestAnimationFrame 的一个痛点(执行的顺序是按照其调用顺序,无法在中途插入一个优先级更高的监听),你可以通过在不同的生命周期中添加监听来处理优先级的问题,而无需关心调用顺序。
实际上,因为它也算是使用了适配器模式,sync.xxx 在内部对 requestAnimationFrame 是否存在进行了检测,如果不存在它会使用 setTimeout。也即,解决了 requestAnimationFrame 的另一个痛点:可能存在的兼容性问题。
原生的 requestAnimationFrame 如果要持续的运行下去需要使它自调用,一般会写一些类似下面的逻辑:
js
requestAnimationFrame(() => {
//一些逻辑
requestAnimationFrame(//...)
})
这实在是麻烦,这种麻烦在 framesync.js 中也得到了封装:
js
import sync,{flushSync,cancelSync,getFrameData} from "framesync"
sync.render(() => {
//一些逻辑
},true)//注意这第二个参数,
//如果它是true,则回调会在每一帧都执行,直到使用cancelSync.render进行取消。
//如果你也希望手动控制,那么你其实也可以这样写
sync.render(() => {
//这与上面的 requestAnimationFrame 自调用是类似的
sync.render(//...)//为下一帧添加新订阅
这里再次解决一个痛点(命令式的自调用),将为自调用添加了声明式的语法糖。
另外,添加到 requestAnimationFrame 上的回调我有时候希望它立刻执行,这在 framesync.js中,则是 flushSync 的职责了,它本身就是发布函数,因此调用它就可以很方便的马上触发订阅。
js
import sync,{flushSync,cancelSync,getFrameData} from "framesync"
sync.render(() => {})
//一些逻辑后
//希望马上调用sync.render(() => {})中的回调,你可以使用
flushSync.render()
//flushSync.xxx 实际上是一种"合并",
//假设你的代码是:sync.render(() => {},true),也就是它是自调用的,
//那么它本身的代码执行应该是:1 2 3 4 5 ...帧的执行
//如果你调用了 flushSync.xxx,那么 在你调用 flushSync.xxx 的那一帧里,
//(假设是一开始就调用了flushSync.xxx)代码执行会变为:12 3 4 5 ...帧的执行,
//也就是本来 1 2 用两帧处理的逻辑,合并到一帧去处理了。
//当然如果你的逻辑本身就是一帧解决的,那么flushSync.xxx 的作用就是马上处理这些逻辑(同步),
//而不是将其放到 requestAnimationFrame 的异步去处理。
一言以蔽之:flushSync.xxx的作用就是:合并加速。将两帧的逻辑合并到一帧去处理。
最后 getFrameData 是一个用于获取上一帧执行时间戳的函数,其类似于 requestAnimationFrame回调函数接收到的时间戳。
总结:
framesync.js解决了requestAnimationFrame的以下痛点:
- 执行的顺序是按照其调用顺序,无法在中途插入一个优先级更高的监听
- 可能存在的兼容性问题
- 命令式的自调用
- 无法立刻触发回调,压缩掉某一帧
有些东西用文字描述是困难的,作者能力有限只能够描述到这种程度,想要更深入的理解,还需要去阅读源码才行。
实际上库的源码并不多,但因为其大量使用了可变变量,可变变量在各函数间相互修改,导致逻辑理解困难。
但其核心其实就是一个订阅监听模式,只需要把握好"sync、cancelSync、flushSync"本质是用于订阅、取消订阅、发布的对象,那么在阅读源码的时候,就可以很轻易的找到主干,能够大大节约源码的理解时间。