定义
代码:这里说的代码是指要插入到 innerHTML 中的 html 代码,不是 JavaScript 代码
脚本:代码中包含的 JavaScript 脚本,也就是 html 代码中的 <script> 元素
本地脚本:直接写在 html 代码中的 JavaScript 脚本元素,例如:
<script language="javascript">alert("Hello World");<script>
外部脚本:连接到其它 JavaScript 文件的脚本元素
<script language="javascript" src="helloworld.js"><script>
样式:代码中包含的 CSS 样式,也就是 html 代码中的 <style> 元素
问题
很多人都可能遇到过这种情况:使用 innerHTML 来插入 html 代码,如果代码中包含脚本或者样式,这些脚本和样式就会不生效,或者在 IE 上生效在其它浏览器上不生效。原因很简单:不同浏览器对插入 innerHTML 中的脚本和样式有不同的处理方法。
分析
对于 IE,当用 innerHTML 的方式加载的时候,它会立刻执行所有本地脚本,而外部脚本会在后台分别加载。还有,如果 style 或者 script 元素之前没有可显示元素,IE 会滤掉这些元素。
对于 Firefox,所有脚本都放在后台执行,html 代码加载过程不会等待脚本的加载,立刻返回。
对于 Opera,所有脚本按顺序,并且在 html 代码加载时执行。
其次,如果加载的脚本中包含 document.write,会破坏原页面。
针对上面分析,这里给出两个版本的解决方法,它们各有优缺点。
复杂版
复杂版会严格控制代码中各个脚本的加载顺序,并且处理了脚本中包含 document.write 的问题。代码如下:
ken@ajaxwing.comhttp://www.ajaxwing.com/index.php?id=3
var setInnerHTML = (function () {
var element_stack = [];
var input_stack = [];
var html_stack = [];
var timer = null;
var ua = navigator.userAgent.toLowerCase();
var isIE = (ua.indexOf('msie') >= 0 && ua.indexOf('opera') < 0);
var old_document_write = document.write;
var old_document_writeln = document.writeln;
var loding_script = false;
var callback = function () {
if (loding_script) {
return;
}
if (element_stack.length == 0) {
clearInterval(timer);
timer = null;
document.write = old_document_write;
document.writeln = old_document_writeln;
return;
}
var index = element_stack.length - 1;
var input = input_stack[index];
if (input.length == 0) {
input_stack.pop();
var element = element_stack.pop();
var html = html_stack.pop();
element.innerHTML = '';
if (typeof beforeInsert == 'function') {
html = beforeInsert(html);
}
if (html.match(/<script([^>]*>)((.|r|n)*?)</script>/i) != null) {
setInnerHTML(element, html);
return;
}
if (isIE) {
html = '<div style="display:none">for IE</div>' + html;
element.innerHTML = html;
element.removeChild(element.firstChild);
} else {
element.innerHTML = html;
}
return;
}
var item = input[input.length - 1];
if (typeof item == 'string') {
html_stack[index] += item;
input.pop();
} else if (typeof item == 'object') {
if (item.src) {
loding_script = true;
var script = document.createElement('script');
script.src = item.src;
script.__index = index;
if (isIE) {
script.onreadystatechange = script_loaded;
} else {
script.onload = script_loaded;
}
var head = document.getElementsByTagName('head')[0];
head.appendChild(script);
}
if (item.text) {
var script = document.createElement('script');
script.text = item.text;
var head = document.getElementsByTagName('head')[0];
head.appendChild(script);
input.pop();
}
} else {
input.pop();
}
}
var script_loaded = function () {
if (isIE && this.readyState.toLowerCase() != "loaded" && this.readyState.toLowerCase() != "complete") {
return;
}
var index = this.__index;
input_stack[index].pop();
loding_script = false;
}
var new_document_write = function() {
for (var i = 0; i < arguments.length; i++) {
html_stack[element_stack.length - 1] += arguments[i];
}
}
var new_document_writeln = function () {
for (var i = 0; i < arguments.length; i++) {
new_document_write(arguments[i] + "n");
}
}
return function (element, htmlCode) {
element_stack.push(element);
html_stack.push('');
var input = [];
while (true) {
if ((m = htmlCode.match(/<script([^>]*>)((.|r|n)*?)</script>/i)) == null) {
break;
}
input.unshift(htmlCode.substr(0, m.index));
htmlCode = htmlCode.substr(m.index + m[0].length);
if ((m2 = m[1].match(/srcs*=s*(['"]?)([^'">s]*)1/i)) != null) {
input.unshift({src:m2[2]});
} else {
input.unshift({text:m[2]});
}
}
input.unshift(htmlCode);
input_stack.push(input);
if (timer == null) {
document.write = new_document_write;
document.writeln = new_document_writeln;
timer = setInterval(callback, 10);
}
}})();
如果你想在最终插入之前修改一下代码,可以定义一个名为 beforeInsert 的函数,例如:
function beforeInsert (html) {
return html;
}
setInnerHTML 会先执行 beforeInsert,然后才插入代码。在你并不知道最终要插入的代码是什么的情况下,beforeInsert 函数是非常有用的。
复杂版的缺点有两个:第一,代码中的脚本不能访问到代码中的其它 html 元素;第二,复杂版的执行速度和兼容性不如简单版。
简单版
如果脚本中不包含 document.write,则不用保证脚本加载顺序的问题,因为各个浏览器有其自身的加载逻辑,只需确保脚本和样式都生效就可以了。代码如下:
ken@ajaxwing.com
var setInnerHTML = function (el, htmlCode) {
var ua = navigator.userAgent.toLowerCase();
if (ua.indexOf('msie') >= 0 && ua.indexOf('opera') < 0) {
htmlCode = '<div style="display:none">for IE</div>' + htmlCode;
htmlCode = htmlCode.replace(/<script([^>]*)>/gi,
'<script$1 defer>');
el.innerHTML = htmlCode;
el.removeChild(el.firstChild);
} else {
var el_next = el.nextSibling;
var el_parent = el.parentNode;
el_parent.removeChild(el);
el.innerHTML = htmlCode;
if (el_next) {
el_parent.insertBefore(el, el_next)
} else {
el_parent.appendChild(el);
}
}
}
简单版充分利用了浏览器自身的特性,执行效率高,兼容性好。唯一的缺点就是脚本中不能包含 document.write。
应用场合
复杂版非常适合用来插入广告代码。广告代码一般使用 document.write 来直接在页面中插入 html,而 document.write 是阻塞的,也就是说浏览器会等待广告代码执行完毕才显示下面的内容,如果广告的执行速度比较慢或者你的页面中放置了多个广告,那么整个页面的显示时间就会变得很慢,但实际上浏览器很早就已经拿到整个页面了。使用 setInnerHTML 就可以加快整个页面的显示速度,因为浏览器不必等待每一个广告代码执行完毕。
别看简单版只有 20 行代码,但她是非常强大的,保证代码按照各个浏览器自身的逻辑加载。也就是说你也可以插入任意 html 代码,只要脚本中不包含 document.write。
其它解决方法
在这篇文章编写之前,已经有一个比较完善的解决方法《让插入到 innerHTML 中的 script 跑起来》。如果你发现这里提供的解决方法不能满足你的要求,可以先试试它。
下载和测试
下载地址(复杂版)
测试地址(复杂版)
测试地址(简单版)