Ответ 1
Я закончил загрузку кода и реализацию функции start/stop/ reset, которую вы хотели. Я думаю, что, вероятно, лучший способ объяснить вещи - показать код, который вы можете запустить и протестировать вместе с некоторыми комментариями.
На самом деле я оказался с двумя реализациями. Я буду называть их Реализация A и Реализация B.
Я подумал, что было бы интересно показать обе реализации. Надеюсь, это не вызовет слишком много путаницы.
Для записи реализация A - лучшая версия.
Ниже приведены краткие описания обеих реализаций:
Реализация A
Эта версия отслеживает состояние на уровне компонента приложения. Таймер управляется передачей props
компоненту Timer. Компонент таймера, тем не менее, отслеживает свое собственное время влево.
Реализация B
Эта версия отслеживает состояние таймера на уровне компонента Timer с помощью модуля TimerStore и TimerAction для управления состоянием и событиями компонента.
Большой (и, вероятно, фатальный) недостаток реализации B заключается в том, что у вас может быть только один компонент Timer. Это связано с тем, что модули TimerStore и TimerAction по существу являются Singletons.
|
|
Реализация A
|
|
Эта версия отслеживает состояние на уровне компонента приложения. Большинство комментариев здесь приведены в коде для этой версии.
Таймер управляется передачей props
в таймер.
Список изменений кода для этой реализации:
- приложение-constants.js
- приложение-actions.js
- приложение-store.js
- App.jsx
- Timer.jsx
приложение-constants.js
Здесь я просто добавил константу для повторного запуска таймера.
module.exports = {
START_TIMER: 'START_TIMER',
STOP_TIMER: 'STOP_TIMER',
RESET_TIMER: 'RESET_TIMER',
CHANGE_PATTERN: 'CHANGE_PATTERN'
};
приложение-actions.js
Я только что добавил метод отправки для обработки действия таймера reset.
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-actions.js(новый)
- timer-store.js(новый)
- приложение-store.js
- App.jsx
- Timer.jsx
приложение-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
Этот модуль не требует пояснений. Я добавил три события - тайм-аут, галочку и 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
})
},
};
Таймер-store.js
Я выделил материал таймера из AppStore
. Это должно сделать компонент Timer немного более многоразовым.
Хранилище таймеров отслеживает следующее состояние:
- статус таймера - может быть "start" или "stop"
- оставшееся время - время, оставшееся на таймере
Хранилище таймеров обрабатывает следующие события:
- Событие запуска таймера устанавливает время запуска таймера.
- Событие остановки таймера устанавливает статус таймера для остановки.
- Событие галочки уменьшает время, оставшееся до 1
- Событие таймера reset устанавливает время, оставшееся до значения по умолчанию, и устанавливает статус таймера для запуска
Вот код:
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
, чтобы вызвать изменение шаблона. Вероятно, вам не нужна эта зависимость, если вы хотите повторно использовать изменение шаблона. Например, если вы хотите изменить шаблон, нажав кнопку или что-то в этом роде.
Кроме того, я просто удалил все связанные с Timer функции из 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
Здесь я просто добавил несколько кнопок для начала/остановки/ reset. По щелчку отправляется 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;
Timer.jsx
Одно из главных изменений заключается в том, что мы используем TimerAction и TimerStore вместо AppAction и AppStore, которые использовались первоначально. Причина заключается в том, чтобы попытаться сделать компонент Timer немного более многоразовым.
Таймер имеет следующее состояние:
- статус Статус таймера может быть "start" или "stop"
- timeLeft Время, оставшееся на таймере
Обратите внимание, что я использовал setTimeout
вместо setInterval
. Я считаю, что setTimeout
легче управлять.
Основная часть логики находится в методе _tick
. В основном мы продолжаем звонить setTimeout
, пока статус "начинается".
Когда таймер достигает нуля, мы сигнализируем событие TIMEOUT
. TimerStore и AppStore прослушивают это событие.
- TimerStore будет просто reset таймером. То же самое событие reset.
- AppStore изменит шаблон.
Если таймер не достиг нулевого значения, мы вычитаем одну секунду, сигнализируя о событии "tick".
Наконец, нам нужно обработать случай, когда таймер остановлен, а затем запущен позже. Это можно обработать с помощью крюка componentDidUpdate
. Этот крючок вызывается, когда изменяется состояние компонента или регенерируются родительские компоненты.
В методе componentDidUpdate
мы обязательно начинаем "тикать", только если статус "старт", а идентификатор таймаута - undefined. Мы не хотим запускать несколько запусков 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;