确切的显示时间:requestAnimationFrame用法和时间表

时间:2018-06-17 09:04:35

标签: javascript animation display timing requestanimationframe

我读了很多关于这个requestAnimationFrame(rAF)的内容,但是我所理解的并不符合我在测试中看到的内容。基本上,正如我想象的那样,rAF应该在大约0-17毫秒内执行其中的所有内容(每当下一帧出现在我的标准60赫兹屏幕上时)。此外,timestamp参数应该给出执行时间的值(并且该值基于与performance.now()相同的DOMHighResTimeStamp度量。)

现在,这是我为此做的众多测试之一:https://jsfiddle.net/gasparl/k5nx7zvh/31/

function item_display() {
    var before = performance.now();
    requestAnimationFrame(function(timest){
        var r_start = performance.now();
        var r_ts = timest;
        console.log("before:", before);
        console.log("RAF callback start:", r_start);
        console.log("RAF stamp:", r_ts);
        console.log("before vs. RAF callback start:", r_start - before);
        console.log("before vs. RAF stamp:", r_ts - before);
        console.log("")
    });
}
setInterval(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);

我在Chrome中看到的是:rAF中的函数总是在0-3毫秒内执行(从紧接着它之前的performance.now()算起),而且,最奇怪的是,rAF时间戳是与我在rAF中的performance.now()完全不同的东西,通常比在rAF之前称为的performance.now()早约0-17毫秒(但有时约为0-之后1毫秒)。

这是一个典型的例子:

before: 409265.00000001397
RAF callback start: 409266.30000001758
RAF stamp: 409260.832 
before vs. RAF callback start: 1.30000000353902
before vs. RAF stamp: -4.168000013974961 

在Firefox和IE中它是不同的。在Firefox中,"之前与RAF回调开始"大概是1-3毫秒或大约16-17毫秒。 "之前与皇家空军的邮票"总是正的,通常在0-3毫秒左右,但有时在3-17毫秒之间。在IE中,两者的差异几乎总是在15-18毫秒左右(正值)。这些或多或少与不同的PC相同。然而,当我在手机的Chrome上运行它时,那时,它似乎似乎是正确的:"之前与RAF邮票"随机在0-17附近," RAF回调开始"总是几毫秒之后。

更多背景信息:我想要实现的只是知道屏幕上出现某个变化的确切时间。例如,我使用$("xelement").show();显示一个项目或使用$("#xelement").text("sth new");更改它,然后我想看看当用户屏幕上出现更改时,performance.now()的确切含义给定的屏幕重绘。因此,如果有比使用rAF更好的方法,我也完全接受替代解决方案。

更多背景信息:这是一个在线响应时间实验。我反复显示各种项目,并从元素显示的时刻(当人们看到它时)到按下键的时刻测量响应时间,并计算平均值。记录特定项目的响应时间,然后检查某些项目类型之间的差异。这也意味着如果记录的时间总是在一个方向上有点倾斜(例如总是在元素的实际出现之前3毫秒),只要这个偏差对于每个显示都是一致的,那么它并不重要,因为只有差异真的很重要。 1-2毫秒的精度将是理想的,但任何减轻随机"刷新率噪声" (0-17 ms)会很好。

我还尝试了jQuery.show()回调,但不考虑刷新率:https://jsfiddle.net/gasparl/k5nx7zvh/67/

var r_start;
function shown() {
    r_start = performance.now();
}
function item_display() {
    var before = performance.now();
    $("#stim_id").show(complete = shown())
    var after = performance.now();
    var text = "before: " + before + "<br>callback RT: " + r_start + "<br>after: " + after + "<br>before vs. callback: " + (r_start - before) + "<br>before vs. after: " + (after - r_start)
    console.log("")
    console.log(text)
    $("p").html(text);
    setTimeout(function(){ $("#stim_id").hide(); }, 500);
}
setInterval(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 800);

使用HTML:

<p><br><br><br><br><br></p>
<span id="stim_id">STIMULUS</span>

1 个答案:

答案 0 :(得分:4)

您正在体验is a Chrome bug(甚至两个)。

基本上,当 requestAnimationFrame 回调池为空时,它们将在当前事件循环结束时直接调用它,而不必等待规格要求的实际绘制框架。 >

要解决此错误,您可以保持一个持续不断的 requestAnimationFrame 循环,但是请注意,这会将您的文档标记为“动画”,并在页面上触发大量副作用(例如在每次屏幕刷新时强制重新绘制)。因此,我不确定您在做什么,但这通常不是个好主意,我希望邀请您仅在需要时运行此动画循环。

let needed = true; // set to false when you don't need the rAF loop anymore

function item_display() {
  var before = performance.now();
  requestAnimationFrame(function(timest) {
    var r_start = performance.now();
    var r_ts = timest;
    console.log("before:", before);
    console.log("RAF callback start:", r_start);
    console.log("RAF stamp:", r_ts);
    console.log("before vs. RAF callback start:", r_start - before);
    console.log("before vs. RAF stamp:", r_ts - before);
    console.log("")
    setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
  });
}
chromeWorkaroundLoop();
item_display();

function chromeWorkaroundLoop() {
  if (needed) {
    requestAnimationFrame(chromeWorkaroundLoop);
  }
};

现在, requestAnimationFrame 回调在下一次绘制之前(实际上是在同一事件循环中)在之前触发,并且TimeStamp参数应代表以下所有绘画的主要任务和微任务之后的时间当前框架已执行,然后开始“更新渲染”子任务(步骤9 here)。

所以这不是您可以拥有的最精确的方法,并且正确的是,在此回调中使用performance.now()应该使您更接近实际绘制时间。

此外,当Chrome在此处遇到另一个可能与第一个错误有关的错误时,当他们确实将此rAF timeStamp设置为...我必须承认我不知道...也许是上一幅画框的timeStamp。

