4

我想在架子上有一个文档片段/元素,我已经连接了一堆其他元素。然后,每当我想将这些元素系统之一添加到 DOM 时,我都会复制片段,添加唯一的 DOM ID 并附加它。

因此,例如:

var doc = document,
    prototype = doc.createElement(),  // or fragment
    ra = doc.createElement("div"),
    rp = doc.createElement("div"),
    rp1 = doc.createElement("a"), 
    rp2 = doc.createElement("a"),
    rp3 = doc.createElement("a");

ra.appendChild(rp);
rp.appendChild(rp1);
rp.appendChild(rp2);
rp.appendChild(rp3);
rp1.className = "rp1";
rp2.className = "rp2";
rp3.className = "rp3";

prototype.appendChild(ra);

这将创建原型。然后我希望能够复制原型,添加 id 并附加。像这样:

var fr = doc.createDocumentFragment(),
    to_use = prototype; // This step is illegal, but what I want!
                        // I want prototype to remain to be copied again.

to_use.id = "unique_id75";
fr.appendChild(to_use);
doc.getElementById("container").appendChild(fr);

我知道这不合法。我做过小提琴和研究等等,但它不起作用。一篇 SO 帖子建议el = doc.appendChild(el);返回el,但这并没有让我走得太远。

那么……这可能吗?你能创建一个可以重复使用的现成元素吗?还是每次都必须从头开始构建想要添加的 DOM 结构?

本质上,我正在寻找性能提升,因为我正在创造数千个这样的傻瓜:)

谢谢。

4

2 回答 2

22

使用Node.cloneNode

var container = document.getElementById('container');

var prototype = document.createElement('div');
prototype.innerHTML = "<p>Adding some <strong>arbitrary</strong> HTML in"
 +" here just to illustrate.</p> <p>Some <span>nesting</span> too.</p>"
 +"<p>CloneNode doesn't care how the initial nodes are created.</p>";

var prototype_copy = prototype.cloneNode(true);

prototype_copy.id = 'whatever'; //note--must be an Element!

container.appendChild(prototype_copy);

速度提示

您要最小化三个操作:

字符串解析

当您使用innerHTML. innerHTML单独使用时速度很快。由于所有这些 DOM 方法调用的开销,它通常比等效的手动 DOM 构造更快。但是,您不想使用innerHTML内部循环,并且不想将其用于追加。 随着元素的内容变得越来越大element.innerHTML += 'more html',特别是具有灾难性的运行时行为。它还会破坏任何事件或数据绑定,因为所有这些节点都被破坏并重新创建。

因此innerHTML,为方便起见,请使用创建“原型”节点,但对于内部循环,请使用 DOM 操作。要克隆您的原型,请使用prototype.cloneNode(true)which 不会调用解析器。(请注意克隆原型中的 id 属性——当您将它们附加到文档时,您需要确保它们是唯一的!)

文档树修改(重复appendChild调用)

每次修改文档树时,您可能会触发文档窗口的重绘并更新文档 DOM 节点关系,这可能会很慢。相反,将您的追加分批成 aDocumentFragment并仅将其追加到文档 DOM 一次。

节点查找

如果您已经有一个内存中的原型对象并想要修改它的片段,那么无论您使用 DOM 遍历、getElement*. 还是querySelector*.

通过在创建原型时保留对要修改的节点的引用,将这些搜索排除在您的内部循环之外。然后,每当您想要克隆原型的几乎相同副本时,修改您已经引用的节点,然后克隆修改后的原型。

示例模板对象

最重要的是,这是一个基本的(可能是快速的)模板对象,说明了使用cloneNode和缓存节点引用(减少了字符串解析和节点查找的使用)。

为它提供一个带有类名和data-attr="slotname attributename"属性的“原型”节点(或字符串)。类名成为文本内容替换的“槽”;元素data-attr成为属性名称设置/替换的插槽。然后,您可以为该render()方法提供一个对象,其中包含您定义的槽的新值,并且您将获得完成替换的节点的克隆。

示例用法在底部。

function Template(proto) {
    if (typeof proto === 'string') {
        this.proto = this.fromString(proto);
    } else {
        this.proto = proto.cloneNode(true);
    }
    this.slots = this.findSlots(this.proto);
}
Template.prototype.fromString = function(str) {
    var d = document.createDocumentFragment();
    var temp = document.createElement('div');
    temp.innerHTML = str;
    while (temp.firstChild) {
        d.appendChild(temp.firstChild);
    }
    return d;
};
Template.prototype.findSlots = function(proto) {
    // textContent slots
    var slots = {};
    var tokens = /^\s*(\w+)\s+(\w+)\s*$/;
    var classes = proto.querySelectorAll('[class]');
    Array.prototype.forEach.call(classes, function(e) {
        var command = ['setText', e];
        Array.prototype.forEach.call(e.classList, function(c) {
            slots[c] = command;
        });
    });
    var attributes = proto.querySelectorAll('[data-attr]');
    Array.prototype.forEach.call(attributes, function(e) {
        var matches = e.getAttribute('data-attr').match(tokens);
        if (matches) {
            slots[matches[1]] = ['setAttr', e, matches[2]];
        }
        e.removeAttribute('data-attr');
    });
    return slots;
};
Template.prototype.render = function(data) {
    Object.getOwnPropertyNames(data).forEach(function(name) {
        var cmd = this.slots[name];
        if (cmd) {
            this[cmd[0]].apply(this, cmd.slice(1).concat(data[name]));
        }
    }, this);
    return this.proto.cloneNode(true);
};
Template.prototype.setText = (function() {
    var d = document.createElement('div');
    var txtprop = (d.textContent === '') ? 'textContent' : 'innerText';
    d = null;
    return function(elem, val) {
        elem[txtprop] = val;
    };
}());
Template.prototype.setAttr = function(elem, attrname, val) {
    elem.setAttribute(attrname, val);
};



var tpl = new Template('<p data-attr="cloneid id">This is clone number <span class="clonenumber">one</span>!</p>');

var tpl_data = {
    cloneid: 0,
    clonenumber: 0
};
var df = document.createDocumentFragment();
for (var i = 0; i < 100; i++) {
    tpl_data.cloneid = 'id' + i;
    tpl_data.clonenumber = i;
    df.appendChild(tpl.render(tpl_data));
}
document.body.appendChild(df);
于 2012-12-27T04:15:13.160 回答
2

如果 innerHTML 没有更快,我会感到震惊。预编译的模板,例如 lo-dash 或 dot 提供的模板,似乎是个不错的选择!

看看这个简单的例子:http: //jsperf.com/lodash-template

它表明,对于一个相当复杂的模板,使用 lo-dash 的预编译模板进行循环,您可以获得 300,000 次操作/秒。对我来说似乎相当快,而且 JS 更干净。

显然,这只是问题的一部分。这会生成 HTML,实际上插入 HTML 是另一个问题,但再一次,innerHTML 似乎胜过了 cloneNode 和其他基于 DOM 的方法,而且通常代码更干净。 http://jsperf.com/clonenode-vs-innerhtml-redo/2

显然,您可以将这些基准值作为一粒盐。真正重要的是您的实际应用程序。但我建议在下定决心之前尝试多种方法并自己对它们进行基准测试。

注意:很多关于 JSPerf 模板的基准测试都做错了。他们在每次迭代时都重新编译模板,这显然会很慢。

于 2012-12-27T01:58:41.153 回答