【问题标题】:Handling a timer in React/Flux在 React/Flux 中处理计时器
【发布时间】:2015-02-20 15:43:26
【问题描述】:

我正在开发一个应用程序,我希望计时器从 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找到。

【问题讨论】:

  • 您需要能够恢复计时器上的剩余时间吗?假设如果商店在服务器上持久化,刷新页面是否应该跟踪剩余时间?如果是这样,timeLeft 可能也属于商店。
  • 我没有在服务器上保存任何东西。我唯一希望它能够启动/暂停/停止计时器。刷新时,它应该再次从 60 秒开始。这是我目前拥有的商店:pastebin.com/MwV6cRbe
  • 如果没有其他组件需要访问 timeLeft,我会将所有这些都保存在 Timer 组件中。然后你可以开始和停止间隔。否则,你需要控制 store 中的时间间隔和 dispatch change 事件。
  • @Shawn 当我的计时器启动时,我想发送一个 CHANGE_PATTERN 事件,所以我可以在我的计时器组件中处理启动和停止(以及将 timeLeft 从商店移到那里)然后执行AppStore.changePattern() 每当我的计时器启动时?或者这是否违背了 Flux 的整个单向流动?对如何正确解决这个问题有点困惑。谢谢!
  • 不确定这是否是 Flux 的方法,但可能会提供由根应用程序管理的启动/停止/暂停/重置状态,并将其作为道具传递给计时器。然后,您可以从根应用程序将单击事件传递给按钮组件。当按下按钮时,更新应用程序的开始/停止/暂停状态,然后触发渲染更新,其中新的开始/停止/暂停状态作为道具传递给计时器。主要是在沉思。

标签: javascript reactjs reactjs-flux


【解决方案1】:

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

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

【讨论】:

  • 您好,谢谢您的回复。我一直在尝试这个,但我仍然无法弄清楚如何正确实施它。我知道我可以将 timeLeft 的东西移动到 Timer 组件,但现在我正在查看按钮来启动/暂停/停止,它们位于它们自己的组件中 ()。或许把所有东西都放在商店里会更好,然后在必要的地方触发动作/事件?
  • 这些按钮是这个组件的子组件吗?如果是这样,您传入回调,因此您可以通过此 Timer 组件控制间隔计时器:。您可能需要处理按钮实现中的点击,例如 this.props.onClick(e)。
  • 它们不是 Timer 组件的子组件,不。我快要放弃了,因为我不知道该怎么做。甚至可能会在这个问题上悬赏,看看是否有人可以向我解释这一点。我真的很喜欢 React/Flux 来构建一个快速原型,但是现在我想做更多“复杂”(简单的计时器,呵呵..)的东西,我碰壁了。可能需要阅读更多相关信息。
  • 好吧,就像我在原始评论中所说的那样,如果多个 cmets 正在使用时间间隔,您可能应该在计时器存储中从外部对其进行管理。该商店将在每个滴答声中调度一个更改事件,并且订阅的组件将使用更改处理程序执行他们需要的操作。
【解决方案2】:

不要在组件中存储状态

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

使用 Action Creator 进行异步操作

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

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

【讨论】:

【解决方案3】:

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

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

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

为了记录,实施 A 是更好的版本。

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

实施 A

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

实施 B

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

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


|

|

实施 A

|

|

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

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

此实现的代码更改列表:

  • app-constants.js
  • app-actions.js
  • app-store.js
  • App.jsx
  • Timer.jsx

app-constants.js

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

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

app-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;

app-store.js

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

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 组件。代码中再次详述 cmets。

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 也有许多变化,因为我删除了 AppStoreAppActions 依赖项以使 Timer 组件更可重用。详细的cmets在代码中。

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-constants.js
  • timer-actions.js (新)
  • timer-store.js (新)
  • app-store.js
  • App.jsx
  • Timer.jsx

app-constants.js

这些可能应该放在一个名为 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

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

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

我从AppStore 中分离出计时器的东西。这是为了让 Timer 组件更具可重用性。

Timer 存储跟踪以下状态:

  • 计时器状态 - 可以是“开始”或“停止”
  • 剩余时间 - 计时器剩余时间

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,尽管您需要对其进行一些更改才能使其可重用。具体来说,我正在直接监听 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。所以如果你点击了“停止”按钮,我们就会调用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 组件更具可重用性。

定时器有以下状态:

  • 状态 计时器状态可以是“开始”或“停止”
  • timeLeft 计时器剩余时间

请注意,我使用了setTimeout 而不是setInterval。我发现setTimeout 更易于管理。

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

当计时器达到零时,我们会发出timeout 事件的信号。 TimerStore 和 AppStore 正在侦听此事件。

  1. TimerStore 只会重置计时器。与重置事件相同。
  2. AppStore 将改变模式。

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

最后,我们需要处理定时器停止然后再启动的情况。这可以通过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;

【讨论】:

  • 非常感谢您的时间和精力。我将回顾这两种实现,并尝试了解我做错了什么,或者至少我的思维过程哪里出错了。您的实施是否遵循 Gil Berman 在他的回答中提到的规则?我没有听说过动作创建者,也不知道不使用 setState。看来我还有很多东西要学习/阅读 Flux。非常感谢!
  • @cabaret 我没有听说过 Action Creators,直到 Gil 提到他们。我也需要研究一下,因为我知道他对异步操作扰乱数据流的意思。我的第二个实现更接近 Gil 提到的规则(不要在组件中存储状态)。
  • 哈,估计我们俩都得调查一下。我将尝试实施您的两个解决方案,看看它们有何不同,最重要的是尝试了解我哪里出错了。我有一种感觉,我不知道 Flux 的一些关键“规则”;话又说回来,它的文档似乎很稀疏。
  • @cabaret 我更新了我的答案。我切换了实现的顺序。 “替代实施”现在是“实施 A”。这是我的首选解决方案。 “实现 B”有一个很大的缺点,即 TimerStore 和 TimerAction 模块本质上是单例的。这意味着您不能同时使用多个计时器组件。
  • 好的,听起来不错。我正在检查(现在是什么)实现 A('alternate')并看到 cmets 关于事情不是很'Flux',所以我试图弄清楚如何'flux'它;)再次感谢您的时间!
猜你喜欢
  • 2015-03-21
  • 2015-11-19
  • 2015-08-06
  • 2014-12-07
  • 2015-08-08
  • 2020-11-29
  • 2015-04-07
  • 2020-10-11
  • 2015-05-16
相关资源
最近更新 更多