jQuery中.html()方法的工作原理

本着知其然知其所以然的态度,我们这次从源码分析一下 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() 函数执行了…

时间: 2024-10-28 03:56:50

jQuery中.html()方法的工作原理的相关文章

Python中各种方法的运作原理解析

         这篇文章主要介绍了深入理解Python中各种方法的运作原理,包括抽象方法和静态方法和类方法等之间异同的比较,需要的朋友可以参考下             方法在Python中是如何工作的         方法就是一个函数,它作为一个类属性而存在,你可以用如下方式来声明.访问一个函数: ? 1 2 3 4 5 6 7 8 >>> class Pizza(object): ... def __init__(self, size): ... self.size = size

jquery中load方法的用法及注意事项说明

 本篇文章主要是对jquery中load方法的用法及注意事项进行了详细介绍,需要的朋友可以过来参考下,希望对大家有所帮助 调用load方法的完整格式是:load( url, [data], [callback] ),其中  url:是指要导入文件的地址.  data:可选参数:因为Load不仅仅可以导入静态的html文件,还可以导入动态脚本,例如PHP文件,所以要导入的是动态文件时,我们可以把要传递的参数放在这里.  callback:可选参数:是指调用load方法并得到服务器响应后,再执行的另

Yii中srbac权限扩展模块工作原理与用法分析_php实例

本文实例讲述了Yii中srbac权限扩展模块工作原理与用法.分享给大家供大家参考,具体如下: 1. 设置权限规则表:可放在module模块配置文件里面 public function init() { //操作权限表,必须存在以下字段: //itemname角色名/ID, //type授权项目类型/1(任务)或者2(角色), //bizrule权限/逻辑运算表达式为false是有权限操作, //data数据/YII暂无利用 Yii::app()->authManager->itemTable

jquery中load方法的用法及注意事项说明_jquery

调用load方法的完整格式是:load( url, [data], [callback] ),其中 url:是指要导入文件的地址. data:可选参数:因为Load不仅仅可以导入静态的html文件,还可以导入动态脚本,例如PHP文件,所以要导入的是动态文件时,我们可以把要传递的参数放在这里. callback:可选参数:是指调用load方法并得到服务器响应后,再执行的另外一个函数. 一:如何使用data 1.加载一个php文件,该php文件不含传递参数 $("#myID").load(

Yii中srbac权限扩展模块工作原理与用法分析

本文实例讲述了Yii中srbac权限扩展模块工作原理与用法.分享给大家供大家参考,具体如下: 1. 设置权限规则表:可放在module模块配置文件里面 public function init() { //操作权限表,必须存在以下字段: //itemname角色名/ID, //type授权项目类型/1(任务)或者2(角色), //bizrule权限/逻辑运算表达式为false是有权限操作, //data数据/YII暂无利用 Yii::app()->authManager->itemTable

JQuery中clone方法复制节点

  本文实例讲述了JQuery中clone方法复制节点.分享给大家供大家参考.具体如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DT

JQuery中Text方法用法实例分析

  本文实例讲述了JQuery中Text方法用法.分享给大家供大家参考.具体如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&g

浅谈jQuery中replace()方法

  这篇文章主要介绍了jQuery中replace()方法用法,实例分析了replace()方法的功能.定义及匹配元素去替换指定内容的方法,需要的朋友可以参考下 今天在读jquery源码时,发现一个以前自己不曾注意过得问题,就是replece()的第二个参数为函数时的问题,以前只是知道replace()的第二个参数可以为函数,但是不知道该怎么操作,今天看到源码里用到了函数作为replace()的第二个参数时,感觉自己读起来比较吃力,于是准备整理下这个函数... 语法 stringObject.r

jQuery中next方法用法讲解

  jQuery中next方法用法讲解         这篇文章主要介绍了jQuery中next方法用法,实例分析了jQuery中next方法的功能.定义及相关使用技巧,需要的朋友可以参考下 本文实例讲述了jQuery中next方法用法.分享给大家供大家参考.具体分析如下: 这里演示jQuery中next的用法,可取得一个包含匹配的元素集合中每一个元素紧邻的后面同辈元素的元素集合. 这个函数只返回后面那个紧邻的同辈元素,而不是后面所有的同辈元素. 可以用一个可选的表达式进行筛选 ? 1 2 3