reactjs 为什么noteName值渲染多次?

2wnc66cl  于 2023-02-08  发布在  React
关注(0)|答案(1)|浏览(100)

我试图显示正在播放的音符的名称。如果我在超时时间内使用console.log(noteName),它将完美地显示每个音符的名称。但是,如果我尝试使用setState,它将呈现太多次。我错过了什么?
这个想法是,每一个音符在琶音显示,因为它正在发挥

const modes = {
    'ionian': [0, 2, 4, 5, 7, 9, 11, 12],
    'dorian': [0, 2, 3, 5, 7, 9, 10, 12],
    'phrygian': [0, 1, 3, 5, 7, 8, 10, 12],
    'lydian': [0, 2, 4, 6, 7, 9, 11, 12],
    'mixolydian': [0, 2, 4, 5, 7, 9, 10, 12],
    'aeolian': [0, 2, 3, 5, 7, 8, 10, 12],
    'locrian': [0, 1, 3, 5, 6, 8, 10, 12],
  };

  const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];

const ArpeggioPlayer = () => {
  const [mode, setMode] = useState('ionian');
  const [isPlaying, setIsPlaying] = useState(false);
  const [context, setContext] = useState(null);
  const [noteName, setNoteName] = useState('');

  useEffect(() => {
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    setContext(new AudioContext());
  }, []);

  const startContext = async () => {
    if (context.state === 'suspended') {
      await context.resume();
    }
  };

  useEffect(() => {
    if (!context || !isPlaying) return;

    startContext();

    const rootNote = 60; // middle C
    const scale = modes[mode];
    let arpeggio = scale.map(i => rootNote + i);

    arpeggio.forEach((note, i) => {
      let newNoteName = noteNames[note % 12];
      setTimeout(() => {
        setNoteName(newNoteName);
        const oscillator = context.createOscillator();
        oscillator.frequency.value = 440 * Math.pow(2, (note - 69) / 12);
        oscillator.connect(context.destination);
        oscillator.start();
        oscillator.stop(context.currentTime + 0.5);
      }, i * 500);
    });
  }, [context, isPlaying, mode, startContext]);
    
  return (
    <div>
      <select value={mode} onChange={e => setMode(e.target.value)}>
        {Object.keys(modes).map(m => (
          <option key={m} value={m}>{m}</option>
        ))}
      </select>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Stop' : 'Start'}
      </button>
     <h1>{noteName}</h1>
    </div>
  );
};

export default ArpeggioPlayer;
yx2lnoni

yx2lnoni1#

无论何时设置状态(例如调用setNoteName),组件重新呈现。每次重新呈现时,都会创建startContext的新示例。由于第二个useEffect块依赖于startContext,因此它会触发forEach循环,从而创建多个超时。但是,之前的超时未清除,因此会触发新超时和之前的超时。并且它们改变状态,这导致设置更多的超时,等等。
您可以将startContext Package 到useCallback中,也可以将其内联到useEffect中,因为它在其他地方不使用。

const { useState, useEffect } = React;

const modes = {
  'ionian': [0, 2, 4, 5, 7, 9, 11, 12],
  'dorian': [0, 2, 3, 5, 7, 9, 10, 12],
  'phrygian': [0, 1, 3, 5, 7, 8, 10, 12],
  'lydian': [0, 2, 4, 6, 7, 9, 11, 12],
  'mixolydian': [0, 2, 4, 5, 7, 9, 10, 12],
  'aeolian': [0, 2, 3, 5, 7, 8, 10, 12],
  'locrian': [0, 1, 3, 5, 6, 8, 10, 12],
};

const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];

const ArpeggioPlayer = () => {
  const [mode, setMode] = useState('ionian');
  const [isPlaying, setIsPlaying] = useState(false);
  const [context, setContext] = useState(null);
  const [noteName, setNoteName] = useState('');

  useEffect(() => {
    const AudioContext = window.AudioContext || window.webkitAudioContext;
    setContext(new AudioContext());
  }, []);

  useEffect(() => {
    if (!context || !isPlaying) return;

    if (context.state === 'suspended') {
      context.resume();
    }

    const rootNote = 60; // middle C
    const scale = modes[mode];
    let arpeggio = scale.map(i => rootNote + i);

    arpeggio.forEach((note, i) => {
      const newNoteName = noteNames[note % 12];
      setTimeout(() => {
        setNoteName(newNoteName);
        const oscillator = context.createOscillator();
        oscillator.frequency.value = 440 * Math.pow(2, (note - 69) / 12);
        oscillator.connect(context.destination);
        oscillator.start();
        oscillator.stop(context.currentTime + 0.5);
      }, i * 500);
    });
  }, [context, isPlaying, mode]);

  return (
    <div>
      <select value={mode} onChange={e => setMode(e.target.value)}>
        {Object.keys(modes).map(m => (
          <option key={m} value={m}>{m}</option>
        ))}
      </select>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Stop' : 'Start'}
      </button>
     <h1>{noteName}</h1>
    </div>
  );
};

ReactDOM
  .createRoot(root)
  .render(<ArpeggioPlayer />);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="root"></div>

您的代码的主要问题是无法在播放过程中停止或切换。我将通过生成音频上下文并在同一个useEffect块中播放来重构您的代码。每当isPlayingmode发生变化时,前一个context就会关闭,并生成一个新的,从而停止当前播放的上下文。
此外,如果有下一个音符,请使用OscillatorNode ended event来播放,而不是使用超时。
一个二个一个一个

相关问题