灏天阁

帧动画和DOM观察器

· Yin灏

requestAnimationFrame

认识 requestAnimationFrame

设计 web 动画可以有多种选择:

  1. 使用 css3animation + keyframes;
  2. 使用 css3transition;
  3. canvas 作图实现动画;
  4. 借助 jQuery 动画实现;
  5. 使用 js 原生的 window.setTimeout() 或者 window.setInterval() ,通过不间断移动元素位置来实现动画;前提是画面的更新频率要达到每秒 60次才能被肉眼看到流畅的动画效果。

现在 html5 新增了 window.requestAnimationFrame() 方法,该方法用来在页面重绘之前,通知浏览器调用一个指定的函数。

如果想得到连贯的逐帧动画,函数中必须重新调用 requestAnimationFrame()

如果想做到逐帧动画,应该调用该方法,这要求用户设计的动画函数执行先于浏览器重绘动画。通常来说,被调用的频率是每秒 60次,但是一般会遵循 W3C 标准规定的频率。如果是后台标签页面,重绘频率则会大大降低。

requestID = window.requestAnimationFrame(callback); // Firefox23 / IE10 / Chrome / Safari 7
requestID = window.mozRequestAnimationFrame(callback); // Firefox < 23
requestID = window.webkitRequestAnimationFrame(callback); // Old versions Chrome/Webkit

参数说明如下:

  • callback 在每次需要重新绘制动画时,会调用这个参数所指定的函数。这个回调函数会收到一个参数,这个 DOMHighResTimeStamp 类型的参数指示当前时间距离开始触发 requestAnimationFrame 的回调的时间。

  • 返回 requestID 是一个长整型非零值,作为一个唯一的标识符,可以将该值作为参数传给 window.cancelAnimationFrame() 来取消这个回调函数。

我们经常使用 setIntervalsetTimeout 制作动画效果,但是这种方式非常耗费资源,经常会出现卡顿的情况。

requestAnimationFrame 的优势如下:

  1. 经过浏览器优化,动画更流畅。
  2. 窗口没激活,动画将停止,省计算机资源。
  3. 更省电,尤其是对移动端。

requestAnimationFrame 的使用方式:

function animate() {
  //任意操作
  requestAnimationFrame(animate);
}
// 请求动画
requestAnimationFrame(animate);

有时候需要加一些控制,requestAnimationFrame() 可以像 setInterval() 一样返回一个句柄,也可以取消他。

var globalID;
function animate() {
  //任意操作
  globalID = requestAnimationFrame(animate);
}
// 当加时赛开始
globalID = requestAnimationFrame(animate);
// 当停止
cancelAnimationFrame(globalID);

案例:设计进度条

/*
  <div id="test" style="width: 1px;height: 17px;background: #0f0;"></div>
  <input type="button" value="Run" id="run" />
*/
window.requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame; 

var start = null;
var ele = document.getElementById("test");
var progress = 0;
function step(timestamp) {
   progress += 1;
   ele.style.width = progress + '%';
   ele.innerHTML = progress + '%';
   if(progress < 100) {
      requestAnimationFrame(step);
   }
}
requestAnimationFrame(step); // 自动执行
document.getElementById('run').addEventListener('click', function() {
   ele.style.width = '1px';
   progress = 0;
   requestAnimationFrame(step);
}, false);

案例:设计旋转的小球

window.requestAnimationFrame = (function() {
   return window.requestAnimationFrame || window.oRequestAnimationFrame ||
   window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame ||
   window.msRequestAnimationFrame || function() {
      window.setTimeout(callback, 1000/60);
   }
})();

var canvas, context;
init();
animate();
function init() {
   canvas = document.createElement('canvas');
   canvas.style.left = 0;
   canvas.style.top = 0;
   canvas.width = 210;
   canvas.height = 210;
   context = canvas.getContext('2d');
   document.body.appendChild(canvas);
}

function animate() {
   requestAnimationFrame(animate);
   draw();
}

function draw() {
   var time = new Date().getTime() * 0.002;
   var x = Math.sin(time) * 96 + 105;
   var y = Math.cos(time * 0.9) * 96 + 105;
   context.fillStyle = 'pink';
   context.fillRect(0,0,255,255);
   context.fillStyle = 'rgb(255,0,0)';
   context.beginPath();
   context.arc(x, y, 10, 0, Math.PI * 2, true);
   context.closePath();
   context.fill();
}

Mutation Observer

变动观察器,是监视 DOM 变动的接口。当 DOM 对象树发生任何变动时,Mutation Observer 就会接到通知。

Mutation Observer 类似于事件,可以理解为当 DOM 发生变动会触发 Mutation Observer 事件。但是它与事件有一个本质不同,比较如下:

  1. 事件是同步触发的,也就是说 DOM 发生变动立刻会触发相应的事件。
  2. Mutation Observer 则是异步触发,DOM 发生变动以后,并不会马上触发,而是要等到当前所有 DOM 操作都结束后才触发。

