使用 redux 创建秒表
Creating a stopwatch with redux
我一直在尝试在 React 和 Redux 中制作秒表。我一直无法弄清楚如何在 redux 中设计这样的东西。
首先想到的是 START_TIMER
设置初始 offset
值的操作。在那之后,我使用 setInterval
一遍又一遍地触发一个 TICK
动作,通过使用偏移量计算已经过去了多少时间,将其添加到当前时间,然后更新 offset
.
这种方法似乎可行,但我不确定如何清除间隔以停止它。另外,这个设计似乎很糟糕,可能有更好的方法。
这是具有 START_TIMER
功能的完整 JSFiddle。如果你只是想看看我的减速器现在是什么样子,这里是:
const initialState = {
isOn: false,
time: 0
};
const timer = (state = initialState, action) => {
switch (action.type) {
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset
};
case 'STOP_TIMER':
return {
...state,
isOn: false
};
case 'TICK':
return {
...state,
time: state.time + (action.time - state.offset),
offset: action.time
};
default:
return state;
}
}
非常感谢任何帮助。
您想使用 clearInterval
函数,该函数获取调用 setInterval
(唯一标识符)的结果并停止该间隔进一步执行。
因此,与其在 start()
中声明一个 setInterval
,不如将其传递给 reducer,以便它可以将其 ID 存储在状态中:
将interval
作为动作对象的成员传递给调度程序
start() {
const interval = setInterval(() => {
store.dispatch({
type: 'TICK',
time: Date.now()
});
});
store.dispatch({
type: 'START_TIMER',
offset: Date.now(),
interval
});
}
在 START_TIMER
action reducer
中的新状态上存储 interval
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset,
interval: action.interval
};
______
根据interval
渲染组件
作为组件的 属性 传入 interval
:
const render = () => {
ReactDOM.render(
<Timer
time={store.getState().time}
isOn={store.getState().isOn}
interval={store.getState().interval}
/>,
document.getElementById('app')
);
}
然后我们可以检查 out 组件中的状态,根据是否有 属性 interval
来渲染它:
render() {
return (
<div>
<h1>Time: {this.format(this.props.time)}</h1>
<button onClick={this.props.interval ? this.stop : this.start}>
{ this.props.interval ? 'Stop' : 'Start' }
</button>
</div>
);
}
______
停止计时器
要停止计时器,我们使用 clearInterval
清除间隔并再次应用 initialState
:
case 'STOP_TIMER':
clearInterval(state.interval);
return {
...initialState
};
______
已更新 JSFiddle
如果您打算在更大的应用程序中使用它,那么我会使用 requestAnimationFrame
而不是 setInterval
来解决性能问题。当您显示毫秒时,您会在移动设备上注意到这一点,而不是在桌面浏览器上。
已更新 JSFiddle
我可能会建议以不同的方式进行此操作:仅存储计算存储中经过时间所必需的状态,并让组件根据需要设置自己的自己的间隔更新显示。
这样可以将操作分派保持在最低限度 — 仅分派启动和停止(以及重置)计时器的操作。请记住,每次 调度一个动作时,您都会 返回一个新的状态对象,然后每个 connect
ed 组件 re-renders (即使它们使用优化来避免包装组件中的 re-renders 太多了)。此外,许多操作分派会使调试应用程序状态更改变得困难,因为您必须与其他操作一起处理所有 TICK
s。
这是一个例子:
// Action Creators
function startTimer(baseTime = 0) {
return {
type: "START_TIMER",
baseTime: baseTime,
now: new Date().getTime()
};
}
function stopTimer() {
return {
type: "STOP_TIMER",
now: new Date().getTime()
};
}
function resetTimer() {
return {
type: "RESET_TIMER",
now: new Date().getTime()
}
}
// Reducer / Store
const initialState = {
startedAt: undefined,
stoppedAt: undefined,
baseTime: undefined
};
function reducer(state = initialState, action) {
switch (action.type) {
case "RESET_TIMER":
return {
...state,
baseTime: 0,
startedAt: state.startedAt ? action.now : undefined,
stoppedAt: state.stoppedAt ? action.now : undefined
};
case "START_TIMER":
return {
...state,
baseTime: action.baseTime,
startedAt: action.now,
stoppedAt: undefined
};
case "STOP_TIMER":
return {
...state,
stoppedAt: action.now
}
default:
return state;
}
}
const store = createStore(reducer);
注意 action creators 和 reducer 只处理原始值,不使用任何类型的间隔或 TICK
动作类型。现在一个组件可以很容易地订阅这些数据并根据需要随时更新:
// Helper function that takes store state
// and returns the current elapsed time
function getElapsedTime(baseTime, startedAt, stoppedAt = new Date().getTime()) {
if (!startedAt) {
return 0;
} else {
return stoppedAt - startedAt + baseTime;
}
}
class Timer extends React.Component {
componentDidMount() {
this.interval = setInterval(this.forceUpdate.bind(this), this.props.updateInterval || 33);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const { baseTime, startedAt, stoppedAt } = this.props;
const elapsed = getElapsedTime(baseTime, startedAt, stoppedAt);
return (
<div>
<div>Time: {elapsed}</div>
<div>
<button onClick={() => this.props.startTimer(elapsed)}>Start</button>
<button onClick={() => this.props.stopTimer()}>Stop</button>
<button onClick={() => this.props.resetTimer()}>Reset</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
const { baseTime, startedAt, stoppedAt } = state;
return { baseTime, startedAt, stoppedAt };
}
Timer = ReactRedux.connect(mapStateToProps, { startTimer, stopTimer, resetTimer })(Timer);
您甚至可以在同一数据上以不同的更新频率显示多个计时器:
class Application extends React.Component {
render() {
return (
<div>
<Timer updateInterval={33} />
<Timer updateInterval={1000} />
</div>
);
}
}
你可以看到一个working JSBin with this implementation here: https://jsbin.com/dupeji/12/edit?js,output
与 andykenward 的回答类似,我会使用 requestAnimationFrame
来提高性能,因为大多数设备的帧速率仅为每秒 60 帧左右。但是,我会尽可能少地使用 Redux。如果你只需要时间间隔来调度事件,你可以在组件级别而不是在 Redux 中完成。请参阅 Dan Abramov 在 this answer 中的评论。
下面是倒计时计时器组件的示例,它既显示倒计时时钟又在计时结束时执行某些操作。在 start
、tick
或 stop
中,您可以调度需要在 Redux 中触发的事件。我只在计时器应该启动时安装此组件。
class Timer extends Component {
constructor(props) {
super(props)
// here, getTimeRemaining is a helper function that returns an
// object with { total, seconds, minutes, hours, days }
this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
}
// Wait until the component has mounted to start the animation frame
componentDidMount() {
this.start()
}
// Clean up by cancelling any animation frame previously scheduled
componentWillUnmount() {
this.stop()
}
start = () => {
this.frameId = requestAnimationFrame(this.tick)
}
tick = () => {
const timeLeft = getTimeRemaining(this.props.expiresAt)
if (timeLeft.total <= 0) {
this.stop()
// dispatch any other actions to do on expiration
} else {
// dispatch anything that might need to be done on every tick
this.setState(
{ timeLeft },
() => this.frameId = requestAnimationFrame(this.tick)
)
}
}
stop = () => {
cancelAnimationFrame(this.frameId)
}
render() {...}
}
我一直在尝试在 React 和 Redux 中制作秒表。我一直无法弄清楚如何在 redux 中设计这样的东西。
首先想到的是 START_TIMER
设置初始 offset
值的操作。在那之后,我使用 setInterval
一遍又一遍地触发一个 TICK
动作,通过使用偏移量计算已经过去了多少时间,将其添加到当前时间,然后更新 offset
.
这种方法似乎可行,但我不确定如何清除间隔以停止它。另外,这个设计似乎很糟糕,可能有更好的方法。
这是具有 START_TIMER
功能的完整 JSFiddle。如果你只是想看看我的减速器现在是什么样子,这里是:
const initialState = {
isOn: false,
time: 0
};
const timer = (state = initialState, action) => {
switch (action.type) {
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset
};
case 'STOP_TIMER':
return {
...state,
isOn: false
};
case 'TICK':
return {
...state,
time: state.time + (action.time - state.offset),
offset: action.time
};
default:
return state;
}
}
非常感谢任何帮助。
您想使用 clearInterval
函数,该函数获取调用 setInterval
(唯一标识符)的结果并停止该间隔进一步执行。
因此,与其在 start()
中声明一个 setInterval
,不如将其传递给 reducer,以便它可以将其 ID 存储在状态中:
将interval
作为动作对象的成员传递给调度程序
start() {
const interval = setInterval(() => {
store.dispatch({
type: 'TICK',
time: Date.now()
});
});
store.dispatch({
type: 'START_TIMER',
offset: Date.now(),
interval
});
}
在 START_TIMER
action reducer
interval
case 'START_TIMER':
return {
...state,
isOn: true,
offset: action.offset,
interval: action.interval
};
______
根据interval
作为组件的 属性 传入 interval
:
const render = () => {
ReactDOM.render(
<Timer
time={store.getState().time}
isOn={store.getState().isOn}
interval={store.getState().interval}
/>,
document.getElementById('app')
);
}
然后我们可以检查 out 组件中的状态,根据是否有 属性 interval
来渲染它:
render() {
return (
<div>
<h1>Time: {this.format(this.props.time)}</h1>
<button onClick={this.props.interval ? this.stop : this.start}>
{ this.props.interval ? 'Stop' : 'Start' }
</button>
</div>
);
}
______
停止计时器
要停止计时器,我们使用 clearInterval
清除间隔并再次应用 initialState
:
case 'STOP_TIMER':
clearInterval(state.interval);
return {
...initialState
};
______
已更新 JSFiddle
如果您打算在更大的应用程序中使用它,那么我会使用 requestAnimationFrame
而不是 setInterval
来解决性能问题。当您显示毫秒时,您会在移动设备上注意到这一点,而不是在桌面浏览器上。
已更新 JSFiddle
我可能会建议以不同的方式进行此操作:仅存储计算存储中经过时间所必需的状态,并让组件根据需要设置自己的自己的间隔更新显示。
这样可以将操作分派保持在最低限度 — 仅分派启动和停止(以及重置)计时器的操作。请记住,每次 调度一个动作时,您都会 返回一个新的状态对象,然后每个 connect
ed 组件 re-renders (即使它们使用优化来避免包装组件中的 re-renders 太多了)。此外,许多操作分派会使调试应用程序状态更改变得困难,因为您必须与其他操作一起处理所有 TICK
s。
这是一个例子:
// Action Creators
function startTimer(baseTime = 0) {
return {
type: "START_TIMER",
baseTime: baseTime,
now: new Date().getTime()
};
}
function stopTimer() {
return {
type: "STOP_TIMER",
now: new Date().getTime()
};
}
function resetTimer() {
return {
type: "RESET_TIMER",
now: new Date().getTime()
}
}
// Reducer / Store
const initialState = {
startedAt: undefined,
stoppedAt: undefined,
baseTime: undefined
};
function reducer(state = initialState, action) {
switch (action.type) {
case "RESET_TIMER":
return {
...state,
baseTime: 0,
startedAt: state.startedAt ? action.now : undefined,
stoppedAt: state.stoppedAt ? action.now : undefined
};
case "START_TIMER":
return {
...state,
baseTime: action.baseTime,
startedAt: action.now,
stoppedAt: undefined
};
case "STOP_TIMER":
return {
...state,
stoppedAt: action.now
}
default:
return state;
}
}
const store = createStore(reducer);
注意 action creators 和 reducer 只处理原始值,不使用任何类型的间隔或 TICK
动作类型。现在一个组件可以很容易地订阅这些数据并根据需要随时更新:
// Helper function that takes store state
// and returns the current elapsed time
function getElapsedTime(baseTime, startedAt, stoppedAt = new Date().getTime()) {
if (!startedAt) {
return 0;
} else {
return stoppedAt - startedAt + baseTime;
}
}
class Timer extends React.Component {
componentDidMount() {
this.interval = setInterval(this.forceUpdate.bind(this), this.props.updateInterval || 33);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const { baseTime, startedAt, stoppedAt } = this.props;
const elapsed = getElapsedTime(baseTime, startedAt, stoppedAt);
return (
<div>
<div>Time: {elapsed}</div>
<div>
<button onClick={() => this.props.startTimer(elapsed)}>Start</button>
<button onClick={() => this.props.stopTimer()}>Stop</button>
<button onClick={() => this.props.resetTimer()}>Reset</button>
</div>
</div>
);
}
}
function mapStateToProps(state) {
const { baseTime, startedAt, stoppedAt } = state;
return { baseTime, startedAt, stoppedAt };
}
Timer = ReactRedux.connect(mapStateToProps, { startTimer, stopTimer, resetTimer })(Timer);
您甚至可以在同一数据上以不同的更新频率显示多个计时器:
class Application extends React.Component {
render() {
return (
<div>
<Timer updateInterval={33} />
<Timer updateInterval={1000} />
</div>
);
}
}
你可以看到一个working JSBin with this implementation here: https://jsbin.com/dupeji/12/edit?js,output
与 andykenward 的回答类似,我会使用 requestAnimationFrame
来提高性能,因为大多数设备的帧速率仅为每秒 60 帧左右。但是,我会尽可能少地使用 Redux。如果你只需要时间间隔来调度事件,你可以在组件级别而不是在 Redux 中完成。请参阅 Dan Abramov 在 this answer 中的评论。
下面是倒计时计时器组件的示例,它既显示倒计时时钟又在计时结束时执行某些操作。在 start
、tick
或 stop
中,您可以调度需要在 Redux 中触发的事件。我只在计时器应该启动时安装此组件。
class Timer extends Component {
constructor(props) {
super(props)
// here, getTimeRemaining is a helper function that returns an
// object with { total, seconds, minutes, hours, days }
this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
}
// Wait until the component has mounted to start the animation frame
componentDidMount() {
this.start()
}
// Clean up by cancelling any animation frame previously scheduled
componentWillUnmount() {
this.stop()
}
start = () => {
this.frameId = requestAnimationFrame(this.tick)
}
tick = () => {
const timeLeft = getTimeRemaining(this.props.expiresAt)
if (timeLeft.total <= 0) {
this.stop()
// dispatch any other actions to do on expiration
} else {
// dispatch anything that might need to be done on every tick
this.setState(
{ timeLeft },
() => this.frameId = requestAnimationFrame(this.tick)
)
}
}
stop = () => {
cancelAnimationFrame(this.frameId)
}
render() {...}
}