javascript 在 React/Flux 中处理计时器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/27604431/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-28 07:41:27  来源:igfitidea点击:

Handling a timer in React/Flux

javascriptreactjsreactjs-flux

提问by cabaret

I'm working on an application where I want a timer to countdown from, say, 60 seconds to 0 and then change some content, after which the timer restarts again at 60.

我正在开发一个应用程序,我希望计时器从 60 秒倒计时到 0,然后更改一些内容,之后计时器在 60 处再次重新启动。

I have implemented this in React and Flux but since I'm new to this, I'm still running into some problems.

我已经在 React 和 Flux 中实现了这个,但由于我是新手,我仍然遇到了一些问题。

I now want to add a start/stop button for the timer. I'm not sure where to put/handle the timer state.

我现在想为计时器添加一个开始/停止按钮。我不确定在哪里放置/处理计时器状态。

I have a component Timer.jsxwhich looks like this:

我有一个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;

It retrieves a countdown duration from the store, where I simply have:

它从商店中检索倒计时持续时间,我只是在那里:

var _timeLeft = 60;

var _timeLeft = 60;

Now, when I want to implement a start/stop button, I feel like I should also implement this through Flux Actions, correct? So I was thinking of having something like this in my store:

现在,当我想实现一个开始/停止按钮时,我觉得我也应该通过 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;
})

However, since my Timer component currently handles the setInterval, I don't know how to get my START/STOP_TIMER events working. Should I move the setInterval stuff from the Timer component to the Store and somehow pass this down to my component?

但是,由于我的 Timer 组件当前处理 setInterval,我不知道如何让我的 START/STOP_TIMER 事件工作。我应该将 setInterval 的东西从 Timer 组件移动到 Store 并以某种方式将它传递给我的组件吗?

Full code can be found here.

完整代码可以在这里找到。

回答by Gohn67

I ended up downloading your code and implementing the start/stop/reset feature you wanted. I think that's probably the best way to explain things - to show code that you can run and test along with some comments.

我最终下载了您的代码并实现了您想要的启动/停止/重置功能。我认为这可能是解释事物的最佳方式 - 展示您可以运行和测试的代码以及一些注释。

I actually ended up with two implementations. I'll call them Implementation A and Implementation B.

我实际上最终得到了两个实现。我将它们称为实现 A 和实现 B。

I thought it would be interesting to show both implementations. Hopefully it doesn't cause too much confusion.

我认为展示这两种实现会很有趣。希望它不会引起太多混乱。

For the record, Implementation A is the better version.

根据记录,实现 A 是更好的版本。

Here are brief descriptions of both implementations:

以下是两种实现的简要说明:

Implementation A

实施A

This version keeps track of the state at the App component level. The timer is managed by passing propsto the Timer component. The timer component does keep track of it's own time left state though.

此版本在 App 组件级别跟踪状态。通过传递props给 Timer 组件来管理计时器。不过,计时器组件确实会跟踪它自己的剩余时间状态。

Implementation B

实施B

This version keeps track of the timer state at the the Timer component level using a TimerStore and TimerAction module to manage state and events of the component.

此版本使用 TimerStore 和 TimerAction 模块在 Timer 组件级别跟踪计时器状态,以管理组件的状态和事件。

The big (and probably fatal) drawback of implementation B is that you can only have one Timer component. This is due to the TimerStore and TimerAction modules essentially being Singletons.

实现 B 的一个大(可能是致命的)缺点是你只能有一个 Timer 组件。这是因为 TimerStore 和 TimerAction 模块本质上是单例。



|

|

|

|

Implementation A

实施A

|

|

|

|

This version keeps track of the state at the App component level. Most of the comments here are in the code for this version.

此版本在 App 组件级别跟踪状态。这里的大部分评论都在这个版本的代码中。

The timer is managed by passing propsto the Timer.

定时器通过传递props给定时器来管理。

Code changes listing for this implementation:

此实现的代码更改列表:

  • app-constants.js
  • app-actions.js
  • app-store.js
  • App.jsx
  • Timer.jsx
  • app-constants.js
  • app-actions.js
  • 应用商店.js
  • 应用程序.jsx
  • 定时器.jsx

app-constants.js

app-constants.js

Here I just added a constant for reseting the timer.

在这里,我只是添加了一个用于重置计时器的常量。

