jQuery源码分析(十六): DOM的节点操作

前端语法/样式/布局 2016-11-30

起步

jq关于dom对象操作可以大致分为取值,插入,替换,移除,克隆。

取值

关于取值,原生dom对象有innerHTML outerHTML innerText outerText textContent

  • innerHTML 设置或获取位于对象起始和结束标签内的 HTML,包括标签。
  • outerHTML 设置或获取对象及其内容的 HTML 形式,包括dom对象本身的标签。
  • innerText 仅设置或获取标签内的文本。
  • outerText 仅设置或获取包括dom对象本身标签内的文本。
  • textContent 属性设置或返回指定节点的文本内容,以及它的所有后代。设置了 textContent 属性,会删除所有子节点,并被替换为包含指定字符串的一个单独的文本节点。

关于兼容,innerHTML textContent是符合W3C标准的属性,浏览器基本都是支持的,而innerText、outerText、outerHTML只适用于IE浏览器和chrome,因此,尽可能地去使用innerHTML,而少用innerText。

innerHTML与outerHTML在设置对象的内容时包含的HTML会被解析,而innerText与outerText则不会,他们会被转义。

在jq的api中,有这几个api:

.html()用为读取和修改元素的HTML标签
.text()用来读取或修改元素的纯文本内容
.val()用来读取或修改表单元素的value

不传参时是获取,传参时是设置。val()其实就是读取表单的value属性而已。

.html()

//有省略部分代码
html: function( value ) {
    return access( this, function( value ) {
        var elem = this[ 0 ] || {},
            i = 0,
            l = this.length;
        //如果没有传参,直接返回innerHTML
        if ( value === undefined && elem.nodeType === 1 ) {
            return elem.innerHTML;
        }

        if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
            !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
            //修复html字符串闭合标签
            value = value.replace( rxhtmlTag, "<$1></$2>" );

            for ( ; i < l; i++ ) {
                elem = this[ i ] || {};
                jQuery.cleanData( getAll( elem, false ) );
                elem.innerHTML = value;//赋值
            }

            elem = 0;

        }

    }, null, value, arguments.length );
},

可以看出,html()函数是对innerHTML操作的。此外,用新的内容替换这些元素前,jQuery从子元素删除其他结构,如清理数据和事件处理程序,防止内存溢出,对插入的值做一下过滤处理jQuery.cleanData()

text()

text: function( value ) {
    return access( this, function( value ) {
        return value === undefined ?
            jQuery.text( this ) :
            this.empty().each(function() {
                if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                    this.textContent = value;
                }
            });
    }, null, value, arguments.length );
},

.text() 方法返回一个字符串,包含所有匹配元素的合并文本。(由于在不同的浏览器中的HTML解析器的变化,返回的文本中换行和其他空白可能会有所不同。jQuery.text( this ) 实际调用Sizzle.getText。jq最后是通过操作 textContent 来处理的,textContent本身是dom3规范的,可以兼容火狐下的innerText问题。一些特殊字符会被转义,与HTML对应(比如< 替换为 &lt; )。

access() 是一个属性操作上用到的函数,.attr() .prop() .removeAttr() .val()等就有用到它,后面的关于属性操作再来看它,这边就简单理解为合并分解多个参数,细分到每一个流程调用中,通过回调接收分解后的参数。

插入

jQuery针对DOM操作的插入的方法有大概10种:

append、prepend、before、after、replaceWith
appendTo、prependTo、insertBefore、insertAfter、replaceAll

他们都有用到domManip(),这个函数上一篇分析过了。

内部插入

上一篇以append()为例,这边就已prepend():

prepend: function() {
    return this.domManip( arguments, function( elem ) {
        if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
            var target = manipulationTarget( this, elem );
            target.insertBefore( elem, target.firstChild );
        }
    });
},

$("#id").prepend( '<!--插入到p元素内部的起始位置-->' ); 与append对应,这个函数是元素内部的起始位置追加内容。 target.firstChild 用得很精髓。

prependTo() 函数用于将当前所有匹配元素追加到指定元素内部的起始位置。实现上也是调用prepend,大致其实现为(非jq源码,为理解而整理):

prependTo: function(selector) {
    var elems,
        ret = [],
        insert = $(selector);
    for (i = 0; i <= insert.length -1; i++) {
        elems = i === insert.length -1 ? this : this.clone( true );
        insert[i].prepend(elems);
        ret.push(elems.get());
    }
    return this.pushStack(ret);
}

ret是收集jq对象的容器,先通过$(selector)要插入的dom集合对应的jq对象,再通过insert[i].prepend(elems) 插入到改元素内部的起始位置

外部插入

.after() 函数用于在每个匹配元素之后插入指定的内容。

after: function() {
    return this.domManip( arguments, function( elem ) {
        if ( this.parentNode ) {
            this.parentNode.insertBefore( elem, this.nextSibling );
        }
    });
},

insertBefore(newchild,refchild)是原生dom对象的方法,在指定的已有子节点之前插入新的子节点。可以看出,外部插入是要求节点存在父节点的。this.nextSibling 是获取下一个兄弟节点,插入到下一个兄弟节点的前面来达到after的效果。可以看到,.before() 函数里仅仅的区别就是 this.nextSibling 换成了 this

替换

.replaceWith()

.replaceWith() 用于使用指定的元素替换每个匹配的元素。考虑到dom方法中也有替换的element.replaceChild(new_node, old_node) 可以替换元素中的子节点。

