createElement相较于innerHTML的优势是什么?

94

在实际应用中,使用createElement相比innerHTML有哪些优势?我问这个问题是因为我认为在性能、代码可读性/可维护性方面,使用innerHTML更加高效,但我的团队已经决定采用createElement作为编码方法。我想了解createElement如何更加高效。


相关链接:https://dev59.com/lWkw5IYBdhLWcg3w1eC7#28136956 - max
5个回答

93

除了安全性等方面的优点,使用createElement而不是修改innerHTML(与仅丢弃已有内容并替换它相对)还有其他几个优点,正如Pekka所提到的:

在附加元素时保留现有DOM元素的引用

当你附加到(或以其他方式修改)innerHTML时,该元素内部的所有DOM节点都必须重新解析和重新创建。如果您保存了任何节点的引用,则它们将基本上无用,因为它们不再显示。

保留附加到任何DOM元素的事件处理程序

这只是最后一个特殊情况(虽然常见)。设置innerHTML不会自动重新连接事件处理程序到新创建的元素,因此您需要自己跟踪它们并手动添加它们。事件委托可以在某些情况下消除此问题。

在某些情况下可能会更简单/更快

如果要进行大量添加操作,则绝对不希望不断重置innerHTML,因为尽管对于简单更改更快,但反复重新解析和创建元素将更慢。解决方法是在字符串中构建HTML,并在完成后一次设置innerHTML。根据情况,字符串操作可能比仅创建元素并将其附加要慢。

此外,字符串操作代码可能更复杂(特别是如果希望它是安全的)。

这是我有时使用的一个函数,可以使使用createElement更方便。

function isArray(a) {
    return Object.prototype.toString.call(a) === "[object Array]";
}

function make(desc) {
    if (!isArray(desc)) {
        return make.call(this, Array.prototype.slice.call(arguments));
    }

    var name = desc[0];
    var attributes = desc[1];

    var el = document.createElement(name);

    var start = 1;
    if (typeof attributes === "object" && attributes !== null && !isArray(attributes)) {
        for (var attr in attributes) {
            el[attr] = attributes[attr];
        }
        start = 2;
    }

    for (var i = start; i < desc.length; i++) {
        if (isArray(desc[i])) {
            el.appendChild(make(desc[i]));
        }
        else {
            el.appendChild(document.createTextNode(desc[i]));
        }
    }

    return el;
}

如果你像这样调用它:

make(["p", "Here is a ", ["a", { href:"http://www.google.com/" }, "link"], "."]);

您将获得与此HTML等效的内容:

<p>Here is a <a href="http://www.google.com/">link</a>.</p>

6
将DOM树片段单独创建,然后在最后一次性附加到实际的DOM上,可以展示出更快的速度优势。 - staticsan
2
我通常避免使用innerHTML,因为它有一些缺点。对于复杂的标记(例如构建表格),我通常编写函数来生成标记的每个部分。例如,我会有一个从数据中生成tr的函数,然后可能会有另一个将行组合成表格的函数。每个函数都可以简单地使用适当的参数调用make。如果性能成为问题,我可以更改函数以返回HTML字符串。 - Matthew Crumley
你能详细说明一下缺点吗? - oninea
我同意@Matthew关于操作现有DOM元素的观点,但是对于创建包含数百行的新大型DOM(如表格),我们应该使用字符串连接方式而不是document.createElement(),这在各种浏览器中都更快。请参见下面的详细分析。 - Sanjeev
在大多数情况下,innerHTMLcreateElement + 设置选项要慢得多,而且会停止错误并处理它,例如文本内可能存在的某些特殊字符需要进行转义,如 &'"<>,而 createElement + textContent 将自动完成此操作。但是,另一方面,使用 innerHTML 需要手动完成这些操作。 - MMMahdy-PAPION
显示剩余4条评论

19

用户bobince在他对jQuery的批评中非常好地列举了许多缺点。

...此外,您可以通过说$(''+message+'')来创建一个

,而不必在文档中创建
并设置文本节点。太好了!只是......等一下。您没有转义HTML,并且可能只是在客户端上创建了跨站脚本攻击安全漏洞。而在您花费如此长时间清理PHP以在服务器端使用htmlspecialchars后,这就成为了一个遗憾。啊好吧,毕竟没有人真正关心正确性或安全性,是吗?

jQuery并不完全对此负责。毕竟,innerHTML属性已经存在很多年了,并且已经证明比DOM更受欢迎。但是该库确实鼓励这种编码风格。

至于性能:InnerHTML肯定会更慢,因为它需要被解析并在内部转换为DOM元素(可能使用createElement方法)。

根据@Pointy提供的quirksmode基准测试, 在所有浏览器中InnerHTML都更快。

至于可读性和易用性,在大多数项目中,您会发现我在大部分情况下会选择使用innerHTML而不是createElement。但正如您所看到的,有许多支持使用createElement的观点。


2
嗯,@Pekka,你真的确定innerHTML速度较慢吗?我知道很长一段时间以来这是绝对错误的。事实上,在某些浏览器中使用innerHTML正因为其显著的性能优势而变得流行,特别是在框架内部。 (猜猜哪个浏览器) - Pointy
@Pointy 很有趣。我没有基准测试,但常识告诉我innerHTML一定会更慢:它必须被解析、验证并转换为DOM元素,这是createElement在第一步完成的操作。不过如果你知道任何相反的基准测试结果,我很乐意改正自己的观点。 - Pekka
3
好的,"quirksmode benchmark" 的测试数据有些陈旧:http://www.quirksmode.org/dom/innerhtml.html - Pointy
我完全同意这种情况毫无道理。 - Pointy
2
感谢Pekka和Pointy的见解。这强化了我对innerHTML更快(除了编码经验)的看法。此外,在数组中使用HTML字符串还提供了额外的性能增益和可读性。我想知道的是createElement是否有使用它的点。 - oninea
显示剩余4条评论

