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

kognpnkq  于 2023-05-21  发布在  Java
关注(0)|答案(1)|浏览(121)

我想实现的是检测屏幕上出现特定更改的精确时间(主要是使用Google Chrome)。例如,我使用$("xelement").show();显示一个项目,或者使用$("#xelement").text("sth new");更改它,然后我想看看当performance.now用户屏幕上出现给定屏幕重绘时,www.example.com()到底是什么。所以我对任何解决方案都持开放态度-下面我主要指的是requestAnimationFrame(rAF),因为这是应该帮助实现这一点的函数,只是它似乎没有;见下文。
基本上,正如我想象的那样,rAF应该在大约0-17毫秒内执行它内部的所有内容(每当下一帧出现在我的标准60 Hz屏幕上时)。此外,timestamp参数应该给予执行时间的值(该值基于与www.example.com()相同的DOMHighResTimeStamp度量performance.now)。
下面是我为此做的众多测试之一: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紧挨着它的www.example.com()开始计算),最奇怪的是,rAF时间戳与我在rAF内部使用performance.now()得到的时间戳完全不同,通常比performance.now()调用 before rAF早大约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中,“before vs.在一些实施例中,“RAF回调开始”的时间大约为1-3 ms或大约为16-17 ms。“之前VS。RAF戳”总是正的,通常在0-3 ms左右,但有时在3-17 ms之间。在IE中,两个差异几乎总是在15-18 ms左右(正)。这些或多或少是不同的PC相同。然而,当我在手机的Chrome上运行它时,只有在那时,它似乎是正确的:“之前与RAF标记”随机地在0-17附近,并且“RAF回调开始”总是在几毫秒之后。
有关更多上下文:这是一个在线响应时间实验,用户使用自己的PC(但我通常限制浏览器为Chrome,所以这是唯一对我来说真正重要的浏览器)。我反复显示各种项目,并测量响应时间为“从元素显示的时刻(当人们看到它时)到他们按下键的时刻”,并从记录的响应时间中计算特定项目的平均值,然后检查某些项目类型之间的差异。这也意味着,如果记录的时间总是在一个方向上有点偏斜,那也没什么关系。总是在元素的实际出现之前3 ms),只要该偏斜对于每个显示器是一致的,因为只有差异才真正重要。1-2毫秒的精度将是理想的,但任何减轻随机“刷新率噪声”(0-17毫秒)将是很好的。
我也尝试了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>

解决方案(基于Kaiido的答案)沿着工作显示示例:

function monkeyPatchRequestPostAnimationFrame() {
  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();
}

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

// here is how I display items
// includes a 100 ms "warm-up"
function item_display() {
  window.needed = true;
  chromeWorkaroundLoop();
  setTimeout(function() {
    var before = performance.now();
    $("#stim_id").text("Random new text: " + Math.round(Math.random()*1000) + ".");
    $("#stim_id").show();
    // I ask for display above, and get display time below
    requestPostAnimationFrame(function() {
      var rPAF_now = performance.now();
      console.log("before vs. rPAF now:", rPAF_now - before);
      console.log("");
      needed = false;
    });
  }, 100);
}

// below is just running example instances of displaying stuff
function example_loop(count) {
  $("#stim_id").hide();
  setTimeout(function() {
    item_display();
    if (count > 1) {
      example_loop(--count);
    }
  }, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
}

example_loop(10);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
<div id="stim_id">Any text</div>

**编辑:**因此,基于经验测量,在所有这些中,turns out所有重要的是rAF循环。rPAF没有真实的的区别。

xwbd5t1u

xwbd5t1u1#

你正在经历的是一个Chrome bug(甚至是两个)。
基本上,当 requestAnimationFrame 回调池为空时,它们将在当前事件循环结束时直接调用它,而无需等待规范要求的实际绘制帧。
要解决这个bug,你可以保持一个不断循环的 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)。

  • [edit]:但是这并不是浏览器真正实现的,请参阅此Q/A了解更多细节。*

所以它不是最精确的,你是对的,在这个回调函数中使用performance.now()应该可以让你更接近实际的绘制时间。
此外,当Chrome在这里面临另一个bug时,可能与第一个bug有关,当他们将此rAF 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循环将修复这个奇怪的错误。
因此,您可能需要检查 maybe incomingrequestPostAnimationFrame method
chrome:flags中启用“Experimental Web Platform features”后,您可以在Chrome 1中访问它。这个方法如果被html标准接受,将允许我们在paint操作发生后立即触发回调。
从那里,你应该在最接近的画。

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);
  }
};

对于那些还没有实现这个建议的浏览器,或者如果这个建议从来没有通过规范来实现,你可以尝试使用Task Scheduling API或MessageEvent来填充它,这应该是下一个事件循环中第一个触发的东西。

// polyfills requestPostAnimationFrame
// requestPostAnimationFrame polyfill
if (typeof requestPostAnimationFrame !== "function") {
  // Either use the Task Scheduling API if available,
  // or fallback to a MessageChannel
  const postTask = (cb) => {
    if (globalThis.scheduler?.postTask) {
      return scheduler.postTask(cb, { priority: "user-blocking" });
    }
    return new Promise((resolve, reject) => {
      const { port1, port2 } = postTask.channel ??= new MessageChannel();
      port1.addEventListener("message", () => {
        try {
          resolve(cb());
        }
        catch(err) {
          reject(err);
        }
      }, { once: true });
      port1.start();
      port2.postMessage("");
    });
  }
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  let scheduled = false; // to make it work from rAF
  let inRAF = false; // to make it work from rAF

  const afterFrame = () => {
    called = false;
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (err) {
        reportError(err);
      }
    });
  }
  // We need to overwrite rAF to let us know we are inside an rAF callback
  // as to avoid scheduling yet an other rAF, which would be one painting frame late
  // We could have hooked an infinite loop on rAF, but this means
  // forcing the document to be animated all the time
  // which is bad for perfs
  const rAF = globalThis.requestAnimationFrame;
  globalThis.requestAnimationFrame = function(...args) {
    if (!scheduled) {
      scheduled = true;
      rAF.call(globalThis, (time) => inRAF = time);
      globalThis.requestPostAnimationFrame(() => {
        scheduled = false;
        inRAF = false;
      });
    }
    rAF.apply(globalThis, args);
  };
  globalThis.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== "function") {
      throw new TypeError("Argument 1 is not callable");
    }
    callbacks.push(callback);
    if (!called) {
      if (inRAF) {
        timestamp = inRAF;
        postTask(afterFrame);
      } else {
        requestAnimationFrame((time) => {
          timestamp = time;
          postTask(afterFrame);
        });
      }
      called = true;
    }
  };
}

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);
  }
};

1.事实证明,这个功能显然已经从Chrome实验中删除了。看看the implementation issue,我不知道为什么,什么时候,也不知道他们是否计划继续工作。

相关问题