使用 redux 创建秒表

IT技术 javascript reactjs ecmascript-6 redux
2021-05-22 03:53:53

我一直在尝试在 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;
  }
}

我真的很感激任何帮助。

4个回答

我可能会建议采用不同的方式:仅存储计算存储中经过的时间所需的状态,并让组件设置自己时间间隔,无论它们希望更新显示的频率如何。

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

下面是一个例子:

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

您可以在此处查看具有此实现有效 JSBinhttps : //jsbin.com/dupeji/12/edit?js , output

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

更新了 JSFiddle

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

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

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

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

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

  store.dispatch({
    type: 'START_TIMER',
    offset: Date.now(),
    interval
  });
}

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

______

停止计时器

要停止计时器,我们使用清除间隔clearIntervalinitialState再次应用:

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

______

更新了 JSFiddle

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

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

下面是一个倒数计时器组件的示例,它既显示倒计时时钟,又在它到期时执行某些操作。start, tick, 或者stop你可以分派你需要在 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() {...}
}