【问题标题】:Creating a stopwatch with redux使用 redux 创建秒表
【发布时间】:2016-04-07 05:32:49
【问题描述】:

我一直在尝试在 react 和 redux 中制作秒表。我一直无法弄清楚如何在 redux 中设计这样的东西。

首先想到的是有一个START_TIMER 操作,它将设置初始offset 值。之后,我使用setInterval 一遍又一遍地触发TICK 动作,通过使用偏移量计算已经过去了多少时间,将其添加到当前时间,然后更新offset

这种方法似乎有效,但我不确定如何清除间隔以停止它。另外,这个设计似乎很糟糕,可能有更好的方法来做到这一点。

这是一个完整的JSFiddle,它具有START_TIMER 功能。如果你只是想看看我的减速器现在是什么样子,这里是:

const initialState = {
  isOn: false,
  time: 0
};

const timer = (state = initialState, action) => {
  switch (action.type) {
    case 'START_TIMER':
      return {
        ...state,
        isOn: true,
        offset: action.offset
      };

    case 'STOP_TIMER':
      return {
        ...state,
        isOn: false
      };

    case 'TICK':
      return {
        ...state,
        time: state.time + (action.time - state.offset),
        offset: action.time
      };

    default: 
      return state;
  }
}

非常感谢任何帮助。

