javascript - Hur implementerar man en gameloop med requestAnimationFrame över flera React Redux-komponenter?


Har svårt att komma på det bästa sättet att göra det på. Jag kan använda ett rekursivt anrop med requestAnimationFrame för att ha en spel-loop:

export interface Props {
    name: string;
    points: number;
    onIncrement?: () => void;
    onDecrement?: () => void;
}

class Hello extends React.Component<Props, object> {

    constructor(props: Props) {
        super(props);
    }

    render() {
        const { name, points, onIncrement, onDecrement } = this.props;

        return (
            <div className="hello">
                <div className="greeting">
                    Hello {name + points}
                </div>
                <button onClick={onDecrement}>-</button>
                <button onClick={onIncrement}>+</button>
            </div>
        );
    }

    componentDidMount() {
        this.tick();
    }

    tick = () => {
        this.props.onIncrement();
        requestAnimationFrame(this.tick)
    }

}

Men vad om jag vill på varje bildruta:

  • Komponent1 att göra X
  • Komponent2 att göra Y
  • Komponent3 att göra Z

Jag skulle kunna ha en annan loop i varje komponent, men min förståelse är att det är dålig praxis att ha flera requestAnimationFrame-loopar igång och det är en betydande prestandapåverkan.

Så jag är lite förlorad här. Hur kan jag få en annan komponent att använda samma loop? (Om det ens är det bästa sättet att göra det på!)

Svar #1

Du behöver en föräldrakomponent som anropar requestAnimationFrame och itererar över en array av refs till barnkomponenterna som behöver uppdateras vid varje cykel, och anropar dess update (eller vad du nu vill kalla det) metod:

class ProgressBar extends React.Component {

  constructor(props) {
    super(props);
    
    this.state = {
      progress: 0,
    };
  }
  
  update() {
    this.setState((state) => ({
      progress: (state.progress + 0.5) % 100,
    }));
  }  

  render() {
    const { color } = this.props;
    const { progress } = this.state;
    
    const style = {
      background: color,
      width: `${ progress }%`,
    };
    
    return(
      <div className="progressBarWrapper">
        <div className="progressBarProgress" style={ style }></div>
      </div>
    );  
  }
}

class Main extends React.Component {

  constructor(props) {
    super(props);
    
    const progress1 = this.progress1 = React.createRef();
    const progress2 = this.progress2 = React.createRef();
    const progress3 = this.progress3 = React.createRef();
    
    this.componentsToUpdate = [progress1, progress2, progress3];
    this.animationID = null;    
  }
  
  componentDidMount() {  
    this.animationID = window.requestAnimationFrame(() => this.update());  
  }
  
  componentWillUnmount() {
    window.cancelAnimationFrame(this.animationID);
  }
  
  update() {
    this.componentsToUpdate.map(component => component.current.update());
  
    this.animationID = window.requestAnimationFrame(() => this.update());  
  }
  
  render() {
    return(
      <div>
        <ProgressBar ref={ this.progress1 } color="magenta" />
        <ProgressBar ref={ this.progress2 } color="blue" />     
        <ProgressBar ref={ this.progress3 } color="yellow" />       
      </div>
    );
  }
}

ReactDOM.render(<Main />, document.getElementById('app'));
body {
  margin: 0;
  padding: 16px;
}

.progressBarWrapper {
  position: relative;
  width: 100%;
  border: 3px solid black;
  height: 32px;
  box-sizing: border-box;
  margin-bottom: 16px;
}

.progressBarProgress {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
}
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="app"></div>

Tänk dock på att om du försöker göra något för komplext och vill nå 60 fps, kanske React inte är rätt verktyg att använda.

Dessutom är setState asynkront, så när du anropar det så trycker du bara in en uppdatering i en kö som React kommer att bearbeta vid någon tidpunkt, vilket faktiskt kan hända i nästa bildruta eller senare.

Jag profilerade detta enkla exempel för att se om det var fallet och det är det faktiskt inte. Uppdateringarna läggs till i kön (enqueueSetState), men arbetet görs direkt:

Jag misstänker dock att i en verklig applikation där React har fler uppdateringar att hantera eller i framtida versioner av React med funktioner som tidsdelning, asynkrona uppdateringar med prioriteringar... kan renderingen faktiskt ske i en annan bildruta.

Svar #2

En lösning skulle vara att definiera en array såsom callbacks som en del av ditt tillstånd. I början av varje komponents livscykel, lägg till en funktion i denna array som gör det du vill vid varje loop. Sedan kalla varje funktion i din rAF-loop på detta sätt:

update( performance.now())

// Update loop
function update( timestamp ) {
    // Execute callback
    state.callbacks.forEach( cb => cb( ...args )) // Pass frame delta, etc.

    requestAnimationFrame( update )
  }

Med lite arbete kan du anpassa detta enkla exempel för att ge ett sätt att ta bort funktioner från callbacks för att tillåta dynamisk tilläggning/avdragning av rutiner från spel-loopen efter namn eller signatur.

Du kan också skicka ett objekt som omsluter funktionen och som också innehåller ett heltal som du kan använda för att sortera återanropen efter prioritet.

Relaterade artiklar