本着知其然知其所以然的态度,我们这次从源码分析一下 element.html() 的工作原理,为什么 innerHTML 不能使其中的脚本执行,而 jQuery 的 html() 却可以,而 zepto.js 的只能执行内联脚本却不能加载外部脚本。
我们使用 jQuery 3.1.0 和 Zepto.js 1.2.0 的源码进行分析
jQuery Github
Zepto.js Github
jQuery
首先看看 html() 方法的主入口:
01
// jQuery/src/manipulation.js
02
03
html: function( value ) {
04
return access( this, function( value ) {
05
var elem = this[ 0 ] || {},
06
i = 0,
07
l = this.length;
08
09
if ( value === undefined && elem.nodeType === 1 ) {
10
return elem.innerHTML;
11
}
12
13
// See if we can take a shortcut and just use innerHTML
14
if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
15
!wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
16
17
value = jQuery.htmlPrefilter( value );
18
19
try {
20
for ( ; i < l; i++ ) {
21
elem = this[ i ] || {};
22
23
// Remove element nodes and prevent memory leaks
24
if ( elem.nodeType === 1 ) {
25
jQuery.cleanData( getAll( elem, false ) );
26
elem.innerHTML = value;
27
}
28
}
29
30
elem = 0;
31
32
// If using innerHTML throws an exception, use the fallback method
33
} catch ( e ) {}
34
}
35
36
if ( elem ) {
37
this.empty().append( value );
38
}
39
}, null, value, arguments.length );
40
},
html() 方法返回了一个闭包函数,它是一个用于 set/get 一个集合的多功能函数
第 9-11 行,表示直接调用 element.html() 而没有加入任何参数时,直接返回当前元素中的内容
第 14 行的 rnoInnerhtml 在前面有定义,rnoInnerhtml = /<script|<style|<link/i,用于匹配不含有 <script>、<style>、<link> 标签的字符串
第 15 行的 wrapMap 用于处理 IE 9 以下版本浏览器的兼容问题,详细见文件 jQuery/src/manipulation/wrapMap.js,此函数不在本次讨论范围内
没有匹配到指定的标签,代码转入 17-30 行,这段代码先过滤 HTML 代码(/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi),之后清空每个元素的内容和绑定的事件,最后用 innerHTML 添加内容。若都没有捕获异常的话,把 elem 变量赋值为 0,以跳过第 36-38 行的代码块
好,前面??抡饷炊啵?衷谥?懒说辈问?邪? <script>、<style>、<link> 后,会在当前元素上先执行 empty(),再使用 append() 进行后续操作。
查看 empty() 方法的实现,发现是直接移除当前元素的绑定事件,释放内存,再删掉元素中的全部 node:
01
// jQuery/src/manipulation.js
02
03
empty: function() {
04
var elem,
05
i = 0;
06
07
for ( ; ( elem = this[ i ] ) != null; i++ ) {
08
if ( elem.nodeType === 1 ) {
09
10
// Prevent memory leaks
11
jQuery.cleanData( getAll( elem, false ) );
12
13
// Remove any remaining nodes
14
elem.textContent = "";
15
}
16
}
17
18
return this;
19
},
接着我们把目光转向 append() 方法:
01
// jQuery/src/manipulation.js
02
03
append: function() {
04
return domManip( this, arguments, function( elem ) {
05
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
06
var target = manipulationTarget( this, elem );
07
target.appendChild( elem );
08
}
09
} );
10
},
show source
append() 方法短得人畜无害,但是它调用了 domManip() 函数,这个函数贴在博客上就显得恶意满满了,因为它有点长。↑ 就是上面被折叠起来的东西,不要轻易点开(雾
其实呢,包括 append() 方法和 domManip() 函数我们都不需要逐行分析,因为我们看到了我们真正感兴趣的东西,嗯,贴在下面:
01
// jQuery/src/manipulation.js
02
if ( hasScripts ) {
03
doc = scripts[ scripts.length - 1 ].ownerDocument;
04
05
// Reenable scripts
06
jQuery.map( scripts, restoreScript );
07
08
// Evaluate executable scripts on first document insertion
09
for ( i = 0; i < hasScripts; i++ ) {
10
node = scripts[ i ];
11
if ( rscriptType.test( node.type || "" ) &&
12
!dataPriv.access( node, "globalEval" ) &&
13
jQuery.contains( doc, node ) ) {
14
15
if ( node.src ) {
16
17
// Optional AJAX dependency, but won't run scripts if not present
18
if ( jQuery._evalUrl ) {
19
jQuery._evalUrl( node.src );
20
}
21
} else {
22
DOMEval( node.textContent.replace( rcleanScript, "" ), doc );
23
}
24
}
25
}
26
}
第 10-12 行,进行简单的判断后,终于到了激动人心的执行脚本步骤了:
如果标签不包含 src 属性,那么就把去除标签的纯脚本传入 DOMEval() 方法执行,就像下面那样。
新建一个 script 元素,把纯脚本写入该元素,最后添加在 <head> 末尾再移除,用以执行这个脚本。
01
// jQuery/src/core/DOMEval.js
02
03
function DOMEval( code, doc ) {
04
doc = doc || document;
05
06
var script = doc.createElement( "script" );
07
08
script.text = code;
09
doc.head.appendChild( script ).parentNode.removeChild( script );
10
}
如果标签包含了 src 属性,那么先判断 jQuery._evalUrl 这个函数是否存在,若存在则调用它,说白了就是又调用了 jQuery.ajax。
01
// jQuery/src/manipulation/_evalUrl.js
02
03
jQuery._evalUrl = function( url ) {
04
return jQuery.ajax( {
05
url: url,
06
07
// Make this explicit, since user can override this through ajaxSetup (#11264)
08
type: "GET",
09
dataType: "script",
10
cache: true,
11
async: false,
12
global: false,
13
"throws": true
14
} );
15
};
发现已经调用了 jQuery.ajax,那么这次对 jQuery 的 html() 方法的探索就到此结束,因为 jQuery.ajax 在 dataType 设置为 script 和 jsonp 时是可以跨域请求 JavaScript 文件并执行的。
Zepto.js
接着我们来看看轻量级(suoshui)的 Zepto.js 是如何表现的。
妈的整篇代码看不到一个分号,特么你就是这样把大小减下来的么,搞得我格式化个代码累得半死,坑,也不知道你是怎么正常工作的…(?°口°)?(┴—┴
依旧找到 html() 的主入口:
01
// Zepto.js/src/zepto.js
02
03
html: function(html){
04
return 0 in arguments ?
05
this.each(function(idx){
06
var originHtml = this.innerHTML
07
$(this).empty().append( funcArg(this, html, idx, originHtml) )
08
}) :
09
(0 in this ? this[0].innerHTML : null)
10
}
相比于 jQuery 的 html() 方法,它这个真是缩水。
看第 4 行,它使用了 0 in arguments 来判断是否传入了参数。经过性能测试,这个方法在数组长度较大时,性能会比 arguments.length 更优,但是毕竟这只是判断数组是否为空,并没有统计出具体长度,性能更好也是应该的。
若没有传入参数,则跳入第 9 行,直接通过 innerHTML 获得其中内容,与 jQuery 一致。否则,也是先调用 empty() 再执行 append() 进行后续操作。
第 7 行的 funcArg() 函数不用在意,因为 Zepto.js 允许 html() 方法的参数为一个函数,其功能只是当参数为函数时调用它,不为函数时直接返回当前字符串:
1
// Zepto.js/src/zepto.js
2
3
function funcArg(context, arg, idx, payload) {
4
return isFunction(arg) ? arg.call(context, idx, payload) : arg
5
}
append() 的实现使用了一个多功能函数,把 “after”、”prepend”、”before”、”append” 四个功能整合在了一起:
01
// Zepto.js/src/zepto.js
02
03
// Generate the `after`, `prepend`, `before`, `append`,
04
// `insertAfter`, `insertBefore`, `appendTo`, and `prependTo` methods.
05
adjacencyOperators.forEach(function(operator, operatorIndex) {
06
var inside = operatorIndex % 2 //=> prepend, append
07
08
$.fn[operator] = function(){
09
// arguments can be nodes, arrays of nodes, Zepto objects and HTML strings
10
var argType, nodes = $.map(arguments, function(arg) {
11
var arr = []
12
argType = type(arg)
13
if (argType == "array") {
14
arg.forEach(function(el) {
15
if (el.nodeType !== undefined) return arr.push(el)
16
else if ($.zepto.isZ(el)) return arr = arr.concat(el.get())
17
arr = arr.concat(zepto.fragment(el))
18
})
19
return arr
20
}
21
return argType == "object" || arg == null ?
22
arg : zepto.fragment(arg)
23
}),
24
parent, copyByClone = this.length > 1
25
if (nodes.length < 1) return this
26
27
return this.each(function(_, target){
28
parent = inside ? target : target.parentNode
29
30
// convert all methods to a "before" operation
31
target = operatorIndex == 0 ? target.nextSibling :
32
operatorIndex == 1 ? target.firstChild :
33
operatorIndex == 2 ? target :
34
null
35
36
var parentInDocument = $.contains(document.documentElement, parent)
37
38
nodes.forEach(function(node){
39
if (copyByClone) node = node.cloneNode(true)
40
else if (!parent) return $(node).remove()
41
42
parent.insertBefore(node, target)
43
if (parentInDocument) traverseNode(node, function(el){
44
if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
45
(!el.type || el.type === 'text/javascript') && !el.src){
46
var target = el.ownerDocument ? el.ownerDocument.defaultView : window
47
target['eval'].call(target, el.innerHTML)
48
}
49
})
50
})
51
})
52
}
53
54
// after => insertAfter
55
// prepend => prependTo
56
// before => insertBefore
57
// append => appendTo
58
$.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){
59
$(html)[operator](this)
60
return this
61
}
62
})
就是上面这辣鸡代码,看不到一个分号…我也是服了,真是佩服得五体投地…
嗯,这代码也没啥好看的,也就是最后的 44-48 行内容,是重点!!
如果是 <script> 标签,并且没有 src 属性!!!喵喵喵喵喵?所以有 src 属性就直接不做任何响应是么…
于是,内联脚本就直接调用 eval() 函数执行了…