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 TextBox自动完成条
Jul 22 Javascript
Javascript 键盘keyCode键码值表
Dec 24 Javascript
jquery 插件实现图片延迟加载效果代码
Feb 06 Javascript
基于jQuery的试卷自动排版系统实现代码
Jan 06 Javascript
javascript的offset、client、scroll使用方法详解
Dec 25 Javascript
javascript中indexOf技术详解
May 07 Javascript
JavaScript实现自动变换表格边框颜色
May 08 Javascript
Vue.js中的图片引用路径的方式
Jul 28 Javascript
使用use注册Vue全局组件和全局指令的方法
Mar 08 Javascript
vue 组件中slot插口的具体用法
Apr 03 Javascript
vue拦截器实现统一token,并兼容IE9验证功能
Apr 26 Javascript
浅谈高大上的微信小程序中渲染html内容—技术分享
Oct 25 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 Notice: Undefined index 错误提示解决方法
2010/08/29 PHP
浅谈php serialize()与unserialize()的用法
2013/06/05 PHP
通过curl模拟post和get方式提交的表单类
2014/04/23 PHP
分享50个提高PHP执行效率的技巧
2015/12/26 PHP
Thinkphp连表查询及数据导出方法示例
2016/10/15 PHP
thinkPHP实现的省市区三级联动功能示例
2017/05/05 PHP
javascript学习笔记(十三) js闭包介绍(转)
2012/06/20 Javascript
Jquery Validate 正则表达式实用验证代码大全
2013/08/23 Javascript
Javascript setInterval的两种调用方法(实例讲解)
2013/11/29 Javascript
从QQ网站中提取的纯JS省市区三级联动菜单
2013/12/25 Javascript
AngularJS内建服务$location及其功能详解
2016/07/01 Javascript
Javascript实现base64的加密解密方法示例
2017/06/27 Javascript
JavaScript数据结构之双向链表定义与使用方法示例
2017/10/27 Javascript
webpack打包react项目的实现方法
2018/06/21 Javascript
JavaScript展开操作符(Spread operator)详解
2019/07/20 Javascript
vue+vant实现购物车全选和反选功能
2020/11/17 Vue.js
详解javascript脚本何时会被执行
2021/02/05 Javascript
使用Python实现博客上进行自动翻页
2017/08/23 Python
python3爬取淘宝信息代码分析
2018/02/10 Python
如何使用 Pylint 来规范 Python 代码风格(来自IBM)
2018/04/06 Python
Python 使用类写装饰器的小技巧
2018/09/30 Python
python2.7实现邮件发送功能
2018/12/12 Python
opencv 获取rtsp流媒体视频的实现方法
2019/08/23 Python
Python 调用有道翻译接口实现翻译
2020/03/02 Python
pyautogui自动化控制鼠标和键盘操作的步骤
2020/04/01 Python
Python3爬虫中Splash的知识总结
2020/07/10 Python
基于CSS3实现立方体自转效果
2016/03/01 HTML / CSS
Expedia丹麦:全球领先的旅游网站
2018/03/18 全球购物
英国最受信任的在线眼镜商之一:Fashion Eyewear
2019/10/31 全球购物
某公司Java工程师面试题笔试题
2016/03/27 面试题
战略合作协议书范本
2014/04/18 职场文书
律师授权委托书范本
2014/10/07 职场文书
作风建设整改方案
2014/10/27 职场文书
2015年外联部工作总结
2015/04/03 职场文书
公司酒会主持词
2015/07/02 职场文书