jQuery 源码系列(十五)DOM 操纵

欢迎来我的专栏检察系列文章。

能够你会以为这个名字很新鲜这个名字很新鲜,为何叫做 domManip,即所谓的 dom 微操纵。

《jQuery 源码系列(十五)DOM 操纵》

实在在 jQuery 中有许多重要的 dom 操纵,这些操纵运用的频次都异常高,不过这些操纵广泛有一个特性,就是须要举行微调,比方将字符串转换成 elem 元素,推断是不是为 script 剧本。

所以 jQuery 内部一个一致的做法,就是采纳 callbacks 的体式格局,先对要举行 dom 操纵的内部函数实行 domManip 操纵,然后回调实行任务。

jQuery 内的一些 DOM 操纵函数

jQuery 内有几个要领挪用了 domManip 函数,他们离别以下:

jQuery.fn.extend( {
  // 在末了一个子元素后增加
  append: function() {
    return domManip( this, arguments, function( elem ) {
      if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
        var target = manipulationTarget( this, elem );
        target.appendChild( elem ); // 原生要领
      }
    } );
  },
  // 在第一个子元素前增加
  prepend: function() {
    return domManip( this, arguments, function( elem ) {
      if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
        var target = manipulationTarget( this, elem );
        target.insertBefore( elem, target.firstChild ); //  原生要领
      }
    } );
  },
  // 在当前节点前增加
  before: function() {
    return domManip( this, arguments, function( elem ) {
      if ( this.parentNode ) {
        this.parentNode.insertBefore( elem, this ); // 原生要领
      }
    } );
  },
  // 在当前节点后增加
  after: function() {
    return domManip( this, arguments, function( elem ) {
      if ( this.parentNode ) {
        this.parentNode.insertBefore( elem, this.nextSibling ); // 原生要领
      }
    } );
  },

  replaceWith: function() {
    var ignored = [];
    return domManip( this, arguments, function( elem ) {
      var parent = this.parentNode;

      if ( jQuery.inArray( this, ignored ) < 0 ) {
        jQuery.cleanData( getAll( this ) );
        if ( parent ) {
          parent.replaceChild( elem, this );
        }
      }
    }, ignored );
  }
} );

仔细观察一下,这几个函数都有一个特性,就是有关于 domManip 的参数 domManip(this, arguments, callback),然后在 callback 函数内里经由过程原生 js 来完成:

// 一个简朴的
jQuery.fn.extend( {
  append: function(elem){
    this[0].appendChild(elem);
    return this;
  },
  prepend: function(elem){
    this[0].insertBefore(elem, this[0].firstChild);
    return this;
  },
  before: function(elem){
    if(this[0].parentNode){
      this[0].parentNode.insertBefore(elem, this[0]);
    }
    return this;
  },
  after: function(elem){
    if(this[0].parentNode){
      this[0].parentNode.insertBefore(elem, this[0],nextSibling);
    }
    return this;
  }
} );

我之前就跟同砚讨论过一个题目,就是如何用原生的要领将字符串转换成 dom 对象,在 jQuery 内里直接jQuery.parseHTML(),原生的话,可以用下面的:

function parseHtml(str){
  var div = document.createElement('div');
  if(typeof str == 'string'){
    div.innerHTML = str;
  }
  return div.children[0];
}

虽然非常抠脚,但也是一种要领。

buildFragment 要领

实在在 jQuery 内部,或者说 jQuery.parseHTML 要领以内,运用的是别的一个要领来竖立 str 到 elem 的转换,那就是 buildFragment 要领。这个要领用于竖立文档碎片,你不要纠结这个要领在 jQuery 中涌现频频,我明白的通知你,它只在两个处所涌现,离别是 domManip 函数里和 parseHTML 函数里。

在之前,有必要先相识一下 createDocumentFragment,文中有几句话说的很好:DocumentFragments are DOM Nodes. They are never part of the main DOM tree. The usual use case is to create the document fragment, append elements to the document fragment and then append the document fragment to the DOM tree. 。它虽然也一样占内存,却比 createElement 要领好多了。

所以,当今后再遇到 create 无需衬着的 dom 的时刻,要运用 document.createDocumentFragment 替换 document.createElement

