jQuery 源码分析(二十) DOM操作模块 插入元素 详解

jQuery的DOM操作模块封装了DOM模型的insertBefore()、appendChild()、removeChild()、cloneNode()、replaceChild()等原生方法。分为5个子模块来实现:插入元素、删除元素、复制元素、替换元素和包裹元素,本节讲解第一个子模块:插入元素

插入元素模块可用于新增DOM节点,修改文本节点等,API如下:

  • append(content)       ;在被选元素子节点的末尾插入指定内容,内部调用appendChild(elem)方法    ;content可以是HTML代码、函数(返回html代码)、DOM元素 或 jQuery对象,下同
  • prepend(content)        ;在被选元素子节点的头部插入指定内容
  • before(content)           ;在匹配元素集合的每个元素之前插入content。
  • after(content)               ;在匹配元素集合的每个元素之后插入内容
  • appendTo(target)        ;将匹配元素中的每个元素插入目标元素末尾        ;内部调用append()方法    ;这四个方法执行后都会调用pushStack()方法,匹配的是全部插入的DOM对象引用。
  • prependTo(target)       ;将匹配元素中的每个元素插入目标元素开头        ;内部调用prepeng()方法
  • insertBefore(target)     ;将匹配元素中的每个元素插入目标元素之前        ;内部调用before()方法
  • insertAfter(target)        ;将匹配元素中的每个元素插入目标元素之后        ;内部调用after()方法

上面的前四个后面四个是一一对应的,例如:

 writer by:大沙漠 QQ:22969969

$('#d').append('<p>1</p>')       
$('<p>1</p>').appendTo($('#d'));      //这两条代码的作用是相同的

其它几个API也是同样的作用:prepend和prependTo、before和insertBefore、after和insertAfter都是一样的。

举个栗子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="http://libs.baidu.com/jquery/1.7.1/jquery.min.js"></script>
</head>
<body>
    <div id="d">
        <p id="p">Hello,World!</p>
    </div>
    <h1>insertBefore测试</h1>
    <h2>insertAfter测试</h2>
    <button id="b1">append测试</button>
    <button id="b2">prepend测试</button>
    <button id="b3">before测试</button>
    <button id="b4">insertAfter测试</button>
    <script>    
        $('#b1').click(()=>{                
            $('#d').append('<p>append测试</p>')        //在div元素的子节点末尾添加一个<p>1</p>节点,这里的参数是一个html代码
        })
        $('#b2').click(()=>{
            $('#d').prepend('<p>prepend测试</p>')      //在div元素的子节点头部添加一个<p>2</p>节点,这里的参数是一个html代码
        })
        $('#b3').click(()=>{
            $('h1').insertBefore('#d')                 //在div元素元素之前添加$('h1')元素,这里的参数是一个jQuery对象
        })  
        $('#b4').click(()=>{
            $('h2').insertAfter($('#d'))                //在div元素元素之后添加$('h2')元素,这里的参数也是一个jQuery对象
        })
    </script>
</body>
</html>

 渲染如下:

对应的DOM树如下:

 

  我们定义了四个按钮,分别在div的子节点之前、子节点默认,div之前位置和div之后位置插入一个DOM元素,每个按钮点击一次,执行后页面如下:

 

对应的DOM树如下:

 

源码分析


append、prepend、before和after的实现如下:

