Jquery-1.9.1源码分析系列(十一)之DOM操作


Posted in Javascript onNovember 25, 2015

DOM操作包括append、prepend、before、after、replaceWith、appendTo、prependTo、insertBefore、insertAfter、replaceAll。其核心处理函数是domManip。

DOM操作函数中后五种方法使用的依然是前面五种方法,源码

jQuery.each({
    appendTo: "append",
    prependTo: "prepend",
    insertBefore: "before",
    insertAfter: "after",
    replaceAll: "replaceWith"
  }, function( name, original ) {
    jQuery.fn[ name ] = function( selector ) {
      var elems,
      i = 0,
      ret = [],
      insert = jQuery( selector ),
      last = insert.length - 1;
      for ( ; i <= last; i++ ) {
        elems = i === last ? this : this.clone(true);
        jQuery( insert[i] )[ original ]( elems );
        //现代浏览器调用apply会把jQuery对象当如数组,但是老版本ie需要使用.get()
        core_push.apply( ret, elems.get() );
      }
      return this.pushStack( ret );
    };
  });

浏览器原生的插入节点的方法有两个:appendChild和inserBefore,jQuery利用这两个方法拓展了如下方法

jQuery.fn.append使用this.appendChild( elem )

jQuery.fn.prepend使用this.insertBefore( elem, this.firstChild )

jQuery.fn.before使用this.parentNode.insertBefore( elem, this );

jQuery.fn.after使用this.parentNode.insertBefore( elem, this.nextSibling );

jQuery.fn.replaceWith 使用this.parentNode.insertBefore( elem, this.nextSibling);

看一个例子的源码(jQuery.fn.append)

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

根据上面的源码。猜测domManip的作用是遍历当前jQuery对象所匹配的元素,然后每个元素调用传入的回调,并将要插入的节点(如果是字符串那么需要创建文档碎片节点)作为传入的回调的参数;并执行传入的回调。

接下来分析domManip,看猜测是否正确。dom即Dom元素,Manip是Manipulate的缩写,连在一起的字面意思就是就是Dom操作。

a. domManip: function( args, table, callback )解析

args 待插入的DOM元素或HTML代码

table 是否需要修正tbody,这个变量是优化的结果

callback 回调函数,执行格式为callback.call( 目标元素即上下文, 待插入文档碎片/单个DOM元素 )

先看流程,再看细节

第一步,变量初始化。其中iNoClone在后面会用到,如果当前的jQuery对象所匹配的元素不止一个(n > 1)的话,意味着构建出来的文档碎片需要被n用到,则需要被克隆(n-1)次,加上碎片文档本身才够n次使用;value 是第一个参数args的第一个元素,后面会对value是函数做特殊处理;

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

第二步,处理特殊下要将当前jQuery对象所匹配的元素一一调用domManip。这种特殊情况有两种:第一种,如果传入的节点是函数(即value是函数)则需要当前jQuery对象所匹配的每个元素都将函数计算出的值作为节点代入domManip中处理。第二种,webkit下,我们不能克隆文含有checked的文档碎片;克隆的文档不能重复使用,那么只能是当前jQuery对象所匹配的每个元素都调用一次domManip处理。

//webkit下,我们不能克隆文含有checked的档碎片
if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) {
  return this.each(function( index ) {
    var self = set.eq( index );
    //如果args[0]是函数,则执行函数返回结果替换原来的args[0]
    if ( isFunction ) {
      args[0] = value.call( this, index, table ? self.html() : undefined );
    }
    self.domManip( args, table, callback );
  });
}

第三步,处理正常情况,使用传入的节点构建文档碎片,并插入文档中。这里面构建的文档碎片就需要重复使用,区别于第二步的处理。这里面需要注意的是如果是script节点需要在加载完成后执行。顺着源码顺序看一下过程

构建文档碎片

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

分离出其中的script,这其中有一个函数disableScript更改了script标签的type值以确保安全,原来的type值是"text/javascript",改成了"true/text/javascript"或"false/text/javascript"

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 );
    // Keep references to cloned scripts for later restoration
    if ( hasScripts ) {
      jQuery.merge( scripts, getAll( node, "script" ) );
    }
  }
  callback.call(
    table && jQuery.nodeName( this[i], "table" ) ?
    findOrAppend( this[i], "tbody" ) :
    this[i],
    node,
    i
    );
}

执行script,分两种情况,远程的使用ajax来处理,本地的直接执行。

