基于这个问题:我如何知道 IntersectionObserver 的滚动方向?
我试图在可观察的回调中重现一些布局/重排案例,但我做不到,所以我试图简化用例并最终提出这个问题。
我正在阅读保罗·爱尔兰 what-forces-layout.md 的要点,我的问题非常简单。
在 body 元素上没有回调的情况下输入肯定会触发布局,请参阅以下示例:
元素焦点() 触发器布局
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<input type="text">
<script type="text/javascript">
var elementB = document.querySelector('input');
elementB.focus();
</script>
</body>
</html>
查看 Chrome 性能记录
但是,如果将focus
事件包装在单击回调中,则不会触发布局/重排。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<input type="text">
<script type="text/javascript">
var elementB = document.querySelector('input');
function onClick() {
elementB.focus();
}
document.addEventListener('click', onClick);
</script>
</body>
</html>
查看 Chrome 性能记录
所以这就是我的问题,为什么不触发布局/回流?
我不确定如果我的问题是正确的,但在情况 1 中,当解析器开始执行脚本时,DOMContentLoaded 尚未触发,它仍在解析文档的其余部分。同时,您在elemB
上调用focus
,您将立即触发布局流。
在情况 2 中,除非您单击文档本身,否则根本不会调用onClick
函数。您可以通过打开您提供的小提琴上的"油漆闪烁">来验证这一点。仅当您单击时,输入才会变为绿色。
而在第一种情况下,您会在开始时看到输入的短暂闪烁(这是您对.focus的调用),然后是整个documentElement(在DOMContentLoaded)。
在第二种情况下,您只有整个 documentElement 闪烁一次(在 DOMContentLoaded 上,前提是没有其他任何东西触发重排/重绘加载事件),然后每次单击时只有一次输入元素。
附注:
现在据我所知,我已经在我的本地机器上尝试了您的 2 个案例,有趣的是,在您的第一个案例中,我在 DOMContentLoaded 之后立即看到了2 个布局活动。
但是,如果我注释掉您的案例 1 中的行elementB.focus();
并再次记录,我再次看到 2 个布局活动。
据我了解,浏览器将在开始时执行 2 个布局操作,一旦它开始解析正文,然后围绕 DOMContentLoaded。如果任何同步强制布局垃圾桶是由javascript完成的(通过调用链接中列出的任何方法/属性),浏览器将尝试批处理这些操作。
为了测试此行为,我修改了您的第一种情况,如下所示:
!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<input type="text" style="position:relative;top:0px;">
<script type="text/javascript">
var elementB = document.querySelector('input');
elementB.focus();
</script>
<script async="true">
setTimeout(function(){
elementB.style.top = parseInt(elementB.style.top) + 5 + "px";
},500)
</script>
</body>
</html>
现在将要发生的事情是,您将在加载事件(~500ms)之后立即进行第三个布局活动(异步是不必要的)。但是,如果您要使setTİmeout0ms,您将再次获得2个布局活动!(如果您看到 3 个布局,可能无法保证微任务队列行为,要强制同步布局,请删除第二个脚本标记内的异步属性和 setTimeout 换行)。底线:所以浏览器对其进行批处理,或者至少这是我从此示例中看到的。
对于您的第二种情况,当我以您发布的方式记录它时,我没有看到布局活动(像以前一样 2 个布局)是正确的。但我看到的是一致的样式重新计算 + 更新布局树 + 每次事件后的绘画。这让我认为,一旦更新了布局树,如果不需要布局垃圾,则不会重新计算。为了测试该行为,我更改了您的第二个脚本,如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<input type="text" style="position:relative;top:0px;">
<script type="text/javascript">
var elementB = document.querySelector('input');
function onClick() {
elementB.focus();
elementB.style.top = parseInt(elementB.style.top) + 5 + "px";
}
document.addEventListener('click', onClick);
</script>
</body>
</html>
在这里,每次单击文档时,输入框将向下移动 5 像素。如果您在 10 秒内记录多次点击事件,您将看到很多更新布局树 + 重新绘制和布局垃圾。这让我认为布局垃圾是在更新布局树后完成的,如果只是必要的。
结论(我可能大错特错)
- 浏览器将尝试在解析 HTML 期间批量布局活动。
- element.focus 将触发重绘 + 更新布局树,但不能保证布局垃圾(至少从这些示例中)
在这种情况下,很难确定为什么开发工具没有注意到重排,这可能是因为布局没有变化,重排算法短路了,开发工具省略了它。也可能是别的东西...
但是回流会发生,至少在必须的情况下。
要测试回流,IMO的最佳方法是触发需要它的东西,而CSS过渡就是这样的东西。
var input = document.querySelector('input');
// test our logic without anything that should trigger a reflow
noreflow.onclick = function() {
// from 20px in CSS
test.style.transition = 'none';
test.style.width = '100px'; // should be ignored
test.style.transition = 'width 1s linear';
test.style.width = '20px';
// should go from 20px to 20px => no transition
};
// test with manually triggered reflow
reflow.onclick = function() {
// from 20px in CSS
test.style.transition = 'none';
test.style.width = '100px';
input.focus(); // reflow
// now should be computed as 100px
test.style.transition = 'width 1s linear';
test.style.width = '20px';
// now that should move
}
#test {
width: 20px;
height: 20px;
background: red;
}
<input type="text"><br>
<button id="noreflow">no reflow</button>
<button id="reflow">reflow</button>
<div id="test"></div>