定义
- 代码:这里说的代码是指要插入到 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 的问题。代码如下:
/* ------------------------------------------------------------------ Cross-browser set innerHTML method. The HTML code to be inserted can contains script and style tag. Tested browsers: IE 5.5+ Firefox 1.0+ Opera 8.5+ Author: kenxu <ken@ajaxwing.com> Version: 0.1.5 Date: 2006-08-04 Usage: setInnerHTML(element, htmlCode); For more informations, visit: http://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) { // 在这里修改 html return html; }
setInnerHTML 会先执行 beforeInsert,然后才插入代码。在你并不知道最终要插入的代码是什么的情况下,beforeInsert 函数是非常有用的。
复杂版的缺点有两个:第一,代码中的脚本不能访问到代码中的其它 html 元素;第二,复杂版的执行速度和兼容性不如简单版。
简单版
如果脚本中不包含 document.write,则不用保证脚本加载顺序的问题,因为各个浏览器有其自身的加载逻辑,只需确保脚本和样式都生效就可以了。代码如下:
/* * 描述:跨浏览器的设置 innerHTML 方法 * 允许插入的 HTML 代码中包含 script 和 style * 作者:kenxu <ken@ajaxwing.com> * 日期:2006-03-23 * 参数: * el: 合法的 DOM 树中的节点 * htmlCode: 合法的 HTML 代码 * 经测试的浏览器:ie5+, firefox1.5+, opera8.5+ */ 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。