jQuery源码分析(十五): DOM操作核心函数domMainip

前端基础 2016-11-29

起步

.domManip()是jQuery DOM操作的核心函数。dom即DOM元素,Mainip是Manipulate的缩写,连在一起就是Dom操作的意思。

$('.inner').after('<p>Test</p>');
$('.container').after($('h2'));
$('p').after(function() {
    return '<div>' + this.className + '</div>';
});

诸如append、prepend、before、after、replaceWith、appendTo、prependTo、insertBefore、insertAfter、replaceAll 都是一些插入的操作但这些都有用到.domManip():

20161129105221.png

dom原生方法

jq将节点操作最后都转化为对dom对象的操作,原生方法中关于dom插入与删除就有这么几个:

element.appendChild()   //向元素添加新的子节点,作为最后一个子节点。
element.cloneNode()     //克隆元素,当前节点以及它的所有子孙节点。
element.hasChildNodes() //如果元素拥有子节点,则返回 true,否则 false。
element.insertBefore()  //在指定的已有的子节点之前插入新节点。
element.removeChild()   //从元素中移除子节点。
element.replaceChild()  //替换元素中的子节点。被替换的节点从文档树种删除。

以上接口都有一个特性,传入的是一个节点元素。如果我们传递不是一个dom节点元素,不是灵活多变的。

所以针对所有接口的操作,jQuery会抽象出一种参数的处理方案,就是domManip了,它的参数不再是简单的dom对象,而可以是jq对象,字符串,函数等。

domManip

在这个函数中,它需要解析参数,字符串,函数还是对象。针对一些文档的碎片处理,参数中包含script的处理。 已append源码为例:

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

调用this.domManip()时传入了两个参数,一个是arguments,这是函数参数对象,是一个类数组对象。第二个参数是一个回调函数,可以看到target.appendChild( elem );经过处理,ele应该是解析出来的dom对象,而target则是该节点的父节点。

函数原型:

domManip: function( args, callback ) {
    //变量初始化
    //构件出文档碎片
    //插入页面
    return this;
}

变量初始化:

args = concat.apply( [], args );

var fragment, first, scripts, hasScripts, node, doc,
    i = 0,
    l = this.length,
    set = this,
    iNoClone = l - 1,
    value = args[ 0 ],
    isFunction = jQuery.isFunction( value );

iNoClone = l - 1, 是否为克隆节点。value取的是args的第一个元素,后面也只对value进行检测。

// We can't cloneNode fragments that contain checked, in WebKit
if ( isFunction ||
        ( l > 1 && typeof value === "string" &&
            !support.checkClone && rchecked.test( value ) ) ) {
    return this.each(function( index ) {
        var self = set.eq( index );
        if ( isFunction ) {
            args[ 0 ] = value.call( this, index, self.html() );
        }
        self.domManip( args, callback );
    });
}

这段是对参数是回调函数的处理。处理过程就是将回调函数的返回值赋值给args[ 0 ],重新调用domManip()函数。

if ( l ) {
    fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );
    first = fragment.firstChild;

    if ( fragment.childNodes.length === 1 ) {
        fragment = first;
    }

    if ( first ) {
        scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
        hasScripts = scripts.length;

        for ( ; i < l; i++ ) {
            node = fragment;

            if ( i !== iNoClone ) {
                node = jQuery.clone( node, true, true );

                if ( hasScripts ) {
                    jQuery.merge( scripts, getAll( node, "script" ) );
                }
            }

            callback.call( this[ i ], node, i );
        }

        if ( hasScripts ) {
            //code...
        }
    }
}

l = this.length这段代码处理当jq对象里面含有dom集合的请况的。

fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );

parentEles[0].ownerDocument就是Document文档对象documentbuildFragment()函数下文讲,它返回的是文档片段对象。fragment.firstChild 获取创建成功的DOM、也就是我们需要添加的对象。if (first){callback.call(parentEles, first);} 如果创建成功,写入文档。

碎片文档

createDocumentFragmentcreateElement功能是一样的。但是通过创建文档碎片的方式可以减少多余的渲染过程。具体原因是这样的,每次对dom 操作都会触发"重排",这严重影响到能耗。而碎片文档节点不属于文档树,继承的 parentNode 属性总是 null。当需要大量 appendChild 页面元素时,就可以先将这些元素 appendChild 进碎片文档再将碎片文档插到document中。

innerHTML也可能达到类似createElement的效果,而且性能上更加,还能一次性生成一堆的节点。但是它存在兼容性问题,诸如ie会对它进行trimLeft处理,ie8有些元素是只读,不支持script脚本。

jQuery.buildFragment函数声明:

