对于一个大"文本地图的项目" BigPicture,我需要有超过1000个文本输入。 当您点击+拖动时,您可以" pan"显示的区域。
但性能非常差(在Firefox和Chrome上都有):渲染1000多个DOM元素并不快。
当然,另一个具有更好性能的解决方案是:处理<canvas>
,将文本渲染为位图,每次我们要编辑文本时,让我们展示一个独特的DOM <textarea>
,编辑完成后消失,文字再次呈现为位图... 工作正常(我目前正在朝着这个方向努力)但它还需要更多代码,以便在画布上提供编辑。
问题:是否有可能提高在HTML网页上呈现1000多个DOM元素的性能,以便我根本不需要使用<canvas>
? < / p>
或者在使用1000多个DOM元素平移页面时,不可能获得良好的性能吗?
注意:
1)在这里的演示中,我使用<span contendteditable="true">
,因为我想要多行输入+自动调整大小,但渲染性能与标准<textarea>
相同。*
2)作为参考,这就是我创建1000个文本元素的方法。
for (i=0; i < 1000; i++)
{
var blax = (Math.random()-0.5)*3000;
var blay = (Math.random()-0.5)*3000;
var tb = document.createElement('span');
$(tb).data("x", blax / $(window).width());
$(tb).data("y", blay / $(window).height());
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.position = 'absolute';
tb.style.top = blay + 'px';
tb.style.left = blax + 'px';
tb.innerHTML="newtext";
document.body.appendChild(tb);
}
答案 0 :(得分:5)
对于类似这样的东西,你可以使用文档片段,这些是不属于实际DOM树的DOM节点(更多信息可以在这里https://developer.mozilla.org/en-US/docs/Web/API/document.createDocumentFragment找到),所以你可以做所有的设置片段然后附加片段,这片段只会导致一个重新流而不是1000个。
所以这是一个例子 - http://jsfiddle.net/leighking2/awzoz7bj/ - 快速检查运行时间大约需要60-70毫秒才能运行
var currentzoom = 1;
var docFragment = document.createDocumentFragment();
var start = new Date();
for (i=0; i < 1000; i++)
{
var blax = (Math.random()-0.5)*3000;
var blay = (Math.random()-0.5)*3000;
var tb = document.createElement('span');
$(tb).data("x", blax / $(window).width());
$(tb).data("y", blay / $(window).height());
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.position = 'absolute';
tb.style.top = blay + 'px';
tb.style.left = blax + 'px';
tb.innerHTML="newtext";
docFragment.appendChild(tb);
}
document.body.appendChild(docFragment);
var end = new Date();
console.log(end-start)
与原始版本相比,花费了大约645毫秒来运行http://jsfiddle.net/leighking2/896pusex/
更新因此,为了再次提高拖动速度,请将个别编辑保留在DOM之外,以避免每次拖动鼠标时重复1000次的费用
所以这是使用jquery的detach()方法的一种方法(例如http://jsfiddle.net/sf72ubdt/)。这将删除DOM中的元素,但是会将它们的所有属性提供给您,以便您可以操作它们并在以后重新插入它们
redraw = function(resize) {
//detach spans
var spans = $("span").detach();
//now loop other them, because they are no longer attached to the DOM any changes are
//not going to cause a reflow of the page
$(spans).each(function(index) {
var newx = Math.floor(($(this).data("x") - currentx) / currentzoom * $(window).width());
var newy = Math.floor(($(this).data("y") - currenty) / currentzoom * $(window).height());
if (resize) {
displaysize = Math.floor($(this).data("size") / currentzoom);
if (displaysize) {
$(this).css({
fontSize: displaysize
});
$(this).show();
} else
$(this).hide();
}
//changed this from offset as I was getting a weird dispersing effect around the mouse
// also can no longer test for visible but i assume you want to move them all anyway.
$(this).css({
top: newy + 'px',
left: newx + 'px'
});
});
//reattach to the body
$("body").append(spans);
};
更新2 -
因此,为了获得更多性能,您可以缓存窗口宽度和高度,使用vanilla for循环,使用vanilla js来更改跨度的css。现在每次重绘(在chrome上)需要大约30-45毫秒(http://jsfiddle.net/leighking2/orpupsge/),相比我上面的更新,它看到它们大约80-100毫秒(http://jsfiddle.net/leighking2/b68r2xeu/)
所以这里是更新的重绘
redraw = function (resize) {
var spans = $("span").detach();
var width = $(window).width();
var height = $(window).height();
for (var i = spans.length; i--;) {
var span = $(spans[i]);
var newx = Math.floor((span.data("x") - currentx) / currentzoom * width);
var newy = Math.floor((span.data("y") - currenty) / currentzoom * height);
if (resize) {
displaysize = Math.floor(span.data("size") / currentzoom);
if (displaysize) {
span.css({
fontSize: displaysize
});
span.show();
} else span.hide();
}
spans[i].style.top = newy + 'px',
spans[i].style.left = newx + 'px'
}
$("body").append(spans);
};
SNIPPET示例 -
var currentzoom = 1;
var docFragment = document.createDocumentFragment();
var start = new Date();
var positions = []
var end = new Date();
console.log(end - start);
var currentx = 0.0,
currenty = 0.0,
currentzoom = 1.0,
xold = 0,
yold = 0,
button = false;
for (i = 0; i < 1000; i++) {
var blax = (Math.random() - 0.5) * 3000;
var blay = (Math.random() - 0.5) * 3000;
var tb = document.createElement('span');
$(tb).data("x", blax / $(window).width());
$(tb).data("y", blay / $(window).height());
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.position = 'absolute';
tb.style.top = blay + 'px';
tb.style.left = blax + 'px';
tb.innerHTML = "newtext";
docFragment.appendChild(tb);
}
document.body.appendChild(docFragment);
document.body.onclick = function (e) {
if (e.target.nodeName == 'SPAN') {
return;
}
var tb = document.createElement('span');
$(tb).data("x", currentx + e.clientX / $(window).width() * currentzoom);
$(tb).data("y", currenty + e.clientY / $(window).height() * currentzoom);
$(tb).data("size", 20 * currentzoom);
tb.contentEditable = true;
tb.style.fontFamily = 'arial';
tb.style.fontSize = '20px';
tb.style.backgroundColor = 'transparent';
tb.style.position = 'absolute';
tb.style.top = e.clientY + 'px';
tb.style.left = e.clientX + 'px';
document.body.appendChild(tb);
tb.focus();
};
document.body.onmousedown = function (e) {
button = true;
xold = e.clientX;
yold = e.clientY;
};
document.body.onmouseup = function (e) {
button = false;
};
redraw = function (resize) {
var start = new Date();
var spans = $("span").detach();
var width = $(window).width();
var height = $(window).height();
for (var i = spans.length; i--;) {
var span = $(spans[i]);
var newx = Math.floor((span.data("x") - currentx) / currentzoom * width);
var newy = Math.floor((span.data("y") - currenty) / currentzoom * height);
if (resize) {
displaysize = Math.floor(span.data("size") / currentzoom);
if (displaysize) {
span.css({
fontSize: displaysize
});
span.show();
} else span.hide();
}
spans[i].style.top = newy + 'px',
spans[i].style.left = newx + 'px'
}
$("body").append(spans);
var end = new Date();
console.log(end - start);
};
document.body.onmousemove = function (e) {
if (button) {
currentx += (xold - e.clientX) / $(window).width() * currentzoom;
currenty += (yold - e.clientY) / $(window).height() * currentzoom;
xold = e.clientX;
yold = e.clientY;
redraw(false);
}
};
$(function () {
$('body').on('mousedown', 'span', function (event) {
if (event.which == 3) {
$(this).remove()
}
})
});
zoomcoef = function (coef) {
middlex = currentx + currentzoom / 2
middley = currenty + currentzoom / 2
currentzoom *= coef
currentx = middlex - currentzoom / 2
currenty = middley - currentzoom / 2
redraw(true)
}
window.onkeydown = function (event) {
if (event.ctrlKey && event.keyCode == 61) {
zoomcoef(1 / 1.732);
event.preventDefault();
}
if (event.ctrlKey && event.keyCode == 169) {
zoomcoef(1.732);
event.preventDefault();
}
if (event.ctrlKey && event.keyCode == 48) {
zoomonwidget(1 / 1.732);
event.preventDefault();
}
};
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
答案 1 :(得分:1)
恕我直言,我会按照你目前的想法来最大化表现。
原因:1000多个DOM元素将始终限制性能。
是的,编码稍微多一些,但你的表现要好得多。
创建一个包含所有1000个文本的大型屏幕外画布。
使用context.textMeasure
计算相对于图像的所有1000个文本的边界框。
保存有关对象中每个文本的信息
var texts=[];
var texts[0]={ text:'text#0', x:100, y:100, width:35, height:20 }
...
context.drawImage
画布上的图像使用offset-X来“平移”图像。这样,您只有1个canvas元素而不是1000个文本元素。
在mousedown处理程序中,检查鼠标位置是否在任何文本的边界框内。
如果在文本边界框内单击鼠标,则绝对将输入类型文本直接放在画布上的文本上。这样,您只需要1个输入元素,可以重复使用1000个文本中的任何一个。
使用input元素的功能让用户编辑文本。 canvas元素没有原生文本编辑功能,因此不要通过编写画布文本编辑来“重新创建轮子”。
用户完成编辑后,重新计算新编辑文本的边界框并将其保存到文本对象中。
使用新编辑的文本重绘包含所有1000个文本的屏幕外画布,并将其绘制到屏幕画布上。
平移:如果用户拖动屏幕画布,请将屏幕外画布绘制到屏幕画布上,其偏移量等于用户拖动鼠标的距离。平移几乎是瞬间的,因为将屏幕外画布绘制到屏幕画布上 - 视口比移动1000个DOM输入元素要快得多
[添加:编辑和平移的完整示例]
**在全屏模式下观看最佳**
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var texts=[];
var fontSize=12;
var fontFace='arial';
var tcanvas=document.createElement("canvas");
var tctx=tcanvas.getContext("2d");
tctx.font=fontSize+'px '+fontFace;
tcanvas.width=3000;
tcanvas.height=3000;
var randomMaxX=tcanvas.width-40;
var randomMaxY=tcanvas.height-20;
var panX=-tcanvas.width/2;
var panY=-tcanvas.height/2;
var isDown=false;
var mx,my;
var textCount=1000;
for(var i=0;i<textCount;i++){
var text=(i+1000);
texts.push({
text:text,
x:parseInt(Math.random()*randomMaxX),
y:parseInt(Math.random()*randomMaxY)+20,
width:ctx.measureText(text).width,
height:fontSize+2,
});
}
var $textbox=$('#textbox');
$textbox.css('left',-200);
$textbox.blur(function(){
$textbox.css('left',-200);
var t=texts[$textbox.textsIndex]
t.text=$(this).val();
t.width=ctx.measureText(t.text).width;
textsToImage();
});
textsToImage();
$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});
// create one image from all texts[]
function textsToImage(){
tctx.clearRect(0,0,tcanvas.width,tcanvas.height);
for(var i=0;i<textCount;i++){
var t=texts[i];
tctx.fillText(t.text,t.x,t.y)
tctx.strokeRect(t.x,t.y-fontSize,t.width,t.height);
}
redraw();
}
function redraw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(tcanvas,panX,panY);
}
function handleMouseDown(e){
e.preventDefault();
e.stopPropagation();
mx=parseInt(e.clientX-offsetX);
my=parseInt(e.clientY-offsetY);
// is the mouse over a text?
var hit=false;
var x=mx-panX;
var y=my-panY;
for(var i=0;i<texts.length;i++){
var t=texts[i];
if(x>=t.x && x<=t.x+t.width && y>=t.y-fontSize && y<=t.y-fontSize+t.height){
$textbox.textsIndex=i;
$textbox.css({'width':t.width+5, 'left':t.x+panX, 'top':t.y+panY-fontSize});
$textbox.val(t.text);
$textbox.focus();
hit=true;
break;
}
}
// mouse is not over any text, so start panning
if(!hit){isDown=true;}
}
function handleMouseUpOut(e){
e.preventDefault();
e.stopPropagation();
isDown=false;
}
function handleMouseMove(e){
if(!isDown){return;}
e.preventDefault();
e.stopPropagation();
var mouseX=parseInt(e.clientX-offsetX);
var mouseY=parseInt(e.clientY-offsetY);
panX+=mouseX-mx;
panY+=mouseY-my;
mx=mouseX;
my=mouseY;
redraw();
}
body{ background-color: ivory; padding:10px; }
#wrapper{position:relative; border:1px solid blue; width:600px; height:600px;}
#textbox{position:absolute;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Click on #box to edit.<br>Tab to save changes.<br>Drag on non-text.</h4><br>
<div id=wrapper>
<input type=text id=textbox>
<canvas id="canvas" width=600 height=600></canvas>
</div>
<button></button>
答案 2 :(得分:1)
@Shmiddty给出了一个解决方案,它比以前的所有尝试都快得多:所有元素都应该被包装,只需要移动包装器(而不是移动每个元素):
即使有超过1000个DOM元素,它也能顺畅而快速地运行。
var container = document.createElement("div"),
wrapper = document.createElement("div"),
dragging = false,
offset = {x:0, y:0},
previous = {x: 0, y:0};
container.style.position = "absolute";
wrapper.style.position = "relative";
container.appendChild(wrapper);
document.body.appendChild(container);
for (var i = 1000, span; i--;){
span = document.createElement("span");
span.textContent = "banana";
span.style.position = "absolute";
span.style.top = (Math.random() * 3000 - 1000 | 0) + 'px';
span.style.left = (Math.random() * 3000 - 1000 | 0) + 'px';
wrapper.appendChild(span);
}
// Don't attach events like this.
// I'm only doing it for this proof of concept.
window.ondragstart = function(e){
e.preventDefault();
}
window.onmousedown = function(e){
dragging = true;
previous = {x: e.pageX, y: e.pageY};
}
window.onmousemove = function(e){
if (dragging){
offset.x += e.pageX - previous.x;
offset.y += e.pageY - previous.y;
previous = {x: e.pageX, y: e.pageY};
container.style.top = offset.y + 'px';
container.style.left = offset.x + 'px';
}
}
window.onmouseup = function(){
dragging = false;
}
答案 3 :(得分:0)
我只是运行几个测试,似乎用CSS转换移动绝对定位(位置:绝对;)DOM元素(div):translate比通过Canvas移动更快(大约30%)。但我使用CreateJS框架作为canvas作业,所以我的结果可能不适用于其他用例。