使用 redux 创建秒表
Posted
技术标签:
【中文标题】使用 redux 创建秒表【英文标题】:Creating a stopwatch with redux 【发布时间】:2016-04-07 05:32:49 【问题描述】:我一直在尝试在 react 和 redux 中制作秒表。我一直无法弄清楚如何在 redux 中设计这样的东西。
首先想到的是有一个START_TIMER
操作,它将设置初始offset
值。之后,我使用setInterval
一遍又一遍地触发TICK
动作,通过使用偏移量计算已经过去了多少时间,将其添加到当前时间,然后更新offset
。
这种方法似乎有效,但我不确定如何清除间隔以停止它。另外,这个设计似乎很糟糕,可能有更好的方法来做到这一点。
这是一个完整的JSFiddle,它具有START_TIMER
功能。如果你只是想看看我的减速器现在是什么样子,这里是:
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;
非常感谢任何帮助。
【问题讨论】:
【参考方案1】:我可能会建议以不同的方式进行此操作:仅存储计算存储中经过时间所需的状态,并让组件设置其自己的间隔,以适应它们希望更新显示的频率。
这将动作调度保持在最低限度——只调度启动和停止(和重置)计时器的动作。请记住,每次调度一个动作时,您都会返回一个新的状态对象,然后每个 connect
ed 组件都会重新渲染(即使它们使用优化来避免在包装的组件)。此外,由于您必须处理所有 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);
请注意,动作创建者和缩减程序仅处理原始值,不使用任何类型的间隔或 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:https://jsbin.com/dupeji/12/edit?js,output
【讨论】:
我为我迟到的评论道歉,但非常感谢!通读所有这些确实帮助我更好地理解了我应该如何构建/设计所有这些。如果你不介意的话,我有两个问题。首先是为什么如果我使用null
而不是undefined
,上面的代码不起作用?其次,我对clearInterval(this.interval);
这行有点不确定。 this.interval
是在哪里定义的?或者你的意思是在上面做this.interval = setInterval()
?再次感谢,这意味着你会不遗余力地做到这一点!
@meh_programmer 我使用了undefined
,所以getElapsedTime
中的默认参数有效(传递 undefined 使其使用默认值,但传递 null 时并非如此)。你对间隔的看法是对的——我会解决的! :)
快速注意:看起来你在减速器中有非纯函数:新日期():“减速器保持纯净是非常重要的。你不应该在减速器内做的事情......”来自文档github.com/reactjs/redux/blob/master/docs/basics/Reducers.md 我认为最佳实践是在 ActionCreators 中包含所有杂质github.com/reactjs/redux/issues/1088
我不久前赞成这个答案,但经过进一步思考,我不同意这种方法。当您在每次调度时创建一个新的状态对象时,如果您使用 mapStateToProps,依赖于未更改状态属性的组件将不会重新渲染。此外,组件应该根据状态变化重新渲染。在您的方法中,没有状态更改,我们依靠间隔强制更新来为我们保留该“状态”。另外,你不能序列化这样的状态。
这是一个很棒的秒表示例。我会稍微更改减速器,这样当您多次发送stopTimer
时,计时器不会更新。 see it here【参考方案2】:
如果您要在更大的应用程序中使用它,那么我会使用 requestAnimationFrame
而不是 setInterval
来解决性能问题。当您显示毫秒时,您会在移动设备上注意到这一点,而不是在桌面浏览器上。
更新的 JSFiddle
https://jsfiddle.net/andykenward/9y1jjsuz
【讨论】:
是的,这是一个更大的应用程序的一部分。非常感谢,我一直认为requestAnimationFrame
是用来和canvas
做事的,我不知道我可以在这种情况下使用它。点赞!【参考方案3】:
您想使用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
);
将interval
存储在START_TIMER
action reducer 中的新状态
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
https://jsfiddle.net/8z16xwd2/2/
【讨论】:
非常感谢您的回答和解释!这几乎就是我想要做的。我还有一个问题——你会说使用setInterval
触发另一个动作的动作是不好的做法吗?
@meh_programmer 可能,是的。 This discussion on GitHub 似乎建议将间隔逻辑放在订阅商店的单独“业务逻辑”对象中会更明智。【参考方案4】:
与 andykenward 的回答类似,我会使用 requestAnimationFrame
来提高性能,因为大多数设备的帧速率仅为每秒 60 帧左右。但是,我会尽可能少地投入 Redux。如果您只需要间隔来调度事件,您可以在组件级别而不是在 Redux 中完成所有操作。请参阅 this answer 中 Dan Abramov 的评论。
下面是倒计时计时器组件的示例,它既显示倒计时时钟,又在它到期时执行某些操作。在 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() ...
【讨论】:
以上是关于使用 redux 创建秒表的主要内容,如果未能解决你的问题,请参考以下文章