【发布时间】:2026-01-09 10:40:01
【问题描述】:
标题确实说明了一切。我已经看到一些关于如何防止组件卸载的问题,但我不确定这正是我需要的(或者至少不是我需要的全部)。
现在我从后台线程更新计时器,这样当浏览器选项卡进入后台时,它就不会进入睡眠状态。但我也希望它在我转到应用程序的不同屏幕时继续计数。 (我正在制作一个时间跟踪应用程序)。
谁能告诉我如何处理这个问题?
谢谢
我完整的clock.js - 不幸的是它现在嵌套在其他一些组件中,将逻辑提升到*组件将是一件苦差事。
时钟与后台任务结合使用,我已将其发布在其下方。
import React, { useEffect, useState, useRef, useContext } from 'react'
import './Clock.css'
import useSound from 'use-sound';
import bark from '../../Sounds/bong.wav';
import tweet from '../../Sounds/bong.wav';
import gong from '../../Sounds/opening_gong.wav'
import SkipNext from '@material-ui/icons/SkipNextOutlined';
import Stop from '@material-ui/icons/StopOutlined';
import PausePresentation from '@material-ui/icons/PauseOutlined';
import PlayCircleOutline from '@material-ui/icons/PlayArrowOutlined';
import MainContext from '../../MainContext'
import myWorker from '../../test.worker';
function Clock(props) {
const context = useContext(MainContext)
let breakPrefs = context.prefs
const [timer, setTimer] = useState({
onBreak: false,
firstPageLoad: true,
isPaused: true,
time: 0,
timeRemaining: 0,
cycle: 0,
skipped: false
})
const [playBark] = useSound(bark,
{ volume: 0.65 }
);
const [playTweet] = useSound(tweet,
{ volume: 0.20 }
);
const [playGong] = useSound(gong,
{ volume: 0.20 }
);
//sets up a worker thread to keep the clock running accurately when browser is in background
const worker = useRef()
useEffect(() => {
worker.current = new myWorker()
return () => {
worker.current.terminate();
}
}, [])
//updates time remaining in state from the worker thread every second
useEffect(() => {
const eventHander = e => {
if(e.data === true){
//here is where the sound should play
}else{
setTimer((timer) => ({
...timer,
timeRemaining: e.data
}))
}
}
worker.current.addEventListener('message', eventHander)
return () => {
worker.current.removeEventListener('message', eventHander)
}
}, [])
//stops the countdown from resetting during certain UI events
let allowCountdownRestart = false
useEffect(() => {
if (allowCountdownRestart) {
allowCountdownRestart = false
} else {
allowCountdownRestart = true
}
}, [props, allowCountdownRestart])
useEffect(() => {
allowCountdownRestart = false
}, [breakPrefs, props.noClockStop, context.handleAddProject, context.currentProject]);
//resets the timer when the user selects a new cycle
//couldn't pass in [props.cycle] for this because of an issue when user selects the same cycle twice
useEffect(() => {
if (allowCountdownRestart) {
worker.current.postMessage({ message: "start", "time": props.cycle * 60 })
setTimer((timer) => ({
...timer,
time: props.cycle * 60,
timeRemaining: props.cycle * 60,
cycle: props.cycle,
onBreak: false
}));
}
}, [props])
//starts the timer after it's reset
useEffect(() => {
if (timer.time > 0) {
worker.current.postMessage({ message: "start", "time": timer.time })
}
}, [timer.time])
//listens for when take break is pressed
useEffect(() => {
if(props.takeBreak !== 0){
setTimer((timer) => ({
...timer,
onBreak: true
}))
}
}, [props.takeBreak])
//handles the automatic switch to a break after a regular cycle
useEffect(() => {
if (timer.time === 0 && !timer.firstPageLoad) {
setTimeout(function () {
if (timer.onBreak) {
timer.onBreak = false
} else {
// const breakDuration = breakPrefs["break_duration"] * 60
// if (breakDuration !== 0) {
// worker.current.postMessage({ message: "start", "time": breakDuration })
// setTimer((timer) => ({
// ...timer,
// onBreak: true,
// time: breakDuration,
// timeRemaining: breakDuration
// }));
// }
if (!timer.skipped) {
props.updateDBWithTask(timer.cycle)
props.subtractFromTimeUntilBreak(timer.cycle, false)
}
setTimer((timer) => ({
...timer,
skipped: false
}));
}
}, 1000);
} else {
if (timer.time === timer.timeRemaining) {
timer.firstPageLoad = false
handleResume()
}
}
}, [timer.time, timer.time === timer.timeRemaining])
//determines which sound to play, and resets the timer to its original state at the end of a cycle.
useEffect(() => {
if (timer.timeRemaining === 0) {
if(!timer.onBreak){
playTweet()
}
setTimer((timer) => ({
...timer,
time: 0,
isPaused: true
}));
}
}, [timer.timeRemaining])
//listens for pause/unpause and updates timer accordingly
useEffect(() => {
if (timer.isPaused) {
worker.current.postMessage({ message: "pause", "time": timer.timeRemaining })
} else {
worker.current.postMessage({ message: "start", "time": timer.timeRemaining })
}
}, [timer.isPaused])
const handlePause = e => {
setTimer({ ...timer, isPaused: true })
}
const handleResume = e => {
if (timer.time !== 0) {
setTimer({
...timer,
isPaused: false
})
}
}
const handleSkip = () => {
const elapsedMinutes = Math.floor((timer.time - timer.timeRemaining) / 60)
const remainingSeconds = (timer.time - timer.timeRemaining) - elapsedMinutes * 60;
const roundedMinutes = remainingSeconds > 30 ? elapsedMinutes + 1 : elapsedMinutes
if(roundedMinutes > 0 && !timer.onBreak){
props.updateDBWithTask(roundedMinutes)
props.subtractFromTimeUntilBreak(roundedMinutes, true)
}
setTimer({ ...timer, skipped: true, timeRemaining: 0 })
worker.current.postMessage({ message: "stop", "time": 0 })
}
const handleStop = () => {
setTimer({ ...timer, onBreak: true, cycle: 0, timeRemaining: 0 })
worker.current.postMessage({ message: "stop", "time": 0 })
}
const timeFormat = (duration) => {
if (duration > 0) {
var hrs = ~~(duration / 3600);
var mins = ~~((duration % 3600) / 60);
var secs = ~~duration % 60;
var ret = "";
if (hrs > 0) {
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
}
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
ret += "" + secs;
return ret;
} else {
return "00:00"
}
}
return <>
<div className="floatLeft">
<div id="timer">
{timer.onBreak ?
<div><h2 className="display-timer-header">On Break </h2> <h2 className="display-timer">{timeFormat(timer.timeRemaining)}</h2></div>
: <div><h3 className="display-timer-header"> Time Left </h3> <h3 ref={context.timerRef} className="display-timer">{timeFormat(timer.timeRemaining)}</h3></div>}
<div className="toolbar-container">
<div className={`toolbar-icons ${props.taskBarOpen ? "taskbar-open" : ""}`}>
<i className="tooltip"><Stop className="toolbar-icon" onClick={handleStop}></Stop>
<span className="tooltiptext">Stop/Cancel</span></i>
{!timer.isPaused ?
<i className="tooltip pause"><PausePresentation className="toolbar-icon" onClick={handlePause}></PausePresentation>
<span className="tooltiptext pause-tooltip">Pause</span></i>
:
<i className="tooltip pause"><PlayCircleOutline className="toolbar-icon" onClick={handleResume}></PlayCircleOutline>
<span className="tooltiptext">Resume</span></i>
}
<i className="tooltip"><SkipNext className="toolbar-icon" onClick={handleSkip} ></SkipNext>
<span className="tooltiptext">Finish Early</span></i>
</div>
</div>
</div>
</div>
</>
}
export default Clock;
backgroundtask.js
let myInterval;
/* eslint-disable-next-line no-restricted-globals */
self.onmessage = function(evt) {
clearInterval(myInterval);
if(evt.data.message === 'pause' || evt.data.message === 'stop' || evt.data.message === 'skip'){
postMessage(evt.data.time)
}
if (evt.data.message == "start" || evt.data.message == "break") {
var i = evt.data.time;
myInterval = setInterval(function() {
i--;
postMessage(i);
}, 1000);
}
};
【问题讨论】:
-
所以这将取决于计时器更新代码在哪里。可以附上一些sn-ps吗?
-
您可以使用
context、redux、API,或者将时间状态移动到树中更高的组件吗? -
不幸的是,我现在拥有的时间保持逻辑位于嵌套更深的组件之一中。
-
我不确定发布代码会有多少帮助,我认为我遇到了架构/理论问题。
-
有什么方法可以防止组件被卸载?或者也许在计时器 UI 的“顶部”打开跟踪 UI?
标签: reactjs timer countdowntimer