function buildFragment( elems, context, scripts, selection, ignored ) {
  var elem, tmp, tag, wrap, contains, j,
    // context 平常为 document
    fragment = context.createDocumentFragment(),
    nodes = [],
    i = 0,
    l = elems.length;

  for ( ; i < l; i++ ) {
    elem = elems[ i ];

    if ( elem || elem === 0 ) {

      // Add nodes directly
      if ( jQuery.type( elem ) === "object" ) {

        // Support: Android <=4.0 only, PhantomJS 1 only
        // push.apply(_, arraylike) throws on ancient WebKit
        jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );

      // 非 HTML 标签
      } else if ( !rhtml.test( elem ) ) {
        nodes.push( context.createTextNode( elem ) );

      // 将 str 转换成 html dom
      } else {
        tmp = tmp || fragment.appendChild( context.createElement( "div" ) );

        // 取得 标签 范例,处置惩罚特殊情况
        tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
        wrap = wrapMap[ tag ] || wrapMap._default;
        tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];

        // 平常从 0 最先
        j = wrap[ 0 ];
        while ( j-- ) {
          tmp = tmp.lastChild;
        }

        // 在这里合并到 nodes 内里
        jQuery.merge( nodes, tmp.childNodes );

        // 返回 div
        tmp = fragment.firstChild;

        // 清空
        tmp.textContent = "";
      }
    }
  }

  // 清空 fragment
  fragment.textContent = "";

  i = 0;
  while ( ( elem = nodes[ i++ ] ) ) {

    // 跳过已存在的 context
    if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
      if ( ignored ) {
        ignored.push( elem );
      }
      continue;
    }

    contains = jQuery.contains( elem.ownerDocument, elem );

    // 增加到 fragment 内部,根据递次,并取得 scripts
    tmp = getAll( fragment.appendChild( elem ), "script" );

    // Preserve script evaluation history
    if ( contains ) {
      setGlobalEval( tmp );
    }

    // Capture executables
    if ( scripts ) {
      j = 0;
      while ( ( elem = tmp[ j++ ] ) ) {
        if ( rscriptType.test( elem.type || "" ) ) {
          scripts.push( elem );
        }
      }
    }
  }

  return fragment;
}

末了的返回效果是 fragment,但它并非我们想要的 dom,而真正的效果应该是:fragment.childNodes,一个 dom 伪数组。

domManip 要领

实在本文的重点应该是 domManip 要领,不急,如今最先来说。

前面已引见了五个基础的 domManip 用法,下面是几个扩大,也就是反过来用,也算是间接运用 domManip 吧:

jQuery.each( {
  appendTo: "append",
  prependTo: "prepend",
  insertBefore: "before",
  insertAfter: "after",
  replaceAll: "replaceWith"
}, function( name, original ) {
  jQuery.fn[ name ] = function( selector ) {
    var elems,
      ret = [],
      // 新建一个 jQuery 对象
      insert = jQuery( selector ),
      last = insert.length - 1,
      i = 0;

    for ( ; i <= last; i++ ) {
      elems = i === last ? this : this.clone( true );
      jQuery( insert[ i ] )[ original ]( elems );

      // 将 elems 存入 ret
      push.apply( ret, elems.get() );
    }
    // 返回一个新的 jQuery 对象
    return this.pushStack( ret );
  };
} );

这又是五个要领,不过是和之前那五个要领恰好先反的逻辑,有用。

来看看 domManip 函数:

function domManip( collection, args, callback, ignored ) {

  // var concat = [].concat; 用于将伪 args 转换成真是的数组
  args = concat.apply( [], args );

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

  // 处置惩罚 WebKit 中出 checked
  if ( isFunction ||
      ( l > 1 && typeof value === "string" &&
        !support.checkClone && rchecked.test( value ) ) ) {
    return collection.each( function( index ) {
      var self = collection.eq( index );
      if ( isFunction ) {
        args[ 0 ] = value.call( this, index, self.html() );
      }
      domManip( self, args, callback, ignored );
    } );
  }

  if ( l ) {
    // 挪用 buildFragment
    fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored );
    // 第一个 elem
    first = fragment.firstChild;

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

    // Require either new content or an interest in ignored elements to invoke the callback
    if ( first || ignored ) {
      scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
      hasScripts = scripts.length;

      // Use the original fragment for the last item
      // instead of the first because it can end up
      // being emptied incorrectly in certain situations (#8070).
      for ( ; i < l; i++ ) {
        node = fragment;

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

          // 克隆 scripts
          if ( hasScripts ) {

            // Support: Android <=4.0 only, PhantomJS 1 only
            // push.apply(_, arraylike) throws on ancient WebKit
            jQuery.merge( scripts, getAll( node, "script" ) );
          }
        }
        // 回调,this 指向当前回调的 elem,这点很重要
        // 很重要
        callback.call( collection[ i ], node, i );
      }
      // 这个 scripts 到底有什么用,不懂
      if ( hasScripts ) {
        doc = scripts[ scripts.length - 1 ].ownerDocument;

        // Reenable scripts
        jQuery.map( scripts, restoreScript );

        // Evaluate executable scripts on first document insertion
        for ( i = 0; i < hasScripts; i++ ) {
          node = scripts[ i ];
          if ( rscriptType.test( node.type || "" ) &&
            !dataPriv.access( node, "globalEval" ) &&
            jQuery.contains( doc, node ) ) {

            if ( node.src ) {

              // Optional AJAX dependency, but won't run scripts if not present
              if ( jQuery._evalUrl ) {
                jQuery._evalUrl( node.src );
              }
            } else {
              DOMEval( node.textContent.replace( rcleanScript, "" ), doc );
            }
          }
        }
      }
    }
  }
  return collection;
}

在我看来,domManip 重要的几个功用包含:接收 HTML 字符串,并天生相对于的 dom,callback 回调函数,处置惩罚 dom,而且回调函数中的 this 是指向当前操纵的 dom 的。剩下的事变,就交给回调函数去处置惩罚。

所以,domManip 的作用远比设想的要少。

参考

解密jQuery内核 DOM操纵的中心函数domManip
解密jQuery内核 DOM操纵的中心buildFragment

本文在 github 上的源码地点,欢迎来 star。

欢迎来我的博客交换。

    原文作者:songjz
    原文地址: https://segmentfault.com/a/1190000008619756
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