17

虽然 innerHTML 可能更快,但我不认为在可读性或维护性方面更好。将所有内容放入一个字符串中可能会更短,但较短的代码并不总是更易于维护。

当需要创建动态DOM元素时,字符串连接并不能很好地扩展,因为加号和引号的打开和关闭很难追踪。请考虑以下示例:

生成的元素是一个带有两个动态内部span的div。第一个span内部的一个类名(warrior)也是动态的。

<div>
    <span class="person warrior">John Doe</span>
    <span class="time">30th May, 2010</span>
</div>
假设以下变量已经被定义:

var personClass = 'warrior';
var personName = 'John Doe';
var date = '30th May, 2010';

仅使用 innerHTML 并将所有内容组合成一个字符串,我们可以得到:

someElement.innerHTML = "<div><span class='person " + personClass + "'>" + personName + "</span><span class='time'>" + date + "</span></div>";
上述混乱的代码可以通过使用字符串替换来清理,以避免每次打开和关闭字符串。即使是简单的文本替换,我仍然更喜欢使用replace而不是字符串连接。
这是一个简单的函数,它使用键和替换值的对象,并替换它们在字符串中。它假设键是带有$前缀的特殊值。它不进行任何转义或处理 $ 出现在替换值中的边缘情况等。
function replaceAll(string, map) {
    for(key in map) {
        string = string.replace("$" + key, map[key]);
    }
    return string;
}

var string = '<div><span class="person $type">$name</span><span class="time">$date</span></div>';
var html = replaceAll(string, {
    type: personClass,
    name: personName,
    date: date
});
someElement.innerHTML = html;

通过在构造对象时分离属性、文本等内容,可以改进此方法以获取更多的编程控制权。例如,使用MooTools,我们可以将对象属性作为映射传递。这无疑更易于维护,我认为也更易于阅读。jQuery 1.4使用类似的语法来传递初始化DOM对象的映射。

var div = new Element('div');

var person = new Element('span', {
    'class': 'person ' + personClass,
    'text': personName
});

var when =  new Element('span', {
    'class': 'time',
    'text': date
});

div.adopt([person, when]);

我不认为下面的纯DOM方法比上面的任何一种更易读,但它肯定更易于维护,因为我们不必跟踪开/闭引号和众多加号。

var div = document.createElement('div');

var person = document.createElement('span');
person.className = 'person ' + personClass;
person.appendChild(document.createTextNode(personName));

var when = document.createElement('span');
​when.className = 'date​​​​​​';
when.appendChild(document.createTextNode(date));

​div.appendChild(person);
div.appendChild(when);

最易读的版本很可能是使用某种JavaScript模板

<div id="personTemplate">
    <span class="person <%= type %>"><%= name %></span>
    <span class="time"><%= date %></span>
</div>

var div = $("#personTemplate").create({
    name: personName,
    type: personClass,
    date: date
});

@Anurag:对于你的第一个innerHTML示例,我倾向于这样写。我知道它有点长,但就我个人而言,它很容易阅读,甚至片段的结构也得到了保留。你对此有何看法?array.push("<div>"); array.push("<span class='", person + personClass, "'>", personName, "</span>"); array.push("<span class='", time, "'>", date, "</span>"); array.push("</div>"); someElement.innerHTML = array.join(""); - oninea
1
innerHTML 更快吗?根据 http://jsperf.com/innerhtml-vs-createelement-and-appendchild 的测试结果,不是这样的。 - jpillora
我认为这个例子不足以证明任何事情。你需要使用innerHTML和手动构建创建大小和复杂度(深度)不同的DOM,如果结果是一致的,那么这清楚地表明innerHTML更慢。此外,该测试高度倾斜,因为它强制呈现引擎使body标记的整个DOM无效并重新构建它。而在另一个示例中,它仅将新节点附加到DOM中,并且不触及body标记的其他部分。 - Anurag
我认为你刚刚证明了你的观点是错误的。innerHTML版本更易读/修改-只需在适当的位置插入几个换行符即可。 - T4NK3R
由于Matthew Crumley解释的原因和优点,使用document.createDocumentFragment()使createElement比innerHTML更好、更快。https://developer.mozilla.org/en-US/docs/Web/API/Document/createDocumentFragment - Luís Assunção
显示剩余3条评论

14

如果您想在代码中保留引用,应使用createElement。 InnerHTML有时可能会创建难以发现的错误。

HTML代码:

<p id="parent">sample <span id='test'>text</span> about anything</p>

JS 代码:

var test = document.getElementById("test");

test.style.color = "red"; //1 - it works

document.getElementById("parent").innerHTML += "whatever";

test.style.color = "green"; //2 - oooops

1)您可以更改颜色。

2)由于在上面的行中添加了一些innerHTML的内容,所有内容都被重新创建并且您无法再更改颜色或其他任何内容,因为您已经无法访问不存在的内容。为了更改它,您必须再次使用getElementById

请记住,它还会影响任何事件。您需要重新应用事件。

InnerHTML很好,因为它更快并且大部分时间更易读,但您必须小心并谨慎使用。如果您知道自己在做什么,就没问题。


3

模板字符串(Template literals)是另一种选择。

const container = document.getElementById("container");

const item_value = "some Value";

const item = `<div>${item_value}</div>`

container.innerHTML = item;

5
使用此方法时要注意HTML注入。 - sean

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接