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 相关文章推荐
jQuery 美元符冲突的解决方法
Mar 28 Javascript
在网页中使用document.write时遭遇的奇怪问题
Aug 24 Javascript
JavaScript数组对象实现增加一个返回随机元素的方法
Jul 27 Javascript
JS判断元素是否在数组内的实现代码
Mar 30 Javascript
个人网站留言页面(前端jQuery编写、后台php读写MySQL)
May 03 Javascript
JavaScript的Vue.js库入门学习教程
May 23 Javascript
node.js缺少mysql模块运行报错的解决方法
Nov 13 Javascript
vue.js全局API之nextTick全面解析
Jul 07 Javascript
Angular.js实现获取验证码倒计时60秒按钮的简单方法
Oct 18 Javascript
vue+express+jwt持久化登录的方法
Jun 14 Javascript
基于axios 的responseType类型的设置方法
Oct 29 Javascript
vue用elementui写form表单时,在label里添加空格操作
Aug 13 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
一个程序下载的管理程序(三)
2006/10/09 PHP
php下过滤html代码的函数 提高程序安全性
2010/03/02 PHP
Thinkphp搭建包括JS多语言的多语言项目实现方法
2014/11/24 PHP
php递归遍历删除文件的方法
2015/04/17 PHP
ThinkPHP3.2.3实现分页的方法详解
2016/06/03 PHP
PHP实现的最大正向匹配算法示例
2017/12/19 PHP
PHP根据key删除数组中指定的元素
2019/02/28 PHP
PHP代码加密的方法总结
2020/03/13 PHP
JavaScript 对象成员的可见性说明
2009/10/16 Javascript
js 屏蔽鼠标右键脚本附破解方法
2009/12/03 Javascript
JQuery Tips(4) 一些关于提高JQuery性能的Tips
2009/12/19 Javascript
关于URL中的特殊符号使用介绍
2011/11/03 Javascript
Javascript中的delete介绍
2012/09/02 Javascript
利用JS进行图片的切换即特效展示图片
2013/12/03 Javascript
无闪烁更新网页内容JS实现
2013/12/19 Javascript
JavaScript在浏览器标题栏上显示当前日期和时间的方法
2015/03/19 Javascript
jquery trigger函数执行两次的解决方法
2016/02/29 Javascript
jQuery实现鼠标跟随提示层效果代码(可显示文本,Div,Table,Html等)
2016/04/18 Javascript
jQuery Easyui快速入门教程
2016/08/21 Javascript
基于JS实现二维码图片固定在右下角某处并跟随滚动条滚动
2017/02/08 Javascript
PHP实现本地图片上传和验证功能
2017/02/27 Javascript
Bootstrap弹出框(Popover)被挤压的问题小结
2017/07/11 Javascript
JavaScript实现多个物体同时运动
2020/03/12 Javascript
JavaScript设计模式--桥梁模式引入操作实例分析
2020/05/23 Javascript
在Vue里如何把网页的数据导出到Excel的方法
2020/09/30 Javascript
python判断、获取一张图片主色调的2个实例
2014/04/10 Python
Python字符串详细介绍
2015/05/09 Python
纽约的奢华内衣店:Journelle
2016/07/29 全球购物
L’Artisan Parfumeur官网:法国香水品牌
2020/08/11 全球购物
JRE、JDK、JVM之间的关系怎样
2012/05/16 面试题
前处理组长岗位职责
2014/03/01 职场文书
教师应聘自荐信范文
2014/03/14 职场文书
村容村貌整治方案
2014/05/21 职场文书
2014年小学德育工作总结
2014/12/05 职场文书
高三英语教学计划
2015/01/23 职场文书
普希金诗歌赏析(6首)
2019/08/22 职场文书