if ( hasScripts ) {
  doc = scripts[ scripts.length - 1 ].ownerDocument;
  // Reenable scripts
  jQuery.map( scripts, restoreScript );
  //在第一个文档插入使执行可执行脚本
  for ( i = 0; i < hasScripts; i++ ) {
    node = scripts[ i ];
    if ( rscriptType.test( node.type || "" ) &&
      !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
      if ( node.src ) {
        // Hope ajax is available...
        jQuery.ajax({
          url: node.src,
          type: "GET",
          dataType: "script",
          async: false,
          global: false,
          "throws": true
        });
      } else {
        jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) );
      }
    }
  }
}

b. dom操作拓展

jQuery.fn.text

jQuery.fn.text: function( value ) {
  return jQuery.access( this, function( value ) {
    return value === undefined ?
    jQuery.text( this ) :
    this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );
  }, null, value, arguments.length );
}

 

最终执行value === undefined ? jQuery.text( this ) : this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) );

其中jQuery.text = Sizzle.getText;

jQuery.fn.html

函数使用jQuery.access来处理

jQuery.fn.html: function( value ) {
      return jQuery.access( this, function( value ) {...}, null, value, arguments.length );
    }

如果没有参数表示是取值

if ( value === undefined ) {
  return elem.nodeType === 1 ?
  elem.innerHTML.replace( rinlinejQuery, "" ) :
  undefined;
}

否则看是否能用innerHTML添加内容。点击参考兼容问题

//看看我们是否可以走了一条捷径,只需使用的innerHTML
//需要执行的代码script|style|link等不能使用innerHTML
//htmlSerialize:确保link节点能使用innerHTML正确序列化,这就需要在IE浏览器的包装元素
//leadingWhitespace:IE strips使用.innerHTML需要以空白开头
//不是需要额外添加结束标签或外围包装标签的元素
if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
  ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) &&
  ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) &&
  !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) {
  value = value.replace( rxhtmlTag, "<$1></$2>" );
  try {
    for (; i < l; i++ ) {
        //移除元素节点和缓存,阻止内存泄漏
        elem = this[i] || {};
        if ( elem.nodeType === 1 ) {
          jQuery.cleanData( getAll( elem, false ) );
          elem.innerHTML = value;
        }
      }
      elem = 0;
    //如果使用innerHTML抛出异常,使用备用方法
  } catch(e) {}
}

如果不能使用innerHTML或使用不成功(抛出异常),则使用备用方法append

//备用方法,使用append添加节点
if ( elem ) {
  this.empty().append( value );
}

jQuery.fn.wrapAll(用单个标签将所有匹配元素包裹起来)
 
处理步骤:

传入参数是函数则将函数结果传入
if ( jQuery.isFunction( html ) ) {

return this.each(function(i) {


jQuery(this).wrapAll( html.call(this, i) );

});
}

创建包裹层
//获得包裹标签 The elements to wrap the target around
var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
if ( this[0].parentNode ) {

wrap.insertBefore( this[0] );
}

用包裹裹住当前jQuery对象

wrap.map(function() {
var elem = this;

while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {


elem = elem.firstChild;

}
 
return elem;
}).append( this );

注意:当前jQuery对象匹配的元素最好只有一个,如果有多个的话不推荐使用,这种情况慎用,后面举例可以看到。

简单的例子,原DOM为(后面都使用这个例子)

<div id='center' class="center">
  <div id='ss' class="center">
    <input type='submit' id='left' class="left">
  </div>
</div>
<div class="right">我是right</div>
$('#center').wrapAll("<p></p>")后,dom变成了
<p>

<div id="center" class="center">


<div id="ss" class="center">



<input type="submit" id="left" class="left">


</div>

</div>
</p>
<div class="right">我是right</div>

慎用:如果当前jQuery所匹配的元素不止一个,例如原DOM执行$('div').wrapAll(“<p></p>”)后结果DOM变成

<p>
<div id="center" class="center"></div>

<div id="ss" class="center">
    <input type="submit" id="left" class="left">
  </div>

<div class="right">我是right</div>
</p>

看到结果了吧,本来#center是#ss的父节点,结果变成了#ss的兄弟节点。

jQuery.fn.wrapInner(在每个匹配元素的所有子节点外部包裹指定的HTML结构)

处理步骤:

传入参数是函数则将函数结果传入

