使用requestAnimationFrame()优化JavaScript动画性能

在JavaScript里曾经只有一种方法来设定定时循环任务:setInterval()。如果你想快速的重复有些动作(但又不是像直接调用for循环那样立即执行),你就需要用到定时调度。最常见的就是动画绘制过程,当动画的绘制速度达到每秒钟60帧时,动画会显得非常的流畅,于是,你需要允许像下面这样一个定时循环任务:

setInterval(function() {
  // 做某些动画任务
}, 1000/60);

但现在有了一个新的、性能更好的方法可以实现这个任务。之前我们曾讲过requestAnimationFrame() 这个方法。那篇文章里是一个系统的介绍,今天将在这里举2个实际例子和用法。

为什么requestAnimationFrame()更好?

  • 浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果。
  • 在一个浏览器标签页里运行一个动画,当这个标签页不可见时,浏览器会暂停它,这会减少CPU,内存的压力,节省电池电量。

最简单的一个使用requestAnimationFrame()的例子

function repeatOften() {
  // 做某些事情
  requestAnimationFrame(repeatOften);
}
requestAnimationFrame(repeatOften);

这个方法一旦启动,它就会递归的调用自己。

启动和停止

requestAnimationFrame 函数能返回一个ID,根据这个ID,你可以停止它的允许,这就像 setTimeoutsetInterval 的用法一样。下面是一个实际可运行的例子:

var globalID;

function repeatOften() {
  document.getElementsByTagName("body").appendChild('#');;
  globalID = requestAnimationFrame(repeatOften);
}

$("#start").on("click", function() {
  globalID = requestAnimationFrame(repeatOften);
});

$("#stop").on("click", function() {
  cancelAnimationFrame(globalID);
});

运行效果是这样的:

不要用低于IE9的浏览器观看此演示,你懂得!

一个稍微复杂的使用requestAnimationFrame()的例子

事实上,在使用 canvas 绘画时这样这个函数会更加适合,能获得更好的结果:

这个演示的复杂在于好几种动画的同时发生。

JavaScript代码

var snake = {
  
  canvas: document.getElementById("canvas"),
  ctx: document.getElementById("canvas").getContext('2d'),
  
  // how big the "squares" will be
  xDis: 0,
  yDis: 0,
  
  // where the square will be drawn
  posX: 0,   
  posY: 0,
  
  repeater: 0, // ID of requestAnimationFrame
    
  divisions: 30, // breaks frame into X × X squares

  init: function() {
    
    // Set up "Two Dimensional" Array to remember what is on and off
    this.memory = new Array(this.divisions-1);
    for (var i = 0; i < (this.divisions+1); i++) {
      this.memory[i] = new Array(this.divisions-1);
    }
 
    // Size the canvas appropriately
    var width = window.innerWidth;
    var height = window.innerHeight;
    snake.canvas.width = width;
    snake.canvas.height = height;
    
    // Size of squares is canvas width broken into equal chunks
    snake.xDis = width/snake.divisions;
    snake.yDis = height/snake.divisions;
                
    // All pink, baby
    this.ctx.fillStyle = "#EA80B0";
  
    // Random starting position
    this.posX = Math.floor(Math.random() * this.divisions);
    this.posY = Math.floor(Math.random() * this.divisions);
    
    // global
    drawLoop = function() {
      snake.repeater = requestAnimationFrame(drawLoop);
      snake.oneMovement();
    }
    drawLoop();
        
  },
  
  drawSquare: function(x, y) {
    // Actually draw it
    snake.ctx.fillRect(x*this.xDis, y*this.yDis, this.xDis, this.yDis);
    
    // Record it in memory
    snake.memory[x][y] = true;
  },
  
  checkPossiblePositions: function() {
  
    var posToReturn = [];
    
    if (this.posX == 0) {
      // can't go left
    } else if (this.memory[this.posX-1][this.posY] == true) {
      // left occupied
    } else {
      posToReturn.push("left");
    }
    
    if (this.posX == this.divisions) {
      // can't go right
    } else if (this.memory[this.posX+1][this.posY] == true) {
      // right occupied
    } else {
      posToReturn.push("right");
    }
    
    if (this.posY == 0) {
      // can't go up
    } else if (this.memory[this.posX][this.posY-1] == true) {
      // top occupied
    } else {
      posToReturn.push("up");
    }
    
    if (this.posY == this.divisions) {
      // can't go down
    } else if (this.memory[this.posX][this.posY+1] == true) {
      // bottom occupied
    } else {
      posToReturn.push("down");
    }
          
    return posToReturn;
    
  },
  
  startNewRound: function() {
    // Stop! 
    cancelAnimationFrame(this.repeater);
    
    // Find new spot
    var newSpot = this.findEmpty();
            
    if (newSpot == "nope") {
      
      // Absolutely done, start over.
      
      // clear canvas
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      
      // start over
      this.init();
      
    } else {
      
      // Start over from new position
      this.posX = newSpot[0];
      this.posY = newSpot[1];

      // Actually restart      
      drawLoop();
      
    }
  },
  
  oneMovement: function() {
    
    this.drawSquare(this.posX, this.posY);
        
    var possiblePos = this.checkPossiblePositions();
      
    var numPossible = possiblePos.length;
      
    if (numPossible == 0) {
      
      this.startNewRound();
      
    } else {
      
      var randomDir = Math.floor(Math.random() * numPossible);
      
      if (possiblePos[randomDir] == "left") {
        this.posX--; 
      }
      if (possiblePos[randomDir] == "right") {
        this.posX++; 
      }
      if (possiblePos[randomDir] == "up") {
        this.posY--; 
      }
      if (possiblePos[randomDir] == "down") {
        this.posY++;
      }
      
    }
    
  },
  
  findEmpty: function() {
    
    for (var x = 0; x < (this.divisions+1); x++) {
      for (var y = 0; y < (this.divisions+1); y++) {      
        if (!this.memory[x][y]) {
           return [x, y]; 
        }
      }
    } 
    
    return "nope";
    
  }
 
}

// need this loop to make sure canvas sizes right on CodePen
setTimeout(function() {
  
  snake.init();
  
}, 10);
阅读余下内容
 

3条回应:“使用requestAnimationFrame()优化JavaScript动画性能”

发表评论

电子邮件地址不会被公开。 必填项已用*标注


京ICP备12002735号