我认为react springuseSpring()
会导致组件大量重新渲染,所以如果它是一个已经有大量CPU密集工作要做的组件,那么react spring就不适合执行该任务。
我可以看到它重新渲染了很多,在他们的例子中:
https://codesandbox.io/s/musing-dew-9tfi9?file=/src/App.tsx
(通过查看console.log输出,它有很多renderCount
的打印输出。当我们将持续时间更改为5秒的5000
时,打印输出要多得多)。
同样,如果它是一个类似于反作用弹簧的组件,它会渲染很多:
https://codesandbox.io/s/wonderful-currying-9pheq
但是,以下代码:
let renderCount = 0
export default function App() {
const styles = useSpring({
loop: true,
to: [
{ opacity: 1, color: '#ffaaee' },
{ opacity: 0.5, color: 'rgb(14,26,19)' },
{ transform: 'translateX(100px)' },
{ transform: 'translateX(0px)' },
],
from: { opacity: 0, color: 'red', transform: 'translateX(0px)' },
config: { duration: 500 },
})
console.log('renderCount', ++renderCount)
return <a.div style={styles}>I will fade in and out</a.div>
}
演示:https://codesandbox.io/s/dazzling-rgb-j2bx3?file=/src/App.tsx
我们可以看到renderCount
几乎没有被打印出来。react-spring
应该需要不断更新组件的style
,所以一分钟后,我会像上面的前两个例子一样,期待renderCount
的大量打印出来,但事实并非如此。
在这种情况下,react spring是如何以及为什么不导致大量重新渲染的,我们如何知道在什么情况下react spring会导致大量重新绘制(以及如何防止)?
react-spring
以增量方式更新样式以创建动画(与使用transition
的css动画相反)。
React之外的天真动画
如果react-spring
存在于React之外(它显然不存在,因为那样它就不会被命名为react-spring
),那么最容易做到的是,根据基于多个因素(如延迟、持续时间等)的一些预先确定的模式,通过Javascript修改给定元素的样式
...
setTimeout(() => document.getElementById("#el").style.opacity = 0.34,100)
setTimeout(() => document.getElementById("#el").style.opacity = 0.39,150)
setTimeout(() => document.getElementById("#el").style.opacity = 0.42,200)
...
setTimeout(() => document.getElementById("#el").style.opacity = 1.0, 1000)
这将如何实现当然不是这个答案的重点,上面的实现将是一个非常天真的实现,但如果我们想进行一些动画转换,其中两个端点之间的插值将由我们自己计算和实现(使用弹簧物理),而不是在浏览器中(使用csstransition
),这基本上就是可以进行的。
React中的天真动画
在React中,我们知道做事情的首选方式是在React内部提供更改,然后React处理这些更改,然后由React处理对DOM的必要更改。以前面的React(幼稚)示例为例,这意味着某种方案,其中存储opacity
的状态将被重复更新,直到达到所需的端点。
const Component = () => {
...
const [opacity, setOpacity] = useState(0)
useEffect(() => {
...
setTimeout(() => setOpacity(0.34),100)
setTimeout(() => setOpacity(0.39),150)
setTimeout(() => setOpacity(0.42),200)
...
setTimeout(() => setOpacity(1.0), 1000)
}, [])
return (
<div style={{ opacity }} ... />
)
}
这是可行的,但正如人们所料,这可能会非常繁重,因为动画应该快速平滑地进行,并且在每个动画帧上重新渲染React可能会有问题;如果发生动画的组件渲染成本很高,则动画本身可能会受到影响,看起来不太好。
React中的react-spring
react-spring
解决这个问题的方法是通过refs在React的外部进行更新。前面的玩具示例可能看起来像:
const Component = () => {
...
const ref = useRef(null)
useEffect(() => {
if(ref.current) {
...
setTimeout(() => ref.current.style.opacity = 0.34,100)
setTimeout(() => ref.current.style.opacity = 0.39,150)
setTimeout(() => ref.current.style.opacity = 0.42,200)
...
setTimeout(() => ref.current.style.opacity = 1.0, 1000)
}
}, [])
...
return (
<div ref={ref} ... />
)
}
同样,这是一个示例,如何以最佳方式(如react-spring
)实现这一点则另当别论。但我们可以同意,如果我们每次渲染上述组件时都会登录到控制台,那么即使不透明度会继续变化,它也只会登录一次。
总之,当最佳使用react-spring
时,它会使用refs来更新DOM元素的属性,从而绕过React。因此,组件可能只渲染一次,但仍会生成重复的动画。这尤其适用于api用于执行更新的情况(而不是将状态存储在父组件中,每次我们希望动画发生时都会设置该状态):
const [spring, api] = useSpring(() => ({ <INITIAL PROPS> })) // use api to make changes
const spring = useSpring({ <INITIAL PROPS }) // rerender component to update props
当使用react-spring
提供的基本HTML元素(如animated.div
、animated.span
等)时,react-spring
负责在相应的DOM元素上附加一个ref,并通过这个ref来管理元素的动画化,从而也动画化其中的所有内容。当创建用animated
包装的自己的自定义组件时,如果您想要最佳的动画效果,您需要确保您的自定义组件可以接受ref(通过forwardRef
),并将其传递给应该设置动画的元素。如果不执行此操作,则react-spring
将在每个动画帧上重新渲染该元素。尽管这也很有效,但从性能的角度来看,它是次优的。
你的例子
在你的例子中,其他一些事情也在起作用。在第一个例子中,钩子useMeasure
是从react-use-measure
开始使用的。这个钩子将持续提供与子组件不同的值(此处提供高度),因此父组件将重新应答。由于Tree
组件是嵌套的,因此每当一个Tree
组件更改高度时,其高度将更改的所有父Tree
组件也将重新发布。因此,我们看到了相当多的重新招标。此外,由于启用了StrictMode
,因此该数字会加倍。useSpring
是在没有api的情况下使用的,但这在这里并不重要,因为无论如何,由于useMeasure
,父级会重新发送很多。
在react-spring
的第二个示例中,也没有使用api,但由于动画是循环的,因此不需要设置父对象中的任何状态,因此不会重新渲染。因为父级不重新发布,animated
组件也不重新发布——所以在这种情况下,我们是否使用api也无关紧要。然而,在本例中,如果我们想更新动画道具,那么使用api会导致父级不重新发布,否则,持有要更新的道具的状态将位于父级(或父级),因此当父级重新发布时,animated
组件也会重新发布。