if ( jQuery.isFunction( html ) ) {
  return this.each(function(i) {
    jQuery(this).wrapInner( html.call(this, i) );
  });
}

遍历jQuery对象数组,获取每个元素包含的内容(所有子节点)contents,然后使用warpAll包裹住contents

return this.each(function() {
  var self = jQuery( this ),
  contents = self.contents();

  if ( contents.length ) {
    contents.wrapAll( html );

  } else {
    self.append( html );
  }
});

还是使用上面的例子中的原DOM,执行$('div').wrapInner('<p></p>')后结果DOM变成

<div id="center" class="center">
<p>


<div id="ss" class="center">



<p>




<input type="submit" id="left" class="left">



</p>


</div>

</p>
</div>
<div class="right">

<p>


我是right

</p>
</div>

jQuery.fn.wrap(在每个匹配元素外部包裹指定的HTML结构)

对jQuery的每个元素分别使用wrapAll包裹一下

return this.each(function(i) {
  jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
});

行$('div').wrap('<p></p>')后结果DOM变成

<p>
<div id="center" class="center">


<p>



<div id="ss" class="center">




<input type="submit" id="left" class="left">



</div>


</p>

</div>
</p>
<p>

<div class="right">我是right</div>
</p>

 jQuery.fn.unwrap(移除每个匹配元素的父元素)

使用replaceWith用匹配元素父节点的所有子节点替换匹配元素的父节点。当然了父节点是body/html/document肯定是移除不了的

return this.parent().each(function() {
  if ( !jQuery.nodeName( this, "body" ) ) {
    jQuery( this ).replaceWith( this.childNodes );
  }
}).end();
执行$('div').wrap()后结果DOM变成
<div id="ss" class="center">

<input type="submit" id="left" class="left">
</div>
<div class="right">我是right</div>

jQuery.fn.remove(从文档中移除匹配的元素)

你还可以使用选择器进一步缩小移除的范围,只移除当前匹配元素中符合指定选择器的部分元素。

与detach()相比,remove()函数会同时移除与元素关联绑定的附加数据( data()函数 )和事件处理器等(detach()会保留)。

for ( ; (elem = this[i]) != null; i++ ) {
  if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) {
    // detach传入的参数keepData为true,不删除缓存
    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 );
    }
  }
}

可以看到其中有一个重要的函数cleanData,该方法是用来清除缓存:遍历每一个节点元素,对每一个节点元素做一下处理:

1.获取当前元素对应的缓存

id = elem[ internalKey ];
data = id && cache[ id ];

2.如果有绑定事件,则遍历解绑事件

if ( data.events ) {
  for ( type in data.events ) {
    if ( special[ type ] ) {
      jQuery.event.remove( elem, type );
    //这是一个快捷方式,以避免jQuery.event.remove的开销
    } else {
      jQuery.removeEvent( elem, type, data.handle );
    }
  }
}

3.如果jQuery.event.remove没有移除cache,则手动移除cache。其中IE需要做一些兼容处理,而且最终会将删除历史保存如core_deletedIds中

//当jQuery.event.remove没有移除cache的时候,移除cache
if ( cache[ id ] ) {
  delete cache[ id ];
  //IE不允许从节点使用delete删除expando特征,
  //也能对文件节点使用removeAttribute函数;
  //我们必须处理所有这些情况下,
  if ( deleteExpando ) {
    delete elem[ internalKey ];
  } else if ( typeof elem.removeAttribute !== core_strundefined ) {
    elem.removeAttribute( internalKey );
  } else {
    elem[ internalKey ] = null;
  }
  core_deletedIds.push( id );
}

jQuery.fn.detach

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

jQuery.fn.empty(清空每个匹配元素内的所有内容(所有子节点))

函数将会移除每个匹配元素的所有子节点(包括文本节点、注释节点等所有类型的节点),会清空相应的缓存数据。