replaceWith: function() {
    var arg = arguments[ 0 ];

    // Make the changes, replacing each context element with the new content
    this.domManip( arguments, function( elem ) {
        arg = this.parentNode;

        jQuery.cleanData( getAll( this ) );

        if ( arg ) {
            arg.replaceChild( elem, this );
        }
    });

    // Force removal if there was no new content (e.g., from empty arguments)
    return arg && (arg.length || arg.nodeType) ? this : this.remove();
},

.replaceWith() 和大多数方法一样,返回的是jq对象。因此这个方法是可以链式调用的。arg.replaceChild( elem, this ) 这里的this是指jq集合中的dom对象,另外替换之前还有清空数据,this.remove() 删除目标节点。

.replaceAll().replaceWith()唯一区别就是替换的对象不同。replaceAll返回的是替换内容的jQuery对象。

$A.replaceWith( $B ); // 将$A替换成$B $B从原位置上消失
$A.replaceAll( $B );  // 将$B替换成$A $A从原位置上消失

移除

涉及节点移除的jq接口有:detach,empty,remove,unwrap

  • .empty()函数用于清空每个匹配元素内的所有内容。将会移除每个匹配元素的所有子节点(包括文本节点、注释节点等所有类型的节点)。
  • .remove()函数用于从文档中移除匹配的元素。同时移除与元素关联绑定的附加数据( data()函数 )和事件处理器等
  • .detach()函数用于从文档中移除匹配的元素。不会移除与元素关联绑定的附加数据( data()函数 )和事件处理器等
  • .unwrap()函数用于移除每个匹配元素的父元素。会保留其所有的后辈元素。

而元素dom只提供了 ele.removeChild(dom) 方法。

.empty()

empty: function() {
    var elem,
        i = 0;

    for ( ; (elem = this[i]) != null; i++ ) {
        if ( elem.nodeType === 1 ) {

            // Prevent memory leaks
            jQuery.cleanData( getAll( elem, false ) );

            // Remove any remaining nodes
            elem.textContent = "";
        }
    }

    return this;
},

为了避免内存泄漏,jQuery先移除子元素的数据和事件处理函数,然后移除子元素。jQuery是合集元素,所以我们遍历下this[i],然后直接把元素的textContent清空即可。jQuery.cleanData() 不单单清理元素,还会清理附在上面的事件处理和数据缓存。

.remove()

remove: function( selector, keepData /* Internal Use Only */ ) {
    var elem,
        elems = selector ? jQuery.filter( selector, this ) : this,
        i = 0;

    for ( ; (elem = elems[i]) != null; i++ ) {
        if ( !keepData && elem.nodeType === 1 ) {
            jQuery.cleanData( getAll( elem ) );
        }

        if ( elem.parentNode ) {
            if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
                setGlobalEval( getAll( elem, "script" ) );
            }
            elem.parentNode.removeChild( elem );
        }
    }

    return this;
},

和empty()不同的是,这里cleanData()后elem.parentNode.removeChild( elem )将自身元素从文档树种移除。参数keepData 是一个是否要保留事件处理和数据缓存。

.detach()

detach: function( selector ) {
    return this.remove( selector, true );
},

想删除元素,不破坏他们的数据或事件处理程序。就可以将remove()的代码进行复用了。

克隆

.clone() 函数用于克隆当前匹配元素集合的一个副本,并以jQuery对象的形式返回。你也可以简单地理解为:克隆当前jQuery对象。

dom对象有cloneNode( deep ) 方法,克隆所有属性以及它们的值,需要克隆所有后代,请把 deep 参数设置 true。它不会复制javascript属性,比如事件。

.clone()

clone: function( dataAndEvents, deepDataAndEvents ) {
    dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
    deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;

    return this.map(function() {
        return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
    });
},

参数是是否克隆事件和是否连同子节点的事件也克隆,最后实际调用了jQuery.clone() 方法:

//省略部分代码
clone: function( elem, dataAndEvents, deepDataAndEvents ) {
    var i, l, srcElements, destElements,
        clone = elem.cloneNode( true ),
        inPage = jQuery.contains( elem.ownerDocument, elem );

    // Fix IE cloning issues
    if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
            !jQuery.isXMLDoc( elem ) ) {
        destElements = getAll( clone );
        srcElements = getAll( elem );
    }

    // Copy the events from the original to the clone
    // 是否克隆事件
    if ( dataAndEvents ) {
        if ( deepDataAndEvents ) {
            srcElements = srcElements || getAll( elem );
            destElements = destElements || getAll( clone );

            for ( i = 0, l = srcElements.length; i < l; i++ ) {
                cloneCopyEvent( srcElements[ i ], destElements[ i ] );
            }
        } else {
            cloneCopyEvent( elem, clone );
        }
    }

    // Return the cloned set
    // 返回克隆的dom对象
    return clone;
},

jQuery在DOM上做了一个uuid的标记,然后把与这个dom相关联的所有数据都放到一个内存区域,通过这个uuid映射,这样我们在深度拷贝 dom 的时候自然也可以把内存的数据给复制一份了,当然这里要注意一个问题,事件是不能被复制的,需要重新绑定了。所以克隆的大致流程为:

  • 通过elem.cloneNode(true)直接给这个元素克隆一份。
  • 如果需要克隆事件和数据,从在data_priv中获取事件,从data_user获取数据。
  • 2个缓存给找出来,事件和数据重新绑定。事件是通过jQuery.event.add(),如果节点是有嵌套的话,需要遍历每一个元素节点,在每个节点上都要处理事件与数据。

总结

所有关于dom操作最后都是归为对dom对象的操作。因为jq对象是个dom集合,所以需要遍历每个元素。插入和删除的代码复用上做的挺好的。克隆功能的代码比较多,涉及的范围比较广。


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

赏个馒头吧