我正在开发一个应用程序,我希望计时器从60秒倒计时到0,然后更改一些内容,之后计时器将在60再次重新启动。
我已经在React和Flux中实现了这一点,但由于我是新手,我仍然遇到了一些问题。
我现在想为计时器添加一个开始/停止按钮。我不知道在哪里放置/处理定时器状态。
我有一个Timer.jsx
组件,如下所示:
var React = require('react');
var AppStore = require('../stores/app-store.js');
var AppActions = require('../actions/app-actions.js');
function getTimeLeft() {
return {
timeLeft: AppStore.getTimeLeft()
}
}
var Timer = React.createClass({
_tick: function() {
this.setState({ timeLeft: this.state.timeLeft - 1 });
if (this.state.timeLeft < 0) {
AppActions.changePattern();
clearInterval(this.interval);
}
},
_onChange: function() {
this.setState(getTimeLeft());
this.interval = setInterval(this._tick, 1000);
},
getInitialState: function() {
return getTimeLeft();
},
componentWillMount: function() {
AppStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
componentDidMount: function() {
this.interval = setInterval(this._tick, 1000);
},
render: function() {
return (
<small>
({ this.state.timeLeft })
</small>
)
}
});
module.exports = Timer;
它从商店检索倒计时持续时间,我只需要:
var _timeLeft = 60;
现在,当我想实现一个开始/停止按钮时,我觉得我也应该通过Flux Actions实现这个,对吗?所以我想在我的商店里有这样的东西:
dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.actionType) {
case AppConstants.START_TIMER:
// do something
break;
case AppConstants.STOP_TIMER:
// do something
break;
case AppConstants.CHANGE_PATTERN:
_setPattern();
break;
}
AppStore.emitChange();
return true;
})
但是,由于我的Timer组件当前处理setInterval,我不知道如何让我的START / STOP_TIMER事件正常工作。我应该将setInterval内容从Timer组件移动到Store并以某种方式将其传递给我的组件吗?
可以找到完整代码here。
答案 0 :(得分:20)
我最终下载了您的代码并实现了您想要的启动/停止/重置功能。我认为这可能是解释事情的最佳方式 - 显示您可以运行的代码以及一些评论。
我实际上最终得到了两个实现。我将其称为实施A和实施B.
我认为展示这两种实现会很有趣。希望它不会引起太多混乱。
对于记录,实现A是更好的版本。
以下是两种实现的简要说明:
实施A
此版本会跟踪App组件级别的状态。通过将props
传递给Timer组件来管理定时器。计时器组件确实记录了它自己的剩余时间状态。
实施B
此版本使用TimerStore和TimerAction模块跟踪Timer组件级别的定时器状态,以管理组件的状态和事件。
实现B的一大(也可能是致命的)缺点是你只能有一个Timer组件。这是因为TimerStore和TimerAction模块基本上是单例。
<强> | 强>
<强> | 强>
实施A
<强> | 强>
<强> | 强>
此版本会跟踪App组件级别的状态。这里的大多数评论都在这个版本的代码中。
通过将props
传递给计时器来管理计时器。
此实施的代码更改列表:
应用-constants.js 强>
这里我只添加了一个用于重置计时器的常量。
module.exports = {
START_TIMER: 'START_TIMER',
STOP_TIMER: 'STOP_TIMER',
RESET_TIMER: 'RESET_TIMER',
CHANGE_PATTERN: 'CHANGE_PATTERN'
};
应用-actions.js 强>
我刚刚添加了一个处理重置计时器操作的调度方法。
var AppConstants = require('../constants/app-constants.js');
var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppActions = {
changePattern: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.CHANGE_PATTERN
})
},
resetTimer: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.RESET_TIMER
})
},
startTimer: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.START_TIMER
})
},
stopTimer: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.STOP_TIMER
})
}
};
module.exports = AppActions;
应用-store.js 强>
这是事情发生变化的地方。我在内部添加了详细的注释,我做了更改。
var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');
// I added a TimerStatus model (probably could go in its own file)
// to manage whether the timer is "start/stop/reset".
//
// The reason for this is that reset state was tricky to handle since the Timer
// component no longer has access to the "AppStore". I'll explain the reasoning for
// that later.
//
// To solve that problem, I added a `reset` method to ensure the state
// didn't continuously loop "reset". This is probably not very "Flux".
//
// Maybe a more "Flux" alternative is to use a separate TimerStore and
// TimerAction?
//
// You definitely don't want to put them in AppStore and AppAction
// to make your timer component more reusable.
//
var TimerStatus = function(status) {
this.status = status;
};
TimerStatus.prototype.isStart = function() {
return this.status === 'start';
};
TimerStatus.prototype.isStop = function() {
return this.status === 'stop';
};
TimerStatus.prototype.isReset = function() {
return this.status === 'reset';
};
TimerStatus.prototype.reset = function() {
if (this.isReset()) {
this.status = 'start';
}
};
var CHANGE_EVENT = "change";
var shapes = ['C', 'A', 'G', 'E', 'D'];
var rootNotes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];
var boxShapes = require('../data/boxShapes.json');
// Added a variable to keep track of timer state. Note that this state is
// managed by the *App Component*.
var _timerStatus = new TimerStatus('start');
var _pattern = _setPattern();
function _setPattern() {
var rootNote = _getRootNote();
var shape = _getShape();
var boxShape = _getBoxForShape(shape);
_pattern = {
rootNote: rootNote,
shape: shape,
boxShape: boxShape
};
return _pattern;
}
function _getRootNote() {
return rootNotes[Math.floor(Math.random() * rootNotes.length)];
}
function _getShape() {
return shapes[Math.floor(Math.random() * shapes.length)];
}
function _getBoxForShape(shape) {
return boxShapes[shape];
}
// Simple function that creates a new instance of TimerStatus set to "reset"
function _resetTimer() {
_timerStatus = new TimerStatus('reset');
}
// Simple function that creates a new instance of TimerStatus set to "stop"
function _stopTimer() {
_timerStatus = new TimerStatus('stop');
}
// Simple function that creates a new instance of TimerStatus set to "start"
function _startTimer() {
_timerStatus = new TimerStatus('start');
}
var AppStore = merge(EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
// Added this function to get timer status from App Store
getTimerStatus: function() {
return _timerStatus;
},
getPattern: function() {
return _pattern;
},
dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.actionType) {
case AppConstants.RESET_TIMER:
// Handle reset action
_resetTimer();
break;
case AppConstants.START_TIMER:
// Handle start action
_startTimer();
break;
case AppConstants.STOP_TIMER:
// Handle stop action
_stopTimer();
break;
case AppConstants.CHANGE_PATTERN:
_setPattern();
break;
}
AppStore.emitChange();
return true;
})
});
module.exports = AppStore;
<强> App.jsx 强>
App.jsx有很多变化,特别是我们已经从定时器组件将状态移动到App组件。代码中的详细注释。
var React = require('react');
var Headline = require('./components/Headline.jsx');
var Scale = require('./components/Scale.jsx');
var RootNote = require('./components/RootNote.jsx');
var Shape = require('./components/Shape.jsx');
var Timer = require('./components/Timer.jsx');
// Removed AppActions and AppStore from Timer component and moved
// to App component. This is done to to make the Timer component more
// reusable.
var AppActions = require('./actions/app-actions.js');
var AppStore = require('./stores/app-store.js');
// Use the AppStore to get the timerStatus state
function getAppState() {
return {
timerStatus: AppStore.getTimerStatus()
}
}
var App = React.createClass({
getInitialState: function() {
return getAppState();
},
// Listen for change events in AppStore
componentDidMount: function() {
AppStore.addChangeListener(this.handleChange);
},
// Stop listening for change events in AppStore
componentWillUnmount: function() {
AppStore.removeChangeListener(this.handleChange);
},
// Timer component has status, defaultTimeout attributes.
// Timer component has an onTimeout event (used for changing pattern)
// Add three basic buttons for Start/Stop/Reset
render: function() {
return (
<div>
<header>
<Headline />
<Scale />
</header>
<section>
<RootNote />
<Shape />
<Timer status={this.state.timerStatus} defaultTimeout="15" onTimeout={this.handleTimeout} />
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<button onClick={this.handleClickReset}>Reset</button>
</section>
</div>
);
},
// Handle change event from AppStore
handleChange: function() {
this.setState(getAppState());
},
// Handle timeout event from Timer component
// This is the signal to change the pattern.
handleTimeout: function() {
AppActions.changePattern();
},
// Dispatch respective start/stop/reset actions
handleClickStart: function() {
AppActions.startTimer();
},
handleClickStop: function() {
AppActions.stopTimer();
},
handleClickReset: function() {
AppActions.resetTimer();
}
});
module.exports = App;
<强> Timer.jsx 强>
Timer
也有很多更改,因为我删除了AppStore
和AppActions
依赖项,以使Timer
组件更具可重用性。详细的评论在代码中。
var React = require('react');
// Add a default timeout if defaultTimeout attribute is not specified.
var DEFAULT_TIMEOUT = 60;
var Timer = React.createClass({
// Normally, shouldn't use props to set state, however it is OK when we
// are not trying to synchronize state/props. Here we just want to provide an option to specify
// a default timeout.
//
// See http://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html)
getInitialState: function() {
this.defaultTimeout = this.props.defaultTimeout || DEFAULT_TIMEOUT;
return {
timeLeft: this.defaultTimeout
};
},
// Changed this to `clearTimeout` instead of `clearInterval` since I used `setTimeout`
// in my implementation
componentWillUnmount: function() {
clearTimeout(this.interval);
},
// If component updates (should occur when setState triggered on Timer component
// and when App component is updated/re-rendered)
//
// When the App component updates we handle two cases:
// - Timer start status when Timer is stopped
// - Timer reset status. In this case, we execute the reset method of the TimerStatus
// object to set the internal status to "start". This is to avoid an infinite loop
// on the reset case in componentDidUpdate. Kind of a hack...
componentDidUpdate: function() {
if (this.props.status.isStart() && this.interval === undefined) {
this._tick();
} else if (this.props.status.isReset()) {
this.props.status.reset();
this.setState({timeLeft: this.defaultTimeout});
}
},
// On mount start ticking
componentDidMount: function() {
this._tick();
},
// Tick event uses setTimeout. I find it easier to manage than setInterval.
// We just keep calling setTimeout over and over unless the timer status is
// "stop".
//
// Note that the Timer states is handled here without a store. You could probably
// say this against the rules of "Flux". But for this component, it just seems unnecessary
// to create separate TimerStore and TimerAction modules.
_tick: function() {
var self = this;
this.interval = setTimeout(function() {
if (self.props.status.isStop()) {
self.interval = undefined;
return;
}
self.setState({timeLeft: self.state.timeLeft - 1});
if (self.state.timeLeft <= 0) {
self.setState({timeLeft: self.defaultTimeout});
self.handleTimeout();
}
self._tick();
}, 1000);
},
// If timeout event handler passed to Timer component,
// then trigger callback.
handleTimeout: function() {
if (this.props.onTimeout) {
this.props.onTimeout();
}
}
render: function() {
return (
<small className="timer">
({ this.state.timeLeft })
</small>
)
},
});
module.exports = Timer;
<强> | 强>
<强> | 强>
实施B
<强> | 强>
<强> | 强>
代码更改列表:
应用-constants.js 强>
这些应该放在名为timer-constants.js的文件中,因为它们处理Timer组件。
module.exports = {
START_TIMER: 'START_TIMER',
STOP_TIMER: 'STOP_TIMER',
RESET_TIMER: 'RESET_TIMER',
TIMEOUT: 'TIMEOUT',
TICK: 'TICK'
};
<强>定时器actions.js 强>
此模块不言自明。我添加了三个事件 - 超时,勾选和重置。有关详细信息,请参阅代码。
var AppConstants = require('../constants/app-constants.js');
var AppDispatcher = require('../dispatchers/app-dispatcher.js');
module.exports = {
// This event signals when the timer expires.
// We can use this to change the pattern.
timeout: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.TIMEOUT
})
},
// This event decrements the time left
tick: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.TICK
})
},
// This event sets the timer state to "start"
start: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.START_TIMER
})
},
// This event sets the timer state to "stop"
stop: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.STOP_TIMER
})
},
// This event resets the time left and sets the state to "start"
reset: function() {
AppDispatcher.handleViewAction({
actionType: AppConstants.RESET_TIMER
})
},
};
<强>定时器store.js 强>
我从AppStore
中分离出了计时器内容。这是为了使Timer组件更具可重用性。
定时器存储跟踪以下状态:
Timer存储处理以下事件:
以下是代码:
var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');
var CHANGE_EVENT = "change";
var TIMEOUT_SECONDS = 15;
var _timerStatus = 'start';
var _timeLeft = TIMEOUT_SECONDS;
function _resetTimer() {
_timerStatus = 'start';
_timeLeft = TIMEOUT_SECONDS;
}
function _stopTimer() {
_timerStatus = 'stop';
}
function _startTimer() {
_timerStatus = 'start';
}
function _decrementTimer() {
_timeLeft -= 1;
}
var TimerStore = merge(EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
getTimeLeft: function() {
return _timeLeft;
},
getStatus: function() {
return _timerStatus;
},
dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.actionType) {
case AppConstants.START_TIMER:
_startTimer();
break;
case AppConstants.STOP_TIMER:
_stopTimer();
break;
case AppConstants.RESET_TIMER:
_resetTimer();
break;
case AppConstants.TIMEOUT:
_resetTimer();
break;
case AppConstants.TICK:
_decrementTimer();
break;
}
TimerStore.emitChange();
return true;
})
});
module.exports = TimerStore;
应用-store.js 强>
这可以命名为pattern-store.js
,但您需要对其进行一些更改才能重复使用。具体来说,我直接听取Timer的TIMEOUT
动作/事件来触发模式更改。如果您想重用模式更改,您可能不想要这种依赖。例如,如果您想通过单击按钮或其他内容来更改模式。
除此之外,我刚从AppStore
删除了所有与计时器相关的功能。
var AppDispatcher = require('../dispatchers/app-dispatcher.js');
var AppConstants = require('../constants/app-constants.js');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/Object.assign');
var CHANGE_EVENT = "change";
var shapes = ['C', 'A', 'G', 'E', 'D'];
var rootNotes = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];
var boxShapes = require('../data/boxShapes.json');
var _pattern = _setPattern();
function _setPattern() {
var rootNote = _getRootNote();
var shape = _getShape();
var boxShape = _getBoxForShape(shape);
_pattern = {
rootNote: rootNote,
shape: shape,
boxShape: boxShape
};
return _pattern;
}
function _getRootNote() {
return rootNotes[Math.floor(Math.random() * rootNotes.length)];
}
function _getShape() {
return shapes[Math.floor(Math.random() * shapes.length)];
}
function _getBoxForShape(shape) {
return boxShapes[shape];
}
var AppStore = merge(EventEmitter.prototype, {
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
getPattern: function() {
return _pattern;
},
dispatcherIndex: AppDispatcher.register(function(payload) {
var action = payload.action;
switch(action.actionType) {
case AppConstants.TIMEOUT:
_setPattern();
break;
}
AppStore.emitChange();
return true;
})
});
module.exports = AppStore;
<强> App.jsx 强>
这里我只添加了一些启动/停止/重置按钮。单击时,将调度TimerAction。所以如果你点击了&#34;停止&#34;按钮,我们称之为TimerAction.stop()
var React = require('react');
var Headline = require('./components/Headline.jsx');
var Scale = require('./components/Scale.jsx');
var RootNote = require('./components/RootNote.jsx');
var Shape = require('./components/Shape.jsx');
var Timer = require('./components/Timer.jsx');
var TimerActions = require('./actions/timer-actions.js');
var App = React.createClass({
render: function() {
return (
<div>
<header>
<Headline />
<Scale />
</header>
<section>
<RootNote />
<Shape />
<Timer />
<button onClick={this.handleClickStart}>Start</button>
<button onClick={this.handleClickStop}>Stop</button>
<button onClick={this.handleClickReset}>Reset</button>
</section>
</div>
);
},
handleClickStart: function() {
TimerActions.start();
},
handleClickStop: function() {
TimerActions.stop();
},
handleClickReset: function() {
TimerActions.reset();
}
});
module.exports = App;
<强> Timer.jsx 强>
主要变化之一是我们使用TimerAction和TimerStore而不是最初使用的AppAction和AppStore。原因是尝试使Timer组件更具可重用性。
Timer具有以下状态:
请注意,我使用的是setTimeout
而不是setInterval
。我发现setTimeout
更容易管理。
大部分逻辑都在_tick
方法中。基本上我们一直打电话给setTimeout
,只要状态是&#34;开始&#34;。
当计时器到达零时,我们发出timeout
事件的信号。 TimerStore和AppStore正在监听此事件。
如果计时器没有达到零,我们通过发信号&#34; tick&#34;来减去一秒钟。事件
最后,我们需要处理定时器停止然后再启动的情况。这可以通过componentDidUpdate
钩子来处理。当组件的状态发生变化或者父组件被重新渲染时,会调用此挂钩。
在componentDidUpdate
方法中,我们确保启动&#34;勾选&#34;只有当状态是&#34;开始&#34;并且超时标识符未定义。我们不希望运行多个setTimeout。
var React = require('react');
var TimerActions = require('../actions/timer-actions.js');
var TimerStore = require('../stores/timer-store.js');
function getTimerState() {
return {
status: TimerStore.getStatus(),
timeLeft: TimerStore.getTimeLeft()
}
}
var Timer = React.createClass({
_tick: function() {
var self = this;
this.interval = setTimeout(function() {
if (self.state.status === 'stop') {
self.interval = undefined;
return;
}
if (self.state.timeLeft <= 0) {
TimerActions.timeout();
} else {
TimerActions.tick();
}
self._tick();
}, 1000);
},
getInitialState: function() {
return getTimerState();
},
componentDidMount: function() {
TimerStore.addChangeListener(this.handleChange);
this._tick();
},
componentWillUnmount: function() {
clearTimeout(this.interval);
TimerStore.removeChangeListener(this.handleChange);
},
handleChange: function() {
this.setState(getTimerState());
},
componentDidUpdate: function() {
if (this.state.status === 'start' && this.interval === undefined) {
this._tick();
}
},
render: function() {
return (
<small className="timer">
({ this.state.timeLeft })
</small>
)
}
});
module.exports = Timer;
答案 1 :(得分:7)
使用flux的主要原因之一是集中应用程序状态。为此,您应完全避免使用组件的setState
功能。此外,在组件保存其自身状态的范围内,它应仅适用于非常短暂性质的状态数据(例如,您可以在组件上本地设置状态,以指示鼠标是否悬停)。
在Flux中,商店意味着同步。 (请注意,这在Flux实现中有点争议,但我绝对建议您使存储同步。一旦在Stores中允许异步操作,它就会破坏单向数据流并损害应用程序推理。)。相反,异步操作应该存在于 Action Creator 中。在你的代码中我没有提到Action Creator,所以我怀疑这可能是你混乱的根源。不过,您的实际计时器应该存在于Action Creator中。如果您的组件需要影响计时器,它可以调用Action Creator上的方法,Action Creator可以创建/管理计时器,计时器可以调度将由商店处理的事件。
更新:请注意,在2014年的react-conf Flux面板上,一位开发大型Flux应用程序的开发人员表示,对于该特定应用程序,他们确实允许在商店中进行异步数据获取操作(GET但不是PUT或POST)。
答案 2 :(得分:2)
我会从商店中删除计时器,而现在,只需管理那里的模式。您的计时器组件需要进行一些小的更改:
var Timer = React.createClass({
_tick: function() {
if (this.state.timeLeft < 0) {
AppActions.changePattern();
clearInterval(this.interval);
} else {
this.setState({ timeLeft: this.state.timeLeft - 1 });
}
},
_onChange: function() {
// do what you want with the pattern here
// or listen to the AppStore in another component
// if you need this somewhere else
var pattern = AppStore.getPattern();
},
getInitialState: function() {
return { timeLeft: 60 };
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
componentDidMount: function() {
this.interval = setInterval(this._tick, 1000);
AppStore.addChangeListener(this._onChange);
},
render: function() {
return (
<small>
({ this.state.timeLeft })
</small>
)
}
});