Управление таймером в React / Flux
Я работаю над приложением, где я хочу таймер для обратного отсчета от, скажем, 60 секунд до 0, а затем изменить некоторое содержимое, после чего таймер перезапускается снова в 60.
я реализовал это в React и Flux, но поскольку я новичок в этом, я все еще сталкиваюсь с некоторыми проблемами.
теперь я хочу добавить кнопку start/stop для таймера. Я не уверен, куда поместить / обработать состояние таймера.
у меня есть компонент 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;
теперь, когда я хочу реализовать кнопку start/stop, я чувствую, что я должен также реализовать это через действия Flux, правильно? Поэтому я думал о том, чтобы иметь что-то подобное в моем магазине:
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;
})
однако, поскольку мой компонент таймера в настоящее время обрабатывает setInterval, я не знаю, как заставить мои события START/STOP_TIMER работать. Должен ли я двигаться? материал setInterval из компонента Timer в магазин и как-то передать это моему компоненту?
полный код можно найти здесь.
3 ответов
я закончил загрузку вашего кода и реализацию функции запуска/остановки/сброса, которую вы хотели. Я думаю, что это лучший способ объяснить - показать код, который вы можете запустить и протестировать вместе с некоторыми комментариями.
я фактически закончил с двумя реализациями. Я назову их реализацией A и реализацией B.
я думал, что было бы интересно показать обе реализации. Надеюсь, это не вызовет слишком много путаницы.
для запись, реализация A является лучшей версией.
вот краткое описание обеих реализаций:
Реализация A
эта версия отслеживает состояние на уровне компонентов приложений. Таймер управляется путем передачи props
к компоненту таймера. Однако компонент таймера отслеживает собственное состояние времени.
Реализация B
эта версия сохраняет состояния таймера на уровне компонента таймера с помощью модуля TimerStore и TimerAction для управления состоянием и событиями компонента.
большой (и, вероятно, фатальный) недостаток реализации B заключается в том, что у вас может быть только один компонент таймера. Это связано с TimerStore и модули TimerAction по существу синглтоны.
|
|
реализация А
|
|
эта версия отслеживает состояние на уровне компонентов приложений. Большинство комментариев здесь находятся в коде для этой версии.
таймер управляется путем передачи props
к таймеру.
изменения кода для этого реализация:
- app-константы.js
- app-действия.js
- app-store.js
- App.jsx
- таймер.jsx
приложение-константы.js
здесь я просто добавил константу для сброса таймера.
module.exports = {
START_TIMER: 'START_TIMER',
STOP_TIMER: 'STOP_TIMER',
RESET_TIMER: 'RESET_TIMER',
CHANGE_PATTERN: 'CHANGE_PATTERN'
};
app-действия.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;
app-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
есть многочисленные изменения в приложение.jsx, в частности, мы переместили состояние в компонент приложения из компонента timer. Опять подробные комментарии в коде.
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;
таймер.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
|
|
изменения кода листинг:
- app-константы.js
- таймер-действия.js (новое)
- таймер-магазине.js (новое)
- app-store.js
- App.jsx
- таймер.jsx
приложение-константы.js
они, вероятно, должны идти в файл с именем timer-constants.js, так как они имеют дело с таймером деталь.
module.exports = {
START_TIMER: 'START_TIMER',
STOP_TIMER: 'STOP_TIMER',
RESET_TIMER: 'RESET_TIMER',
TIMEOUT: 'TIMEOUT',
TICK: 'TICK'
};
таймер-действия.js
этот модуль не требует пояснений. Я добавил три события-timeout, tick и reset. Посмотреть код детали.
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
})
},
};
таймер-магазине.js
я отделил материал таймера от AppStore
. Это должно сделать компонент Timer немного более многоразовым.
магазин таймера отслеживает следующее состояние:
- времени - может быть "старт"или " стоп"
- времени осталось - времени осталось на таймере
магазин таймера обрабатывает следующие события:
- событие запуска таймера устанавливает статус таймера для запуска.
- событие остановки таймера устанавливает статус таймера для остановки.
- событие tick уменьшает время, оставшееся на 1
- событие сброса таймера устанавливает время слева по умолчанию и устанавливает статус таймера для запуска
вот код:
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;
app-store.js
это можно назвать pattern-store.js
, хотя вам нужно будет внести некоторые изменения, чтобы он был многоразовым. В частности, я непосредственно слушаю таймер 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. Поэтому, если вы нажали кнопку "стоп", мы вызываем 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;
таймер.jsx
одним из основных изменений является то, что мы используем TimerAction и TimerStore вместо AppAction и AppStore, который использовался изначально. Причина в том, чтобы попытаться сделать компонент таймера немного более многоразовые.
таймер имеет следующее состояние:
- статус состояние таймера может быть "Пуск"или " стоп"
- времени timeleft времени осталось на таймере
обратите внимание, что я использовал setTimeout
вместо setInterval
. Я нахожу setTimeout
легче управлять.
в основная часть логики находится в _tick
метод. В основном мы продолжаем звонить setTimeout
до тех пор, пока статус "пуск".
когда таймер достигает нуля, то сигнал timeout
событие. TimerStore и AppStore прослушивают это событие.
- TimerStore просто сбросит таймер. То же самое событие сброса.
- AppStore изменит шаблон.
если таймер не достиг нуля, мы вычитаем одну секунду на сигнализация события" ТИК".
наконец, нам нужно обработать случай, когда таймер остановлен, а затем запущен позже. Это можно обработать через componentDidUpdate
крюк. Этот крюк вызывается при изменении состояния компонента или повторном отображении родительских компонентов.
на componentDidUpdate
метод, мы обязательно запустим "тиканье", только если статус" пуск " и идентификатор тайм-аута не определен. Мы не хотим, чтобы несколько setTimeouts работает.
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;
Не хранить состояние в компонентах
одной из основных причин использования flux является централизовать заявления о государственной. С этой целью вы должны избегайте использования компонента на всех. Кроме того, в той мере, в какой компоненты сохраняют свое собственное состояние, это должно быть только для данных состояния очень мимолетного характера (например, вы можете установить состояние локально на компоненте, который указывает, если мышь парит).
Использовать Создателей Действий для асинхронных операций
В Поток, магазины предназначены для синхронного. (Обратите внимание, что это несколько спорный момент среди реализаций Flux, но я определенно предлагаю вам сделать магазины синхронными. Как только вы разрешите асинхронную операцию в магазинах, она нарушает однонаправленный поток данных и ухудшает рассуждения приложения.). Вместо этого асинхронная операция должна жить в вашем Создатель Действий. В вашем коде я не вижу упоминания о создателе действия, поэтому я подозреваю это может быть источником путаницы. Тем не менее, ваш фактический таймер должен жить в Создателе действия. Если ваш компонент должен влиять на таймер, он может вызвать метод для создателя действия, создатель действия может создать / управлять таймером, а таймер может отправлять события, которые будут обрабатываться магазине.
обновление: обратите внимание, что на панели 2014 react-conf Flux один разработчик, работающий над большим приложением Flux, сказал, что для это конкретное приложение они позволяют асинхронные операции извлечения данных в магазинах (получает, но не ставит или сообщения).
Я бы удалил таймер из магазина, а пока просто управляйте шаблонами там. Вашему компоненту таймера потребуется несколько небольших изменений:
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>
)
}
});