jQuery.fn.extend({
    append: function() {        //在被选元素子节点的末尾插入指定内容,内部调用appendChild(elem)方法
        return this.domManip(arguments, true, function( elem ) {
            if ( this.nodeType === 1 ) {
                this.appendChild( elem );
            }
        });
    },

    prepend: function() {        //在被选元素子节点的头部插入指定内容,内部调用insertBefore( elem, this.firstChild )方法
        return this.domManip(arguments, true, function( elem ) {
            if ( this.nodeType === 1 ) {
                this.insertBefore( elem, this.firstChild );
            }
        });
    },

    before: function() {        //在匹配元素集合的每个元素之前插入内容
        if ( this[0] && this[0].parentNode ) {            //如果该元素有父元素
            return this.domManip(arguments, false, function( elem ) {
                this.parentNode.insertBefore( elem, this );
            });
        } else if ( arguments.length ) {
            var set = jQuery.clean( arguments );
            set.push.apply( set, this.toArray() );
            return this.pushStack( set, "before", arguments );
        }
    },
    after: function() {            //在匹配元素集合的每个元素之后插入内容
        if ( this[0] && this[0].parentNode ) {             //如果该元素有父元素
            return this.domManip(arguments, false, function( elem ) {
                this.parentNode.insertBefore( elem, this.nextSibling );
            });
        } else if ( arguments.length ) {
            var set = this.pushStack( this, "after", arguments );
            set.push.apply( set, jQuery.clean(arguments) );
            return set;
        }
    },
    /**/
})

可以看到内部都调用了domManip这个函数,执行时第一个传入arguments,也就是我们调用append、prepend等传入的参数,domManip会把参数转化为一个DOM对象,然后调用参数3,参数3负责执行最后的DOM操作,domManip实现如下:

jQuery.fn.extend({
    domManip: function( args, table, callback ) {            //jQuery插入元素实现的核心,负责转换html代码为DOM元素,然后调用传入的回调函数插入DOM元素
        var results, first, fragment, parent,
            value = args[0],                                    //例如$().append()的第一个参数,比如:,<p>123</p>
            scripts = [];                                        //存储args[0]里的script脚本

        // We can't cloneNode fragments that contain checked, in WebKit
        if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
            return this.each(function() {
                jQuery(this).domManip( args, table, callback, true );
            });
        }

        if ( jQuery.isFunction(value) ) {                        //如果args[0]是函数,则遍历匹配元素集合,执行args[0]函数,并把返回值作为参数,再次遍历执行domManip()函数
            return this.each(function(i) {
                var self = jQuery(this);    
                args[0] = value.call(this, i, table ? self.html() : undefined);        //在每个元素上执行该函数,返回一个DOM元素
                self.domManip( args, table, callback );                                    //迭代调用.domMapnip()方法
            });
        }

        if ( this[0] ) {                                        //如果至少有一个匹配元素
            parent = value && value.parentNode;

            // If we're in a fragment, just use that instead of building a new one
            if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
                results = { fragment: parent };                            //在1.7.1中jQuery.support.parentNode方法未定义,所以这段代码不会执行,在1.8中已经移除这段代码。

            } else {
                results = jQuery.buildFragment( args, this, scripts );    //调用buildFragment()把html代码转换为DOM元素。返回一个对象。对象里的fragment属性包含了转换后的DOM元素,cacheable表示缓存状态,指示了该HTML元素是否可以缓存
            }

            fragment = results.fragment;                                //fragment是转换后的DOM元素

            if ( fragment.childNodes.length === 1 ) {                    //如果文档片段只有一个子元素
                first = fragment = fragment.firstChild;                        //重置变量fragment为子元素,因为插入单个子元素比插入含有单个子元素的文档片段会稍快些。
            } else {    
                first = fragment.firstChild;                            //否则把first作为第一个子节点。
            }

            if ( first ) {                                                //如果子节点存在
                table = table && jQuery.nodeName( first, "tr" );            //如果table参数为true,且first是tr节点,那么设置table为true,否则为false。

                for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {    //遍历当前匹配的元素
                    callback.call(                                             //执行callback函数,上下文是每个匹配元素,即callback函数内的this指针。参数是转换后的DOM元素,即传给callback的参数,也就是append、prepend等试下内部执行doMainip()传递的参数3这个函数
                        table ?
                            root(this[i], first) :                             //修正目标对象,如果待插入元素是tr元素,则调用函数root()检查当前元素(容器元素)是否是table元素,如果是则返回tr元素的父元素tbody作为目标元素。
                            this[i],                                        //否则把匹配元素作为call的第一个参数,即this指针。
                        // Make sure that we do not leak memory by inadvertently discarding
                        // the original fragment (which might have attached data) instead of
                        // using it; in addition, use the original fragment object for the last
                        // item instead of first because it can end up being emptied incorrectly
                        // in certain situations (Bug #8070).
                        // Fragments from the fragment cache must always be cloned and never used
                        // in place.
                        results.cacheable || ( l > 1 && i < lastIndex ) ?     //修正待插入元素,如果返回的文档片段是可缓存的或者不能缓存但是当前匹配元素大于1个且当前操作不是操作最后一个匹配元素时,总是插入该文档片段的副本。
                            jQuery.clone( fragment, true, true ) :                 //则总是插入该文档片段的副本,调用jQuery.clone深度复制事件和数据
                            fragment                                             //否则插入文档本身
                    ); 
                }
            }

            if ( scripts.length ) {                                         //执行转换后的DOM目标元素的script元素
                jQuery.each( scripts, evalScript );                             //jQuery.buildFragment()和jQuery.clean()利用浏览器的innerHTML机制把HTML代码转换为DOM元素,但是转换后的script元素并不会自动加载和执行,需要手动处理。
            }
        }

        return this;                                             //返回匹配元素集合,以支持链式操作。
    }
    /**/
})