这样设计是为了应付 DOM 变动频繁的情况。

Mutation Observer 有以下特点:

  • 等待所有脚本任务完成后才会运行,采用异步方式。
  • 把 DOM 变动记录封装成一个数组进行处理,而不是一条条的个别处理 DOM 变动。
  • 可以观察发生在 DOM 节点的所有变动,也可以观察某一类变动。

使用下面的表达式检查浏览器是否支持 API

var MutationObserver = window.MutationObserver || window.WebkitMutationObserver || window.MozMutationObserver;

var MutationObserverSupper = !!MutationObserver;

使用步骤:

  1. 使用 MutationObserver() 构造函数,新建一个实例,同时指定这个实例的回调函数。
var observer = new MutationObserver(callback);
  1. 使用 observer() 方法指定所要观察的 DOM 元素,以及要观察的特定变动。
var article = document.querySelector('article');
var options = {
  'childList': true,
  'arrtibutes': true
}

上面代码首先要指定所要观察的 DOM 元素时 article,然后指定所要观察的变动是子元素的变动和属性变动,最后,将这两个限定条件作为参数,传入 observer 对象的 observer() 方法。

MutationObserver 所观察的 DOM 变动(即上面 option 对象),包含以下类型,设置值为 布尔值。

  • childList: 子元素的变动。
  • arrtibutes: 属性的变动。
  • characterData: 节点内容或节点文本的变动。

除了变动类型,option 对象还可以设定以下属性,设置值为布尔值。

  • attributeOldValue: 如果为 true,则表示需要记录变动前的属性值。
  • characterDataOldValue: 如果为 true,则表示需要记录变动前的数据值。
  • attributesFilter: 值为一个数组,表示需要观察的特定属性,如 [‘class’, ‘str’]。
  • subtree: 所有下属节点(包括子节点和子节点的子节点)的变动。
  1. 使用 disconnect() 停止观察,发生相应变化时,不再调用回调函数。
observer.disconnect();
  1. 使用过 takeRecord() 清除变动记录,即不再处理未处理的变动。
observer.takeRecord

DOM 对象每次发生变化,就会生成一条变动记录。这个变动记录对应一个 MutationRecord 对象,该对象包含了与变动相关的所有信息。

  • type: 观察变动的类型,如 attribute、characterData、childList。
  • target: 发生变化的 DOM 对象。
  • addedNodes: 新增的 DOM 对象。
  • removeNodes: 删除的 DOM 对象。
  • previousSibling: 前一个同级的 DOM 对象,如果没有则返回 null。
  • nextSibling: 下一个同级的 DOM 对象,如果没有返回 null。
  • attributeName: 发生变动的属性。如果设置了 attibuteFilter,则只返回预先指定的属性。
  • oldValue: 变动前的值。这个属性只对 attribute 和 characterData 变动有效,如果发生 childList 变动,则返回 null。
  1. 使用 MutationObserver 对象时,可能触发的各种事件必须设定的 MutationObserver 选项值说明如下,小括号内的选项为可选项,如下:

案例:观察 DOM 元素

/*
    <div id="div" style="height:100px;width: 100%;background-color:pink"></div>
    <input type="button" name="" value="插入 span 元素" onclick="changeDiv()">
*/
function onchange(mutationRecords, mutationObserver) {
  console.log('onchange');
  console.log(mutationRecords);
  console.log(mutationObserver);
}

var div = document.getElementById('div');
var mo = new window.MutationObserver(onchange);

options = {
  childList: true
};

mo.observe(div, options);

function changeDiv() {
  var span = document.createElement("span");
  span.innerHTML = "我是一个 span 元素";
  div.appendChild(span);
}

案例:观察 DOM 属性

/*
    <a href="#a" id="a1">链接1</a>
    <a href="#b" id="a2">链接2</a>
    <input type="button" onclick="changeA()" value="修改 a 元素">
*/
var a1E1 = document.getElementById('a1');
var a2E2 = document.getElementById('a2');
var attr = ['href'];
var mo = new window.MutationObserver(onchange);
options = {
  attributes: true,
  attributeFilter: attr,
  attributeOldValue: true
}
mo.observe(a1E1, options);
mo.observe(a2E2, options);

function changeA() {
  a1E1.setAttribute('href', "http://www.baidu.com");
  a2E2.setAttribute('href', "http://www.weibo.com");
}

function onchange(mutationRecords, mutationObserver) {
  for(var i = 0; i < mutationRecords.length; i++) {
    console.log("修改前的" + mutationRecords[i].attributeName + "属性为:" + mutationRecords[i].oldValue);
    /*
       修改前的href属性为:#a
       修改前的href属性为:#b
    */
  }
}

- Book Lists -