注意:一个轻松且可能没用的回答
然而,这个问题本身也似乎相当无聊。还没有提供任何真实的用例,我也想不到任何一个。同样地,在理论上,我的答案可能是有用的,但你更有可能被小行星击中,而不是发现自己需要它。
发布的重点在于提供了一些关于其他解决方案性能的视角。你可以看到,至少需要数百个元素才会开始关注性能。
我的“答案”仅在以下情况下有效:
潜在的“性能问题”
也许“不是循环”要求是指具有不需要您在 JS 中循环可能大量其他项目的解决方案?如果项目数量可能变得非常大,则这可能是一个有效的问题。
假设要测试的区域相对于项目较小,并且有成千上万个可能在内部或外部的项目,循环所有这些项目可能会比较昂贵。特别是如果你需要为每个事件侦听器。
如已经指出,如果存在类似于 document.getElementFromPoint
的本机 API,则最好,因为这无疑比在 JS 中实现要更有效。
但是这种 API 不存在。可能因为在真实世界的用例中找不到需要它的人。
采样帧的点
现在你可以在框架的每个点上使用document.ElementFromPoint
API。然而,与帧的大小成比例地扩展。
但是,我们需要检查每个点来保证检测到所有元素吗?如果元素不能重叠:由于最小元素可能仍具有许多像素高和宽,因此我们可以创建一个具有这些最小值的点网格。只要元素没有变化尺寸(或者只能增大),我们就只需要循环它们一次(以确定最小值),而不是更新时。 请注意,我每次都会循环它们,以考虑设置更改。如果您确定元素具有固定尺寸,则只需在脚本开始时执行一次即可。
当然,现在你必须循环点。然而...
在最理想的情况下,其中最小元素的宽度和高度相等(或更大),您只需要检查4个点。实际上,我在一个函数中使用了这个功能来生成随机立方体,以避免与早期立方体重叠。
它不能用于重叠的元素,因为document.ElementFromPoint
仅知道最顶部的元素。您可以通过临时设置 z-index
来解决这个问题,但我必须停止。
它表现得更好吗?
我不确定是否有必要这样做,但我目前没有看到其他处理大量项目的方法。在最好的情况下,只需要检查4个点(小区域重叠),很难想象另一种方法会更快,如果另一种方法需要在JS中遍历成千上万个元素。即使有几十个点,无论页面上有多少元素,它也可能仍然很“快”。
let allItems = [...document.querySelectorAll('.item')];
const frame = document.getElementById('frame')
function measureLoop() {
const start = performance.now();
const large = document.querySelector('#frame');
const largeRect = large.getBoundingClientRect();
const Left = largeRect.left;
const Right = largeRect.right;
const Top = largeRect.top;
const Bottom = largeRect.bottom;
const items = document.querySelectorAll('#frame ~ *');
let overlappers = [];
items.forEach(item => {
const itemRect = item.getBoundingClientRect();
const l = itemRect.left;
const r = itemRect.right;
const t = itemRect.top;
const b = itemRect.bottom;
if (((l <= Right) && (r >= Left)) && ((t <= Bottom) && (b >= Top))) {
overlappers.push(item);
}
});
document.getElementById('result-loop').innerHTML = overlappers.length;
document.getElementById('time-loop').innerHTML = performance.now() - start;
}
function randomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
function within_frame(frame, items) {
const rect = frame.getBoundingClientRect();
const frameX = rect.left;
const frameY = rect.top;
const frameWidth = frame.clientWidth;
const frameHeight = frame.clientHeight;
const smallestWidth = Math.min(...(items.map(i => i.clientWidth)));
const smallestHeight = Math.min(...(items.map(i => i.clientHeight)));
const set = new Set();
let points = 0;
const lastY = frameHeight + smallestHeight;
const lastX = frameWidth + smallestWidth;
for (let y = 0; y < lastY; y += smallestHeight) {
for (let x = 0; x < lastX; x += smallestWidth) {
points++;
const checkX = Math.min(frameX + x, rect.right)
const checkY = Math.min(frameY + y, rect.bottom)
set.add(document.elementFromPoint(checkX, checkY));
}
}
set.forEach(el => (el === frame || el === document.documentElement || !items.includes(el)) && set.delete(el))
document.getElementById('points').innerHTML = points;
return set;
}
function measure() {
frame.style.zIndex = 1;
const start = performance.now();
const result = within_frame(frame, allItems)
const duration = performance.now() - start
document.getElementById('result').innerHTML = [...result.entries()].length;
document.getElementById('time').innerHTML = duration;
frame.style.zIndex = 3;
}
document.getElementById('measure').addEventListener('click', () => {measure(); measureLoop();})
const overlapsExisting = (el) => {
return within_frame(el, allItems);
}
let failedGenerated = 0;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function spawnCubes() {
frame.style.zIndex = 1;
allItems.forEach(item => item.parentNode.removeChild(item));
const nPoints = document.getElementById('nCubes').value;
const cubeSize = document.getElementById('size').value;
let newItems = [];
let failedGenerated = 0;
for (let i = 0; i < nPoints && failedGenerated < 1000; i++) {
if ((i + failedGenerated) % 100 === 0) {
document.getElementById('nCubes').value = newItems.length;
await sleep(0);
}
const el = document.createElement('div');
el.className = 'item';
el.style.backgroundColor = randomColor();
el.style.top = `${Math.round(Math.random() * 90)}%`;
el.style.left = `${Math.round(Math.random() * 60)}%`;
el.style.width = `${cubeSize}px`;
el.style.height = `${cubeSize}px`;
frame.after(el);
const existingOverlapping = within_frame(el, newItems);
if (existingOverlapping.size > 0) {
i--;
failedGenerated++;
el.parentNode.removeChild(el);
continue;
}
newItems.push(el);
}
console.log('failedAttempts', failedGenerated);
allItems = newItems;
frame.style.zIndex = 3;
document.getElementById('nCubes').value = newItems.length;
}
frame.addEventListener('mouseup', () => {measure(); measureLoop()});
spawnCubes().then(() => {measure(); measureLoop();});
document.getElementById('randomize').addEventListener('click', e => {
spawnCubes().then(measure);
})
#frame {
height: 3rem;
width: 3rem;
display: inline-block;
resize: both;
border: solid black 0.1rem;
overflow: auto;
position: absolute;
z-index: 3;
}
.item {
height: 1rem;
width: 1rem;
position: absolute;
z-index: 2;
}
.controls {
position: fixed;
bottom: 4px;
right: 4px;
text-align: right;
}
<div id="frame"></div>
<div class="controls">
<button id="measure">
measure
</button>
<button id="randomize">
spawn cubes
</button>
<div>
N cubes:
<input id="nCubes" type="number" value="40">
</div>
<div>
Cube size:
<input id="size" type="number" value="16">
</div>
<div>
N inside large:
<span id="result">
</span>
</div>
<div>
Time (ms):
<span id="time">
</span>
</div>
<div>
Points:
<span id="points">
</span>
</div>
<div>
N inside large (loop):
<span id="result-loop">
</span>
</div>
<div>
Time (ms) (loop):
<span id="time-loop">
</span>
</div>
</div>