是什么
React Spring 是一个用于构建互动式、数据驱动和动画化 UI 组件的库。
特点:使用高度优化的物理驱动动画(对比 css 的 timing-function),模拟了真实物理世界中的弹簧模型来驱动动画效果。
而且react-spring 通过直接操作 DOM 属性更新动画,而不会触发 React 组件的重新渲染。
- 动画的计算和更新逻辑独立于 React 的状态系统,放在外部。
- 使用 raf(requestAnimationFrame)来驱动动画,而不是依赖 React 的生命周期。
基本使用
最最简单的 use case:
import { useSpring, animated } from '@react-spring/web'
export default function MyComponent() {
const [springs, api] = useSpring(() => ({
from: { x: 0 },
}))
const handleClick = () => {
api.start({
from: {
x: 0,
},
to: {
x: 100,
},
})
}
return (
<animated.div
onClick={handleClick}
style={{width: 80,height: 80,background: '#ff6d6d',borderRadius: 8,...springs,}}
/>
)
}
核心概念
SpringValue
styles 整体的类型是 SpringValues,包含各种 style 相关信息,比如
- 位置(如 x, y, z)
- 透明度(如 opacity)
- 旋转(如 rotate) 这些值本身是由物理驱动计算得出的,并通过 spring 函数的模拟物理模型(如弹簧的刚度、阻尼等)动态更新的。其中每个属性的值类型都是 SpringValue
每个 useSpring() 方法都实际上是创建了一个 Controller 实例,返回两个参数[springs, api]
- api 是 SpringRef 类型,可以调用 Controller 的实例方法,进而精细控制动画的 start/pause/set/update
- springs 中每个value的类型都是 SpringValue,内部有维护 isAnimating、isPaused、from、 to、velocity 等等信息,所以每个属性都可以实现丝滑的转移过程。
targets
像react 支持多个 reconciler,react-spring 也提供了对接多个 target 的能力,对运动过程中的每个指标也抽象了一层--也就是 SpringValue 类 ,最终发布不同target 的包时再分别实例化,同时也根据不同平台注册元素并处理SpringValue 到实际生效动画的映射逻辑。
type CreateHost = (
// 不同 target 平台注册的组件也不同
components: AnimatableComponent[] | { [key: string]: AnimatableComponent },
// config 中包含如何处理props 等方法,比如web环境下 transition 属性需要特殊处理
config: Partial<HostConfig>
) => {
animated: WithAnimated
}
以 web 为例,看 CreateHost 的入参
const host = createHost(primitives, {
applyAnimatedValues,
// AnimatedStyle是将比如 x,y,转换成css 的 translate3d及 rotate 等transform的属性
createAnimatedStyle: style => new AnimatedStyle(style),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getComponentProps: ({ scrollTop, scrollLeft, ...props }) => props,
});
export const animated = host.animated
// 直接更新dom 属性,截取部分代码示例
function applyAnimatedValues(instance: Instance, props: Lookup) {
const {
className,
style,
children,
scrollTop,
scrollLeft,
viewBox,
...attributes
} = props!
if (children !== void 0) {
instance.textContent = children;
}
// Apply CSS styles
for (const name in style) {
if (style.hasOwnProperty(name)) {
const value = dangerousStyleValue(name, style[name])
if (isCustomPropRE.test(name)) {
instance.style.setProperty(name, value)
} else {
instance.style[name] = value
}
}
}
// Apply DOM attributes
Object.keys(attributes).forEach((name, i) => {
instance.setAttribute(name, values[i])
})
if (className !== void 0) {
instance.className = className
}
}
animated component
- 通过 style prop 接收 SpringValues,然后直接更新 dom 属性
- animated.xxElement 都是HOC,传递所有props 到目标元素,包括 SpringValues 的插值逻辑,提取其中所有跟 style 相关的属性特殊处理。 插值:指根据目标状态(如位置、透明度等)和当前的物理模拟状态(如弹簧的刚度、阻尼等)来动态地计算这些属性值的过程
如何避免的react rerender?
- 通过 ref 可以直接获取到 react node,然后直接更新了 DOM 节点属性,而不再通过更新 style-related state/props 来触发reac的更新机制。
animated 的注册逻辑:
const animated: WithAnimated = (Component: any) => {
// Component is string tyle
Component =
animated[Component] ||
(animated[Component] = withAnimated(Component, hostConfig))
return Component
}
// withAnimated 是实际的 HOC
const withAnimated = (Component, host) => {
return forwardRef((givenProps, givenRef) => {
// deps 都是 FluidValue 类型,useSpring 返回的style中每一个value都是该类型
const [props, deps] = getAnimatedState(givenProps, host)
// 依赖属性变更时的回调
const callback = () => {
// 实际更新 DOM style
const didUpdate = host.applyAnimatedValues(instance, props.getValue(true));
// Re-render the component when native updates fail.
if (didUpdate === false) {
forceUpdate()
}
}
// 向组件上绑定 deps(fluidValue类型的属性),打造独立于react之外的订阅-监听体系,当动画属性变化时,对应组件均可以触发更新
const observer = new PropsObserver(callback, deps)
const observerRef = useRef<PropsObserver>()
// node 端是useEffect, clien 端是 useLayoutEffect
useIsomorphicLayoutEffect(() => {
observerRef.current = observer
/**
* Observe the latest dependencies.
* addFluidObserver 的大致逻辑:
* 1. dep.observers.add(observer),每个dep(FluidValue类) 内部有属性比如observers维护了监听器
* 2. 每个observer中都有update方法,当 dep值变化时 dep.observers.forEach(observer => observer.update(newValue))
*/
each(deps, dep => addFluidObserver(dep, observer))
return () => {
// Stop observing previous dependencies.
if (observerRef.current) {
each(observerRef.current.deps, dep =>
removeFluidObserver(dep, observerRef.current!)
)
raf.cancel(observerRef.current.update)
}
}
})
const usedProps = host.getComponentProps(props.getValue())
return <Component {...usedProps} ref={givenRef} />
})
}
Do your spring
前置物理知识
弹簧力(Spring Force)描述弹簧在受到压缩或拉伸时,产生的回复力,也就是弹簧试图恢复到原始状态的力。 公式: F=−k⋅x, k 越大,移动越快。x是弹簧位移/形变,也就是弹簧伸长或者压缩的量,可以认为 to 是弹簧的原位。
源码中的使用: const springForce = - config.tension * 0.000001 * (position - to);
阻尼力(Damping Force) dampingForce = -config.friction * 0.001 * velocity:这个公式用于计算阻尼力,config.friction 是阻尼常数,velocity 是物体的速度,0.001 是缩放系数。
加速度(Acceleration)
- acceleration = (springForce + dampingForce) / config.mass:根据牛顿的第二定律,物体的加速度由总力除以物体的质量得出。
更新速度和位置
- velocity = velocity + acceleration * step:更新物体的速度。
- position = position + velocity * step:更新物体的位置。
模拟 spring 动画实现
我们先模拟最简单的水平位移场景
class SpringSimulation {
constructor(config) {
this.config = config;
this.position = 0; // 物体初始位置
this.velocity = 0; // 物体初始速度
this.running = false; // 动画是否正在运行
}
// 设置物体初始位置
setPosition(position) {
this.position = position;
}
// 设置物体初始速度
setVelocity(velocity) {
this.velocity = velocity;
}
// 更新位置的计算方法
updatePosition(step, from, to) {
// 计算弹簧力
const springForce = -this.config.tension * 0.000001 * (this.position - to);
// 计算阻尼力
const dampingForce = -this.config.friction * 0.001 * this.velocity;
// 计算加速度
const acceleration = (springForce + dampingForce) / this.config.mass;
this.velocity = this.velocity + acceleration * step;
this.position = this.position + this.velocity * step;
return this.position;
}
// 启动动画并通过 requestAnimationFrame 进行更新
startAnimation(from, to, applyAnimationValue) {
// 初始化位置和速度
this.setPosition(from);
this.setVelocity(0);
const timeStep = 16.67; // 每一帧的时间步长,16.67ms
const animate = () => {
// 更新物体位置
const newPosition = this.updatePosition(timeStep, from, to);
// 可以根据 position 更新 DOM style
applyAnimationValue?.()
// 判断是否已经接近目标位置
if (Math.abs(newPosition - to) > 1) {
// 如果物体尚未到达目标位置,继续下一帧动画
requestAnimationFrame(animate);
} else {
// 物体到达目标位置,停止动画
console.log("Reached target position:", newPosition);
}
};
this.running = true;
requestAnimationFrame(animate);
}
// 停止动画
stopAnimation() {
this.running = false;
}
}
// 配置弹簧参数
const config = {
tension: 170, // 张力
friction: 26, // 阻尼
mass: 1, // 物体质量
};
// 创建模拟实例
const springSim = new SpringSimulation(config);
export default springSim;
组件中使用:
const handleClick = () => {
// 设置初始位置和目标位置
const from = 0; // 起始位置
const to = 500; // 目标位置
springSim.startAnimation(from, to, (newPos) => {
// 仅仅处理水平位移的style,库中还得兼容所有 web style 的赋值,尤其像 transform 这种组合属性的case
domRef.current.style.left = `${newPos}px`;
});
};
在 react-spring 中因为动画过程会有多个属性同时变化,还需要考虑所有属性的及时更新。每个 useSpring() 都会创建一个 Controller 实例,在调用 springRef/Api 更新动画(start/update)的时候,内部都会将待更新对象值推到队列中,然后利用 requestAnimationFrame 执行 flushUpdateQueue 操作,promise.all 等待本次 queue 中所有子任务更新后得到新的 springValues, 再去 applyAnimationValues (实际更新到 dom),循环往复。