buildFragment: function( elems, context, scripts, selection ) {
    var fragment = context.createDocumentFragment();
    //code...
    return fragment;//碎片文档对象
};

DocumentFragment节点具有下列特征:

  1. nodeType的值为11
  2. nodeName的值为“#document-fragment”
  3. nodeValue的值为 null
  4. parentNode的值为 null
  5. 子节点可以是 Element、ProcessingInstruction、Comment、Text、CDATASection 或 EntityReference

文档片段继承了Node的所有方法。

for ( ; i < l; i++ ) {
    elem = elems[ i ];
    if ( elem || elem === 0 ) {
        if ( jQuery.type( elem ) === "object" ) {
            //code...
        } else if ( !rhtml.test( elem ) ) {
            //code...
        } else {
            //code...
        }
    }
}

分解类型,jQuery对象,节点对象,文本,字符串,脚本。nodes=[] 则是用来收集各种分解的类型数据。

tmp = tmp || fragment.appendChild( context.createElement("div") );

// Deserialize a standard representation
tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
wrap = wrapMap[ tag ] || wrapMap._default;
tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[ 2 ];

// Descend through wrappers to the right content
j = wrap[ 0 ];
while ( j-- ) {
    tmp = tmp.lastChild;
}

// Support: QtWebKit, PhantomJS
// push.apply(_, arraylike) throws on ancient WebKit
jQuery.merge( nodes, tmp.childNodes );

// Remember the top-level container
tmp = fragment.firstChild;

// Ensure the created nodes are orphaned (#12392)
tmp.textContent = "";

这段代码是解析html字符串,创建一个 div 节点,通过innerHTML写到节点里面去,取出div元素的子元素收集进nodes。这里的innerHTML需要做一些兼容处理,过滤空白,补全tr,td等。j = wrap[ 0 ];表示正确的元素父级。

var
    rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
    rtagName = /<([\w:]+)/,
    rhtml = /<|&#?\w+;/,
    rnoInnerhtml = /<(?:script|style|link)/i,
    // checked="checked" or checked
    rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
    rscriptType = /^$|\/(?:java|ecma)script/i,
    rscriptTypeMasked = /^true\/(.*)/,
    rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,

    // We have to close these tags to support XHTML (#13200)
    wrapMap = {

        // Support: IE9
        option: [ 1, "<select multiple='multiple'>", "</select>" ],

        thead: [ 1, "<table>", "</table>" ],
        col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
        tr: [ 2, "<table><tbody>", "</tbody></table>" ],
        td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],

        _default: [ 0, "", "" ]
    };

整理后这个函数大致为:

function buildFragment(elems, context) {
    var elem, tmp, tag, wrap, contains, j,
        fragment = context.createDocumentFragment(),
        nodes = [],
        i = 0,
        l = elems.length;

    //筛选出不同类型的节点
    for (; i < l; i++) {
        elem = elems[i];

        if (elem || elem === 0) {
            if (jQuery.type(elem) === "object") {
                // 如果是jQuery对象
                // 如果是普通元素对象加[elem]
                // 取出ele放入nodes数组中
                jQuery.merge(nodes, elem.nodeType ? [elem] : elem);
                // 没有html结构,是一个文本节点
            } else if (!/<|&#?\w+;/.test(elem)) {
                nodes.push(context.createTextNode(elem));
            } else {
                //创一个元素div做为容器
                tmp = tmp || fragment.appendChild(context.createElement("div"));
                tag = (/<([\w:]+)/.exec(elem) || ["", ""])[1].toLowerCase();
                //ie对字符串进行trimLeft操作,其余是用户输入处理
                //很多标签不能单独作为DIV的子元素
                //td,th,tr,tfoot,tbody等等,需要加头尾
                wrap = wrapMap[tag] || wrapMap._default;
                tmp.innerHTML = wrap[1] + elem.replace(rxhtmlTag, "<$1></$2>") + wrap[2];

                // Descend through wrappers to the right content
                // 因为warp被包装过
                // 需要找到正确的元素父级
                j = wrap[0];
                while (j--) {
                    tmp = tmp.lastChild;
                }
                // Support: QtWebKit
                // jQuery.merge because push.apply(_, arraylike) throws
                // 把节点拷贝到nodes数组中去
                jQuery.merge(nodes, tmp.childNodes);
                // Remember the top-level container
                tmp = fragment.firstChild;
                // Ensure the created nodes are orphaned (#12392)
                tmp.textContent = "";
            }
        }
    }
    i = 0;
    while ((elem = nodes[i++])) {
        fragment.appendChild(elem)
    }
    return fragment;
}

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

如果对您有用,您的支持将鼓励我继续创作!