【问题讨论】:

    标签: javascript reactjs ecmascript-6 redux


    【解决方案1】:

    我可能会建议以不同的方式进行此操作:仅存储计算存储中经过时间所需的状态,并让组件设置其自己的间隔,以适应它们希望更新显示的频率。

    这将动作调度保持在最低限度——只调度启动和停止(和重置)计时器的动作。请记住,每次调度一个动作时,您都会返回一个新的状态对象,然后每个 connected 组件都会重新渲染(即使它们使用优化来避免在包装的组件)。此外,由于您必须处理所有 TICKs 以及其他操作,因此许多操作分派可能会使调试应用程序状态更改变得困难。

    这是一个例子:

    // Action Creators
    
    function startTimer(baseTime = 0) {
      return {
        type: "START_TIMER",
        baseTime: baseTime,
        now: new Date().getTime()
      };
    }
    
    function stopTimer() {
      return {
        type: "STOP_TIMER",
        now: new Date().getTime()
      };
    }
    
    function resetTimer() {
      return {
        type: "RESET_TIMER",
        now: new Date().getTime()
      }
    }
    
    
    // Reducer / Store
    
    const initialState = {
      startedAt: undefined,
      stoppedAt: undefined,
      baseTime: undefined
    };
    
    function reducer(state = initialState, action) {
      switch (action.type) {
        case "RESET_TIMER":
          return {
            ...state,
            baseTime: 0,
            startedAt: state.startedAt ? action.now : undefined,
            stoppedAt: state.stoppedAt ? action.now : undefined
          };
        case "START_TIMER":
          return {
            ...state,
            baseTime: action.baseTime,
            startedAt: action.now,
            stoppedAt: undefined
          };
        case "STOP_TIMER":
          return {
            ...state,
            stoppedAt: action.now
          }
        default:
          return state;
      }
    }
    
    const store = createStore(reducer);
    

    请注意,动作创建者和缩减程序仅处理原始值,不使用任何类型的间隔或 TICK 动作类型。现在组件可以轻松订阅这些数据并根据需要随时更新:

    // Helper function that takes store state
    // and returns the current elapsed time
    function getElapsedTime(baseTime, startedAt, stoppedAt = new Date().getTime()) {
      if (!startedAt) {
        return 0;
      } else {
        return stoppedAt - startedAt + baseTime;
      }
    }
    
    class Timer extends React.Component {
      componentDidMount() {
        this.interval = setInterval(this.forceUpdate.bind(this), this.props.updateInterval || 33);
      }
    
      componentWillUnmount() {
        clearInterval(this.interval);
      }
    
      render() {
        const { baseTime, startedAt, stoppedAt } = this.props;
        const elapsed = getElapsedTime(baseTime, startedAt, stoppedAt);
    
        return (
          <div>
            <div>Time: {elapsed}</div>
            <div>
              <button onClick={() => this.props.startTimer(elapsed)}>Start</button>
              <button onClick={() => this.props.stopTimer()}>Stop</button>
              <button onClick={() => this.props.resetTimer()}>Reset</button>
            </div>
          </div>
        );
      }
    }
    
    function mapStateToProps(state) {
      const { baseTime, startedAt, stoppedAt } = state;
      return { baseTime, startedAt, stoppedAt };
    }
    
    Timer = ReactRedux.connect(mapStateToProps, { startTimer, stopTimer, resetTimer })(Timer);
    

    您甚至可以以不同的更新频率在同一数据上显示多个计时器:

    class Application extends React.Component {
      render() {
        return (
          <div>
            <Timer updateInterval={33} />
            <Timer updateInterval={1000} />
          </div>
        );
      }
    }
    

    您可以在此处看到带有此实现的working JSBinhttps://jsbin.com/dupeji/12/edit?js,output

    【讨论】:

    • 我为我迟到的评论道歉,但非常感谢!通读所有这些确实帮助我更好地理解了我应该如何构建/设计所有这些。如果你不介意的话,我有两个问题。首先是为什么如果我使用null而不是undefined,上面的代码不起作用?其次,我对clearInterval(this.interval); 这行有点不确定。 this.interval 是在哪里定义的?或者你的意思是在上面做this.interval = setInterval()?再次感谢,这意味着你会不遗余力地做到这一点!
    • @meh_programmer 我使用了undefined,所以getElapsedTime 中的默认参数有效(传递 undefined 使其使用默认值,但传递 null 时并非如此)。你对间隔的看法是对的——我会解决的! :)
    • 快速注意:看起来你在减速器中有非纯函数:新日期():“减速器保持纯净是非常重要的。你不应该在减速器内做的事情......”来自文档github.com/reactjs/redux/blob/master/docs/basics/Reducers.md 我认为最佳实践是在 ActionCreators 中包含所有杂质github.com/reactjs/redux/issues/1088
    • 我不久前赞成这个答案,但经过进一步思考,我不同意这种方法。当您在每次调度时创建一个新的状态对象时,如果您使用 mapStateToProps,依赖于未更改状态属性的组件将不会重新渲染。此外,组件应该根据状态变化重新渲染。在您的方法中,没有状态更改,我们依靠间隔强制更新来为我们保留该“状态”。另外,你不能序列化这样的状态。
    • 这是一个很棒的秒表示例。我会稍微更改减速器,这样当您多次发送stopTimer 时,计时器不会更新。 see it here
    【解决方案2】:

    如果您要在更大的应用程序中使用它,那么我会使用 requestAnimationFrame 而不是 setInterval 来解决性能问题。当您显示毫秒时,您会在移动设备上注意到这一点,而不是在桌面浏览器上。

    更新的 JSFiddle

    https://jsfiddle.net/andykenward/9y1jjsuz

    【讨论】:

    • 是的,这是一个更大的应用程序的一部分。非常感谢,我一直认为requestAnimationFrame 是用来和canvas 做事的,我不知道我可以在这种情况下使用它。点赞!
    【解决方案3】:

    您想使用clearInterval 函数,该函数获取对setInterval(唯一标识符)的调用结果,并停止该间隔继续执行。

    因此,与其在start() 中声明setInterval,不如将​​其传递给reducer,以便它可以将其ID 存储在状态中:

    interval 作为操作对象的成员传递给调度程序

    start() {
      const interval = setInterval(() => {
        store.dispatch({
          type: 'TICK',
          time: Date.now()
        });
      });
    
      store.dispatch({
        type: 'START_TIMER',
        offset: Date.now(),
        interval
      });
    }
    

    interval 存储在START_TIMER action reducer 中的新状态

    case 'START_TIMER':
      return {
        ...state,
        isOn: true,
        offset: action.offset,
        interval: action.interval
      };
    

    ______

    根据interval渲染组件

    传入interval作为组件的属性:

    const render = () => {
      ReactDOM.render(
        <Timer 
          time={store.getState().time}
          isOn={store.getState().isOn}
          interval={store.getState().interval}
        />,
        document.getElementById('app')
      );
    }
    

    然后我们可以检查out组件内的状态,根据是否有属性interval来渲染它:

    render() {
      return (
        <div>
          <h1>Time: {this.format(this.props.time)}</h1>
          <button onClick={this.props.interval ? this.stop : this.start}>
            { this.props.interval ? 'Stop' : 'Start' }
          </button>
        </div>
      );
    }
    

    ______

    停止计时器

    要停止计时器,我们使用clearInterval 清除间隔,然后再次应用initialState

    case 'STOP_TIMER':
      clearInterval(state.interval);
      return {
        ...initialState
      };
    

    ______

    更新的 JSFiddle

    https://jsfiddle.net/8z16xwd2/2/

    【讨论】:

    • 非常感谢您的回答和解释!这几乎就是我想要做的。我还有一个问题——你会说使用setInterval 触发另一个动作的动作是不好的做法吗?
    • @meh_programmer 可能,是的。 This discussion on GitHub 似乎建议将间隔逻辑放在订阅商店的单独“业务逻辑”对象中会更明智。
    【解决方案4】:

    与 andykenward 的回答类似,我会使用 requestAnimationFrame 来提高性能,因为大多数设备的帧速率仅为每秒 60 帧左右。但是,我会尽可能少地投入 Redux。如果您只需要间隔来调度事件,您可以在组件级别而不是在 Redux 中完成所有操作。请参阅 this answer 中 Dan Abramov 的评论。

    下面是倒计时计时器组件的示例,它既显示倒计时时钟,又在它到期时执行某些操作。在 starttickstop 中,您可以调度需要在 Redux 中触发的事件。我只在计时器应该启动时安装这个组件。

    class Timer extends Component {
      constructor(props) {
        super(props)
        // here, getTimeRemaining is a helper function that returns an 
        // object with { total, seconds, minutes, hours, days }
        this.state = { timeLeft: getTimeRemaining(props.expiresAt) }
      }
    
      // Wait until the component has mounted to start the animation frame
      componentDidMount() {
        this.start()
      }
    
      // Clean up by cancelling any animation frame previously scheduled
      componentWillUnmount() {
        this.stop()
      }
    
      start = () => {
        this.frameId = requestAnimationFrame(this.tick)
      }
    
      tick = () => {
        const timeLeft = getTimeRemaining(this.props.expiresAt)
        if (timeLeft.total <= 0) {
          this.stop()
          // dispatch any other actions to do on expiration
        } else {
          // dispatch anything that might need to be done on every tick
          this.setState(
            { timeLeft },
            () => this.frameId = requestAnimationFrame(this.tick)
          )
        }
      }
    
      stop = () => {
        cancelAnimationFrame(this.frameId)
      }
    
      render() {...}
    }
    

    【讨论】:

      猜你喜欢
      • 2023-04-11
      • 1970-01-01
      • 2017-02-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-18
      • 1970-01-01
      相关资源
      最近更新 更多