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 相关文章推荐
JMenuTab简单使用说明
Mar 13 Javascript
javascript动态加载三
Aug 22 Javascript
jQuery学习笔记之toArray()
Jun 09 Javascript
基于jQuery实现的图片切换焦点图整理
Dec 07 Javascript
jquery获取当前元素索引值用法实例
Jun 10 Javascript
javascript中使用new与不使用实例化对象的区别
Jun 22 Javascript
js实现点击文本框显示日期选择器特效代码分享
May 21 Javascript
jQuery form插件的使用之处理server返回的JSON, XML,HTML数据
Jan 26 Javascript
js实现一个可以兼容PC端和移动端的div拖动效果实例
Dec 09 Javascript
详解Javascript 中的 class、构造函数、工厂函数
Dec 20 Javascript
解决vue-cli + webpack 新建项目出错的问题
Mar 20 Javascript
简单了解vue中的v-if和v-show的区别
Oct 08 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
不错的PHP学习之php4与php5之间会穿梭一点点感悟
2007/05/03 PHP
封装一个PDO数据库操作类代码
2009/09/09 PHP
HTML node相关的一些资料整理
2010/01/01 Javascript
iframe自适应宽度、高度 ie6 7 8,firefox 3.86下测试通过
2010/07/29 Javascript
探索Emberjs制作一个简单的Todo应用
2012/11/07 Javascript
js自定义事件及事件交互原理概述(二)
2013/02/01 Javascript
Jquery实现图片放大镜效果的思路及代码(自写)
2013/10/18 Javascript
JS window对象的top、parent、opener含义介绍
2013/12/03 Javascript
js字符串日期yyyy-MM-dd转化为date示例代码
2014/03/06 Javascript
javascript验证身份证号
2015/03/03 Javascript
javascript+html5实现绘制圆环的方法
2015/07/28 Javascript
使用jQuery制作浮动工具栏的实例分享
2016/05/13 Javascript
全面介绍javascript实用技巧及单竖杠
2016/07/18 Javascript
BootStrap fileinput.js文件上传组件实例代码
2017/02/20 Javascript
JS加密插件CryptoJS实现的DES加密示例
2018/08/16 Javascript
Vue 路由切换时页面内容没有重新加载的解决方法
2018/09/01 Javascript
ECharts地图绘制和钻取简易接口详解
2019/07/12 Javascript
JavaScript实现简单进度条效果
2020/03/25 Javascript
Vue循环遍历选项赋值到对应控件的实现方法
2020/06/22 Javascript
Vue时间轴 vue-light-timeline的用法说明
2020/10/29 Javascript
Python Web框架Flask中使用新浪SAE云存储实例
2015/02/08 Python
Python实现的彩票机选器实例
2015/06/17 Python
python实现AES加密解密
2019/03/28 Python
为什么你还不懂得怎么使用Python协程
2019/05/13 Python
Django如何防止定时任务并发浅析
2019/05/14 Python
基于Python检测动态物体颜色过程解析
2019/12/04 Python
如何安装并在pycharm使用selenium的方法
2020/04/30 Python
Python使用struct处理二进制(pack和unpack用法)
2020/11/12 Python
html通过canvas转成base64的方法
2019/07/18 HTML / CSS
泰国汽车、火车和轮渡票预订网站:Bus Online Ticket
2017/09/09 全球购物
人力资源管理专业应届生求职信
2014/04/24 职场文书
公司自我介绍演讲稿
2014/08/21 职场文书
2014年服务员工作总结
2014/11/18 职场文书
面试感谢信范文
2015/01/22 职场文书
辅导员学期工作总结
2015/08/14 职场文书
python脚本框架webpy的url映射详解
2021/11/20 Python