buildFragment是用于将html代码转换为对应的DOM节点的,它首先尝试从缓存中获取对应的DOM对象,如果没有缓存则通过document.createDocumentFragment创建一个文档碎片,然后则再调用$.clean将html代码转换为一个DOM对象(通过设置innerHTML来实现的),代码比较多,不再贴了。

对于appendTo、prependTo、insertBefore和insertAfter来说,它们在会在内部定义一个临时的jQuery对象,然后再调用对应的append、prepend、before和after来实现的,如下:

jQuery.each({
    appendTo: "append",
    prependTo: "prepend",
    insertBefore: "before",
    insertAfter: "after",
    replaceAll: "replaceWith"
}, function( name, original ) {
    jQuery.fn[ name ] = function( selector ) {        //给jQuery添加appendTo、prependTo、insertBefore、insertAfter、replaceAll方法    name是要添加的方法名,如appendTo,original标识对应的已有方法名,如append
        var ret = [],         
            insert = jQuery( selector ),                            //构造一个jQuery对象,指向目标元素集合,这就是内部的临时的jQuery对象
            parent = this.length === 1 && this[0].parentNode;        //如果操作的html只有一个顶级节点,则把parent设置为父元素引用,否则设为false     ;比如:$('<p>222</p>').appendTo('#dd');

        if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {    //如果待插入元素只有一个,并且是在文档片段中,同时目标元素也只有一个,
            insert[ original ]( this[0] );                                                                        //则直接将待插入元素插入目标元素,不需要遍历目标元素集合。这样速度稍快一点。
            return this;

        } else {
            for ( var i = 0, l = insert.length; i < l; i++ ) {                //遍历目标元素集合,循环体内insert[i]是每一个目标元素
                var elems = ( i > 0 ? this.clone(true) : this ).get();        //elems是要插入的元素集合,第一次插入的是待插入元素集合,之后插入的则是它的副本。调用对应的已经有方法名插入elems元素
                jQuery( insert[i] )[ original ]( elems );                        //调用目标元素的original方法,参数是elems        
                ret = ret.concat( elems );
            }

            return this.pushStack( ret, name, insert.selector );            //用包含了待插入元素集合和它的副本的数组ret构造一个新jQuery对象并返回。
        }
    };
});

通过代码可以看到appendTo、prependTo、insertBefore和insertAfter只是对append、prepend、before和after的一个封装而已。

posted @ 2019-11-06 10:28  大沙漠  阅读(543)  评论(0编辑  收藏  举报