for ( ; (elem = this[i]) != null; i++ ) {
  //防止内存泄漏移除元素节点缓存
  if ( elem.nodeType === 1 ) {
    jQuery.cleanData( getAll( elem, false ) );
  }
  //移除所有子节点
  while ( elem.firstChild ) {
    elem.removeChild( elem.firstChild );
  }
  // IE<9,select节点需要将option置空
  if ( elem.options && jQuery.nodeName( elem, "select" ) ) {
    elem.options.length = 0;
  }
}
Javascript 相关文章推荐
Javascript 错误处理的几种方法
Jun 13 Javascript
初学Jquery插件制作 在SageCRM的查询屏幕隐藏部分行的功能
Dec 26 Javascript
jquery easyui 结合jsp简单展现table数据示例
Apr 18 Javascript
javascript面向对象之对象的深入理解
Jan 13 Javascript
JavaScript实现SHA-1加密算法的方法
Mar 11 Javascript
jQuery使用hide方法隐藏页面上指定元素的方法
Mar 30 Javascript
js滚动条平滑移动示例代码
Mar 29 Javascript
详解Vue中localstorage和sessionstorage的使用
Dec 22 Javascript
详解webpack 最简打包结果分析
Feb 20 Javascript
Vue中Table组件Select的勾选和取消勾选事件详解
Mar 19 Javascript
Bootstrap table 实现树形表格联动选中联动取消功能
Sep 30 Javascript
解决vue项目axios每次请求session不一致的问题
Oct 24 Javascript
Bootstrap每天必学之栅格系统(布局)
Nov 25 #Javascript
jQuery实现宽屏图片轮播实例教程
Nov 24 #Javascript
jquery利用拖拽方式在图片上添加热链接
Nov 24 #Javascript
jquery中checkbox使用方法简单实例演示
Nov 24 #Javascript
基于Jquery和CSS3制作数字时钟附源码下载(CSS3篇)
Nov 24 #Javascript
基于jQuery和CSS3制作数字时钟附源码下载(jquery篇)
Nov 24 #Javascript
基于jquery实现简单的手风琴特效
Nov 24 #Javascript
You might like
星际争霸任务指南——虫族
2020/03/04 星际争霸
php根据操作系统转换文件名大小写的方法
2014/02/24 PHP
PHP将二维数组某一个字段相同的数组合并起来的方法
2016/02/26 PHP
飞鱼(shqlsl) javascript作品集
2006/12/16 Javascript
用javascript将数据库中的TEXT类型数据动态赋值到TEXTAREA中
2007/04/20 Javascript
JS小功能(offsetLeft实现图片滚动效果)实例代码
2013/11/28 Javascript
JavaScript函数的一些注意要点小结及js匿名函数
2015/11/10 Javascript
基于jQuery实现放大镜特效
2020/10/19 Javascript
使用原生js写ajax实例(推荐)
2017/05/31 Javascript
关于vue的npm run dev和npm run build的区别介绍
2019/01/14 Javascript
微信小程序如何修改本地缓存key中单个数据的详解
2019/04/26 Javascript
详解微信小程序胶囊按钮返回|首页自定义导航栏功能
2019/06/14 Javascript
微信小程序实现天气预报功能(附源码)
2020/12/10 Javascript
JavaScript 中的六种循环方法
2021/01/06 Javascript
[33:33]完美世界DOTA2联赛PWL S2 FTD.C vs SZ 第二场 11.27
2020/11/30 DOTA
python开发之for循环操作实例详解
2015/11/12 Python
Python实现的多线程同步与互斥锁功能示例
2017/11/30 Python
python基础学习之如何对元组各个元素进行命名详解
2018/07/12 Python
python调用摄像头显示图像的实例
2018/08/03 Python
用python标准库difflib比较两份文件的异同详解
2018/11/16 Python
python实现在cmd窗口显示彩色文字
2019/06/24 Python
浅谈keras的深度模型训练过程及结果记录方式
2020/01/24 Python
利用python绘制数据曲线图的实现
2020/04/09 Python
金宝贝童装官网:Gymboree
2016/08/31 全球购物
美国知名的时尚购物网站:Anthropologie
2016/12/22 全球购物
维多利亚的秘密官方旗舰店:VICTORIA’S SECRET
2018/04/02 全球购物
电影T恤、80年代T恤和80年代服装:TV Store Online
2020/01/05 全球购物
C++面试题:关于链表和指针
2013/06/05 面试题
在对linux系统分区进行格式化时需要对磁盘簇(或i节点密度)的大小进行选择,请说明选择的原则
2012/01/13 面试题
三分钟演讲稿事例
2014/03/03 职场文书
活动倡议书范文
2014/05/13 职场文书
学校百日安全生产活动总结
2014/07/05 职场文书
幼儿园大班教育随笔
2015/08/14 职场文书
python状态机transitions库详解
2021/06/02 Python
Pycharm连接远程服务器并远程调试的全过程
2021/06/24 Python
Python实现滑雪小游戏
2021/09/25 Python