使用 redux 创建秒表

Posted

技术标签:

【中文标题】使用 redux 创建秒表【英文标题】:Creating a stopwatch with 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;
  

非常感谢任何帮助。

【问题讨论】:

【参考方案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 JSBin:https://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() ...

【讨论】:

以上是关于使用 redux 创建秒表的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Apple Watch(iOS)上创建秒表(计时器)[关闭]

如何创建秒表 Bash 脚本以不断显示经过的时间?

js中秒表无法使用清除间隔方法

JS - 如何为我的秒表编写时间限制功能?

开发板制作秒表计时器---我太难了丶

BackgroundWorker使用秒表更新经过的时间标签,由于调用C#而冻结[重复]