英文:
function inside setInterval does not work properly in ReactJs
问题
我已经看到了很多关于在React中创建定时器的教程,通过以下方式进行操作:
useEffect(() => {
let interval = null;
if (timeractive) {
interval = setInterval(() => {
setCount(count => count + 1);
}, 50);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timeractive, count]);
这是我认为发生的事情:
timeractive = true
- 调用
useEffect
,将变量interval
分配给setInterval
。 - 调用
setCount
,增加count
的值。 - 再次调用
useEffect
,因为count
是其依赖之一。但是前一个useEffect
的清理函数首先运行,清除了变量interval
。 - 回到步骤2
我不想以这种方式做事,因为它似乎不太直观。实际上,它是通过创建和清除一堆仅持续一次迭代的小间隔来创建一个大的“interval”。
因此,我编写了以下代码,尝试创建一个不依赖于正在递增的内容的定时器。这就是我决定使用useRef
的原因,以便间隔在渲染之间保持持久。
这是一个每100ms增加半径并绘制具有该半径(模50)的圆的计时器(应该是动画,但不起作用)。但是半径似乎更新正常。因此,drawcircle
函数存在一些问题。
我在这种情况下是否可以有人解释闭包的概念?
const { useState, useEffect, useRef } = React;
function Timer({ active }) {
const intervalRef = useRef(null);
const canvasRef = useRef(null);
const [width, height] = [500, 500];
const [radius, setRadius] = useState(30);
useEffect(() => {
if (active) {
intervalRef.current = setInterval(() => {
drawcircle();
setRadius(radius => radius + 1);
}, 100);
} else {
clearInterval(intervalRef.current);
}
}, [active]);
const drawcircle = () => {
const context = canvasRef.current.getContext('2d');
context.clearRect(0, 0, width, height);
context.beginPath();
context.arc(width / 2, height / 2, radius % 50, 0, 2 * Math.PI);
context.stroke();
};
return (
<div>
<canvas ref={canvasRef} width={width} height={height} />
<p>半径是 {radius}</p>
</div>
);
}
function Main() {
const [active, setActive] = useState(false);
return (
<div>
<Timer active={active} />
<button onClick={() => { setActive(!active) }}>切换</button>
</div>
);
}
ReactDOM.render(<Main />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
希望这能帮助你理解代码中发生的事情。
英文:
I have seen many tutorials on creating timers in React by doing something like this
useEffect(() => {
let interval = null;
if (timeractive) {
interval = setInterval(() => {
setCount(count => count+ 1);
}, 50);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
},[timeractive,count]);
This is what I think is happening:
1 timeractive = true
2 useEffect
gets called, declaring assigning the variable interval
to a setInterval
3 setCount
gets called, incrementing count
.
4 useEffect
gets called again since count
is one of its dependencies. But the cleanup function from the previous useEffect
runs first, clearing the variable interval
5 go back to step 2
I don't want to do things this way because it seems quite unintuitive. It is essentially creating a large "interval" by creating and clearing a bunch of smaller intervals that only lasts one iteration.
So I wrote the following code, trying to create a timer that does not depend on what's being incremented. That why I decided to use useRef so that the interval will be persist between renders.
This is a timer that increases the radius every 100ms and draw a circle with that radius (modulo 50), it is supposed to be an animation but it doesn't work. However the radius seems to be updating fine. So there is something going on with the drawcircle
function
I asked a similar question here, that's why I decided to pass down an updater function in setRadius
Can someone explain the concept of closure in this case?
<!-- begin snippet: js hide: false console: true babel: true -->
<!-- language: lang-js -->
const {useState,useEffect,useRef} = React;
function Timer({active}) {
const intervalRef = useRef(null);
const canvasRef = useRef(null);
const [width,height] = [500,500];
const [radius, setRadius] = useState(30);
useEffect(()=>{
if(active){
intervalRef.current = setInterval(()=>{
drawcircle();
setRadius(radius => radius + 1);
},100)
} else {
clearInterval(intervalRef.current)
}
},[active])
const drawcircle = ()=>{
const context = canvasRef.current.getContext('2d');
context.clearRect(0,0,width,height)
context.beginPath()
context.arc(width/2,height/2,radius%50,0,2*Math.PI)
context.stroke();
}
return (
<div>
<canvas ref={canvasRef} width={width} height={height}/>
<p>radius is {radius}</p>
</div>
)
}
function Main() {
const [active, setActive] = useState(false)
return (
<div>
<Timer active={active}/>
<button onClick={()=>{setActive(!active)}}>Toggle</button>
</div>
)
}
ReactDOM.render(<Main />, document.getElementById('app'));
<!-- language: lang-html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="app"></div>
<!-- end snippet -->
答案1
得分: 1
The interval callback uses the drawcircle
method of the first render on each iteration, and that drawcircle
method refers to the initial radius value. To solve this, use a ref for the drawcircle
method.
drawcircleRef = useRef()
useEffect(() => {
if (active) {
drawcircleRef.current = drawcircle
const interval = setInterval(() => {
drawcircleRef.current();
setRadius(radius => radius + 1);
}, 100)
return () => clearInterval(interval)
}
}, [active])
useEffect(() => {
drawcircleRef.current = drawcircle
}, [radius])
英文:
The interval callback use the drawcircle
method of the first render on each iteration and that drawcircle
method refers to the initial radius value. To solve this, use a ref to drawcircle
method
drawcircleRef = useRef()
useEffect(()=>{
if(active){
drawcircleRef.current = drawcircle
const interval= setInterval(()=>{
drawcircleRef.current();
setRadius(radius => radius + 1);
},100)
return ()=> clearInterval(interval)
}
},[active])
useEffect(()=>{
drawcircleRef.current = drawcircle
},[radius])
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论