(function() {
let raf_id,
  eventLoopReport = {
    id: 0,
    timeStamp: 0,
    now: 0
  },
  report = {
  	nb_of_loops_between_call_and_start: -1,
  	mouseClick_timeStamp: 0,
    calling_task: {
    	eventLoop: null,
      now: 0
    },
    rAF_task: {
    	eventLoop: null,
      now: 0,
      timeStamp: 0
    }
  };
  
startEventLoopCounter();
  
btn.onclick = triggerSingleFrame;


// increments eventLoop_id at every event loop
// (or at least every time our postMessage loop fires)
function startEventLoopCounter() {
  const channel = new MessageChannel()
  channel.port2.onmessage = e => {
    eventLoopReport.id ++;
    eventLoopReport.timeStamp = e.timeStamp;
    eventLoopReport.now = performance.now();
    channel.port1.postMessage('*');
  };
  channel.port1.postMessage('*');
}

function triggerSingleFrame(e) {
  // mouseClick Event should be generated at least the previous event loop, so its timeStamp should be in the past
	report.mouseClick_timeStamp = e.timeStamp;
	const report_calling = report.calling_task;
  report_calling.now = performance.now();
  report_calling.eventLoop = Object.assign({}, eventLoopReport);

	cancelAnimationFrame(raf_id);
  
	raf_id = requestAnimationFrame((raf_ts) => {
  	const report_rAF = report.rAF_task;
		report_rAF.now = performance.now();
    report_rAF.timeStamp = raf_ts;
    report_rAF.eventLoop = Object.assign({}, eventLoopReport);
    report.nb_of_loops_between_call_and_start = report_rAF.eventLoop.id - report_calling.eventLoop.id;
    // this should always be positive
    report_el.textContent = "rAF.timeStamp - mouse_click.timeStamp: " +
			(report.rAF_task.timeStamp - report.mouseClick_timeStamp) + '\n\n' +
      // verbose
    	JSON.stringify(report, null, 2) ;
  });
}
})();
<button id="btn">flash</button>
<div id="out"></div>
<pre id="report_el"></pre>

再次运行无限的rAF循环将修复此怪异的错误。

所以您可能要检查的一件事是也许是传入的 requestPostAnimationFrame method

chrome:flags中启用“实验性Web平台功能”后,即可在Chrome中访问它。如果该方法被html标准接受,则允许我们在进行绘制操作后 立即触发回调。

从那里,您应该在绘画的最近处。

var needed = true;
function item_display() {
  var before = performance.now();
  requestAnimationFrame(function() {
    requestPostAnimationFrame(function() {
      var rPAF_now = performance.now();
      console.log("before vs. rPAF now:", rPAF_now - before);
      console.log("");
      setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
    });
  });
}

if (typeof requestPostAnimationFrame === 'function') {
  chromeWorkaroundLoop();
  item_display();
} else {
  console.error("Your browser doesn't support 'requestPostAnimationFrame' method, be sure you enabled 'Experimental Web Platform features' in chrome:flags");
}

function chromeWorkaroundLoop() {
  if (needed) {
    requestAnimationFrame(chromeWorkaroundLoop);
  }
};

对于尚未实现此建议的浏览器,或者如果该建议从未通过规范执行,则可以尝试使用MessageEvent对其进行猴子补丁,这应该是在下一个事件循环时首先触发的事情。但是,由于除了运行无限的rAF循环外,我们无法知道是否已经位于rAF回调内部,因此无法从rAF回调内部调用此猴子补丁。

// monkey-patches requestPostAnimationFrame
//!\ Can not be called from inside a requestAnimationFrame callback
function monkeyPatchRequestPostAnimationFrame() {
  console.warn('using a MessageEvent workaround');
  const channel = new MessageChannel();
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  channel.port2.onmessage = e => {
    called = false;
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (e) {}
    });
  }
  window.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Argument 1 is not callable');
    }
    callbacks.push(callback);
    if (!called) {
      requestAnimationFrame((time) => {
        timestamp = time;
        channel.port1.postMessage('');
      });
      called = true;
    }
  };
}

if (typeof requestPostAnimationFrame !== 'function') {
  monkeyPatchRequestPostAnimationFrame();
}

var needed = true;

function item_display() {
  var before = performance.now();
  requestPostAnimationFrame(function() {
    var rPAF_now = performance.now();
    console.log("before vs. rPAF now:", rPAF_now - before);
    console.log("");
    setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
  });
}


chromeWorkaroundLoop();
item_display();

function chromeWorkaroundLoop() {
  if (needed) {
    requestAnimationFrame(chromeWorkaroundLoop);
  }
};

但是,如果您不关心电池电量消耗,而需要从rAF内部调用它,则为:

// monkey-patches requestPostAnimationFrame
// runs an infinite rAF loop for it to be callable inside rAF
function monkeyPatchRequestPostAnimationFrame() {
  console.warn('using a MessageEvent workaround');
  const channel = new MessageChannel();
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  channel.port2.onmessage = e => {
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (e) {}
    });
    // loop in here so that we fire after other rAF loops
    requestAnimationFrame(loop);
  };
  function loop(time) {
    timestamp = time;
    channel.port1.postMessage('');
  }
  window.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Argument 1 is not callable');
    }
    callbacks.push(callback);
  };
  loop();  
}

if (typeof requestPostAnimationFrame !== 'function') {
  monkeyPatchRequestPostAnimationFrame();
}

var needed = true;

function item_display() {
  var before = performance.now();
  requestPostAnimationFrame(function() {
    var rPAF_now = performance.now();
    console.log("before vs. rPAF now:", rPAF_now - before);
    console.log("");
    setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
  });
}

item_display();