module.exports = {
  START_TIMER: 'START_TIMER',
  STOP_TIMER: 'STOP_TIMER',
  RESET_TIMER: 'RESET_TIMER',
  CHANGE_PATTERN: 'CHANGE_PATTERN'
};

app-actions.js

app-actions.js

I just added a dispatch method for handling the reset timer action.

我刚刚添加了一个调度方法来处理重置计时器操作。

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

应用商店.js

Here is where things change a bit. I added detailed comments inline where I made changes.

这是事情发生了一些变化的地方。我在进行更改的地方添加了详细的内联注释。

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

There are numerous changes in App.jsx, specifically we have moved the state to the App component from the timer component. Again detailed comments in the code.

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

定时器.jsx

The Timerhas many changes as well since I removed the AppStoreand AppActionsdependencies to make the Timercomponent more reusable. Detailed comments are in the code.

Timer自从我删除了AppStoreAppActions依赖项以使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;


|

|

|

|

Implementation B

实施B

|

|

|

|

Code changes listing:

代码更改列表:

  • app-constants.js
  • timer-actions.js (new)
  • timer-store.js (new)
  • app-store.js
  • App.jsx
  • Timer.jsx
  • app-constants.js
  • timer-actions.js (新)
  • timer-store.js (新)
  • 应用商店.js
  • 应用程序.jsx
  • 定时器.jsx

app-constants.js

app-constants.js

These should probably go in a file named timer-constants.js since they deal with the Timer component.

这些应该放在一个名为 timer-constants.js 的文件中,因为它们处理 Timer 组件。

module.exports = {
  START_TIMER: 'START_TIMER',
  STOP_TIMER: 'STOP_TIMER',
  RESET_TIMER: 'RESET_TIMER',
  TIMEOUT: 'TIMEOUT',
  TICK: 'TICK'
};

timer-actions.js

计时器-actions.js

This module is self-explanatory. I added three events - timeout, tick, and reset. See code for details.

这个模块是不言自明的。我添加了三个事件 - 超时、滴答和重置。详情见代码。

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
    })
  },
};

timer-store.js

timer-store.js

I separated out the timer stuff from the AppStore. This is to make the Timer component a bit more reusable.

我从AppStore. 这是为了使 Timer 组件更具可重用性。

The Timer store keeps track of the following state:

Timer 存储跟踪以下状态:

  • timer status- Can be "start" or "stop"
  • time left- Time left on timer
  • 计时器状态- 可以是“开始”或“停止”
  • 剩余时间 - 计时器剩余时间

The Timer store handles the following events:

定时器存储处理以下事件:

  • The timer start event sets timer status to start.
  • The timer stop event sets timer status to stop.
  • The tick event decrements the time left by 1
  • The timer reset event sets the time left to the default and sets timer status to start
  • 定时器启动事件将定时器状态设置为启动。
  • 定时器停止事件将定时器状态设置为停止。
  • 滴答事件将剩余时间减 1
  • 计时器重置事件将剩余时间设置为默认值并将计时器状态设置为开始

Here is the code:

这是代码:

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

应用商店.js

This could be named pattern-store.js, although you'd need to make some changes for it to be reusable. Specifically, I'm directly listening for the Timer's TIMEOUTaction/event to trigger a pattern change. You likely don't want that dependency if you want to reuse pattern change. For example if you wanted to change the pattern by clicking a button or something.

这可以命名为pattern-store.js,尽管您需要进行一些更改以使其可重用。具体来说,我直接监听定时器的TIMEOUT动作/事件来触发模式更改。如果您想重用模式更改,您可能不想要这种依赖性。例如,如果您想通过单击按钮或其他东西来更改模式。

Aside from that, I just removed all the Timer related functionality from the AppStore.

除此之外,我刚刚从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

应用程序.jsx

Here I just added some buttons for start/stop/reset. On click, a TimerAction is dispatched. So if you clicked the "stop" button, we call TimerAction.stop()

在这里,我只是添加了一些用于启动/停止/重置的按钮。单击时,将调度 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

定时器.jsx

One of the main changes is that we are using a TimerAction and TimerStore instead of the AppAction and AppStore that was used originally. The reason is to try to make the Timer component a bit more reusable.

主要变化之一是我们使用了 TimerAction 和 TimerStore,而不是最初使用的 AppAction 和 AppStore。原因是尝试使 Timer 组件更易于重用。

