为什么 window.getComputedStyle 不调用重新计算样式和重排?

Why doesn't window.getComputedStyle invoke recalculate styles and reflow?

看这个例子:

const startTime = performance.now();
setTimeout(() => console.log(`taken time: ${performance.now() - startTime}ms`))
for(let i = 0; i < 1000; i++){
  const element = document.createElement("div");
  document.body.appendChild(element);
  element.textContent = `Текст n${i}`;
  window.getComputedStyle(document.body);
}

再看看另一个:

const startTime = performance.now();
setTimeout(() => console.log(`taken time: ${performance.now() - startTime}ms`))
for(let i = 0; i < 1000; i++){
  const element = document.createElement("div");
  document.body.appendChild(element);
  element.textContent = `Текст n${i}`;
  window.getComputedStyle(document.body).width;
}

差异很小:在第一种情况下,我只是调用 window.getComputedStyle(document.body) 而没有获得 属性,而在第二种情况下,我使用 width 属性 进行调用。因此,在第一个中我们看不到重新计算样式和回流,但在第二个中我们看到了相反的情况。为什么?

这是因为getComputedStyle(element)实际上returns一个live对象。

const el = document.querySelector("div");
const style = getComputedStyle(el);
console.log("original color:",  style.color);
el.classList.add("colored");
// we use the same 'style' object as in the first step
console.log("with class color:",  style.color);
.colored {
  color: orange;
}
<div>hello</div>

获取此对象不需要执行完整的重新计算,只有它的 getters 会强制执行。

但现在的浏览器比这更聪明,它们甚至不会触发影响正在检查的 CSSStyleDeclaration 对象的 CSSOM 树之外的某些属性的重排。

例如,在下面的示例中,我们可以看到从检查器内部元素的 CSSStyleDeclaration 获取 fontSize 属性 将强制重排影响我们的检查器,同时获取一种形式outside 不会,因为与 width 不同,fontSize 属性 仅受祖先影响,不受兄弟姐妹影响。

function testReflow(func) {
  return new Promise( (res, rej) => {
    const elem = document.querySelector(".reflow-tester");
    // set "intermediary" values
    elem.style.opacity = 1;
    elem.style.transition = "none";
    try { func(elem); } catch(err) { rej(err) }
    elem.style.opacity = 0;
    elem.style.transition = "opacity 0.01s";

    // if the tested func does trigger a reflow
    // the transition will start from 1 to 0
    // otherwise it won't happen (from 0 to 0)    
    elem.addEventListener("transitionstart", (evt) => {
      res(true); // let the caller know the result
    }, { once: true });
    // if the transition didn't start in 100ms, it didn't cause a reflow
    setTimeout(() => res(false), 100);

  });
}

(async () => {
    await new Promise(res=>setTimeout(res, 1000));
  let styles;
    const gCS_recalc_inner = await testReflow(() => {
    return (styles = getComputedStyle(document.querySelector("#inner")));
  });
  console.log("getComputedStyle inner recalc:", gCS_recalc_inner);
    const gCS_inner_prop_recalc = await testReflow(() => {
    return styles.fontSize;
  });
  console.log("getComputedStyle inner getter recalc:", gCS_inner_prop_recalc);
    const gCS_recalc_outer = await testReflow(() => {
    return (styles = getComputedStyle(document.querySelector("#outer")));
  });
  console.log("getComputedStyle outer recalc:", gCS_recalc_outer);
    const gCS_outer_prop_recalc = await testReflow(() => {
    return styles.fontSize;
  });
  console.log("getComputedStyle outer getter recalc:", gCS_outer_prop_recalc);  
  
})().catch(console.error);
.reflow-tester {
  opacity: 0;
}
.hidden {
  display: none;
}
<div class="reflow-tester">Tester<div id="inner"></div></div>
<div id="outer"></div>

在这两种情况下都会触发对 width 的相同检查,因为 width 可能会受到兄弟姐妹的影响:

function testReflow(func) {
  return new Promise( (res, rej) => {
    const elem = document.querySelector(".reflow-tester");
    // set "intermediary" values
    elem.style.opacity = 1;
    elem.style.transition = "none";
    try { func(elem); } catch(err) { rej(err) }
    elem.style.opacity = 0;
    elem.style.transition = "opacity 0.01s";

    // if the tested func does trigger a reflow
    // the transition will start from 1 to 0
    // otherwise it won't happen (from 0 to 0)    
    elem.addEventListener("transitionstart", (evt) => {
      res(true); // let the caller know the result
    }, { once: true });
    // if the transition didn't start in 100ms, it didn't cause a reflow
    setTimeout(() => res(false), 100);

  });
}

(async () => {
    await new Promise(res=>setTimeout(res, 1000));
  let styles;
    const gCS_recalc_inner = await testReflow(() => {
    return (styles = getComputedStyle(document.querySelector("#inner")));
  });
  console.log("getComputedStyle inner recalc:", gCS_recalc_inner);
    const gCS_inner_prop_recalc = await testReflow(() => {
    return styles.width;
  });
  console.log("getComputedStyle inner getter recalc:", gCS_inner_prop_recalc);
    const gCS_recalc_outer = await testReflow(() => {
    return (styles = getComputedStyle(document.querySelector("#outer")));
  });
  console.log("getComputedStyle outer recalc:", gCS_recalc_outer);
    const gCS_outer_prop_recalc = await testReflow(() => {
    return styles.width;
  });
  console.log("getComputedStyle outer getter recalc:", gCS_outer_prop_recalc);  
  
})().catch(console.error);
.reflow-tester {
  opacity: 0;
}
.hidden {
  display: none;
}
<div class="reflow-tester">Tester<div id="inner"></div></div>
<div id="outer"></div>