使用 requestAnimationFrame 扩展动画,React 有时不起作用



我正在尝试用简单的"展开";进入/退出编辑模式时的动画。

基本上,我创建了一个包含值的重影元素,这个元素旁边是图标按钮,可以编辑/保存。当您单击编辑按钮时,应显示带有值的输入,而不是重影元素,并且输入的宽度应扩展/减小到定义的常量。

到目前为止,我已经有了这段代码,它大部分都很好用,但对于扩展,它有时不会产生动画,我不知道为什么。

toggleEditMode = () => {
const { editMode } = this.state
if (editMode) {
this.setState(
{
inputWidth: this.ghostRef.current.clientWidth
},
() => {
requestAnimationFrame(() => {
setTimeout(() => {
this.setState({
editMode: false
})
}, 150)
})
}
)
} else {
this.setState(
{
editMode: true,
inputWidth: this.ghostRef.current.clientWidth
},
() => {
requestAnimationFrame(() => {
this.setState({
inputWidth: INPUT_WIDTH
})
})
}
)
}
}

你可以看看这里的例子。有人能解释一下出了什么问题,或者帮我找到解决方案吗?如果我在代码中添加另一个setTimeout(() => {...expand requestAnimationFrame here...}, 0),它就会开始工作,但我根本不喜欢这个代码。

这个答案详细解释了发生了什么以及如何修复它。然而,我实际上并不建议实现它。

自定义动画很混乱,而且有一些很棒的库可以帮你处理这些肮脏的工作。它们包装refrequestAnimationFrame代码,并为您提供一个声明性的API。我过去用过react spring,它对我来说效果很好,但Framer Motion看起来也不错。

然而,如果你想了解你的例子中发生了什么,请继续阅读

发生了什么

requestAnimationFrame是告诉浏览器在每次渲染帧时运行一些代码的一种方式。requestAnimationFrame的一个保证是,浏览器将始终等待代码完成,然后再渲染下一帧,即使这意味着要删除一些帧。

那么,为什么这似乎不起作用呢?

setState触发的更新是异步的。React不保证在调用setState时重新渲染;setState只是请求重新评估虚拟DOM树,React异步执行。这意味着setState可以并且通常在不立即更改DOM的情况下完成,并且实际的DOM更新可能直到浏览器渲染下一帧之后才发生。

这也允许React将多个setState调用绑定到一个重新渲染中,有时会这样做,因此DOM可能在动画完成之前不会更新。

如果您想保证requestAnimationFrame中的DOM更改,您必须使用Reactref:自己执行

const App = () => {
const divRef = useRef(null);
const callbackKeyRef = useRef(-1);
// State variable, can be updated using setTarget()
const [target, setTarget] = useState(100);
const valueRef = useRef(target);
// This code is run every time the component is rendered.
useEffect(() => {
cancelAnimationFrame(callbackKeyRef.current);
const update = () => {
// Higher is faster
const speed = 0.15;

// Exponential easing
valueRef.current
+= (target - valueRef.current) * speed;
// Update the div in the DOM
divRef.current.style.width = `${valueRef.current}px`;
// Update the callback key
callbackKeyRef.current = requestAnimationFrame(update);
};
// Start the animation loop
update();
});
return (
<div className="box">
<div
className="expand"
ref={divRef}
onClick={() => setTarget(target === 100 ? 260 : 100)}
>
{target === 100 ? "Click to expand" : "Click to collapse"}
</div>
</div>
);
};

下面是一个工作示例。

这段代码使用钩子,但类也使用相同的概念;只需用componentDidUpdate替换useEffect,用组件状态替换useState,用React.createRef替换useRef

在组件中从react-transition-group使用CSSTransition似乎是一个更好的方向

function Example() {
const [tr, setIn] = useState(false);
return (
<div>
<CSSTransition in={tr} classNames="x" timeout={500}>
<input
className="x"
onBlur={() => setIn(false)}
onFocus={() => setIn(true)}
/>
</CSSTransition>
</div>
);
}

并且在您的css模块中:

.x {
transition: all 500ms;
width: 100px;
}
.x-enter,
.x-enter-done {
width: 400px;
}

它可以避免使用setTimeouts和requestAnimationFrame,并使代码更干净。

Codesandbox:https://codesandbox.io/s/csstransition-component-forked-3o4x3?file=/index.js

最新更新