The Timer has the following state:

计时器具有以下状态:

  • statusTimer status can be "start" or "stop"
  • timeLeftTime left on timer
  • status定时器状态可以是“开始”或“停止”
  • timeLeft计时器剩余时间

Note that I used setTimeoutinstead of setInterval. I find setTimeouteasier to manage.

请注意,我使用了setTimeout而不是setInterval. 我觉得setTimeout更容易管理。

The bulk of the logic is in the _tickmethod. Basically we keep calling setTimeoutso long as the status is "start".

大部分逻辑都在_tick方法中。基本上setTimeout,只要状态为“开始”,我们就会一直调用。

When the timer reaches zero, then we signal the timeoutevent. The TimerStore and AppStore are listening for this event.

当计时器达到零时,我们发出timeout事件信号。TimerStore 和 AppStore 正在监听这个事件。

  1. The TimerStore will merely reset the timer. Same the reset event.
  2. The AppStore will change the pattern.
  1. TimerStore 只会重置计时器。重置事件相同。
  2. AppStore 将改变模式。

If the timer not reached zero, we subtract one second by signaling the "tick" event.

如果计时器没有达到零,我们通过发送“tick”事件信号来减去一秒。

Lastly we need to handle the case where the timer is stopped and then later started. This can be handled through the componentDidUpdatehook. This hook gets called when the component's state changes or the parent components gets re-rendered.

最后,我们需要处理计时器停止然后再启动的情况。这可以通过componentDidUpdate钩子处理。当组件的状态改变或父组件被重新渲染时,这个钩子会被调用。

In the componentDidUpdatemethod, we make sure to start the "ticking" only if the status is "start" and the timeout identifier is undefined. We don't want multiple setTimeouts running.

在该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;

回答by Gil Birman

Don't store state in components

不要在组件中存储状态

One of the main reasons to use flux is to centralize application state. To that end, you should avoid using a component's setStatefunction at all. Furthermore, to the extent that components save their own state, it should only be for state data of a very fleeting nature (For example, you might set state locally on a component that indicates if a mouse is hovering).

使用 Flux 的主要原因之一是集中应用程序状态。为此,您应该完全避免使用组件的setState功能。此外,就组件保存自己的状态而言,它应该只用于非常短暂的状态数据(例如,您可以在组件上本地设置状态以指示鼠标是否悬停)。

Use Action Creators for async operations

使用 Action Creators 进行异步操作

In Flux, stores are meant to be synchronous. (Note that this is a somewhat contentious point among Flux implementations, but I definitely suggest that you make stores synchronous. Once you allow async operation in Stores, it breaks the unidirectional data flow and impairs application reasoning.). Instead, async operation should live in your Action Creator. In your code I see no mention of an Action Creator, so I suspect this might be the source of your confusion. Nevertheless, your actual Timershould live in the Action Creator. If your component needs to effect the timer, it can call a method on the Action Creator, the Action Creator can create/manage the timer, and the timer can dispatch events which will be handled by the store.

在 Flux 中,商店是同步的。(请注意,这在 Flux 实现中有些争议,但我绝对建议您使存储同步。一旦您在 Stores 中允许异步操作,它就会破坏单向数据流并削弱应用程序推理。)。相反,异步操作应该存在于您的Action Creator 中。在你的代码中,我没有看到提到 Action Creator,所以我怀疑这可能是你困惑的根源。尽管如此,您的实际计时器应该存在于 Action Creator 中。如果你的组件需要影响定时器,它可以调用 Action Creator 上的一个方法,Action Creator 可以创建/管理定时器,定时器可以调度将由store处理的事件。

Update: Note that at the 2014 react-conf Flux panel one developer working on a large Flux application said that for that particular application they do allow async data fetching operations in the stores (GETs but not PUTs or POSTs).

更新:请注意,在 2014 年的 react-conf Flux 小组中,一位开发大型 Flux 应用程序的开发人员表示,对于该特定应用程序,他们确实允许在存储中进行异步数据获取操作(GET 但不是 PUT 或 POST)。

Facebook's Flux Flow Chart

Facebook 的流量流程图

回答by Shawn

I would remove the timer from the store, and for now, just manage the patterns there. Your timer component would need a couple small changes:

我会从商店中删除计时器,现在,只需管理那里的模式。您的计时器组件需要进行一些小的更改:

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>
    )
  }
});