美文网首页
当我调用了$().append()后,jQuery内部发生了什么

当我调用了$().append()后,jQuery内部发生了什么

作者: 小进进不将就 | 来源:发表于2019-03-27 19:42 被阅读0次

    前言:这篇我们着讲


    1、有这样一个页面:

    <body>
      <button id="test1">append操作</button>
    
      <table class="inner">
        <tbody></tbody>
      </table>
    
    </body>
    
    <script>
      $('#test1').click(function(){
        let innerArr=document.querySelectorAll(".inner")
        ajQuery.append(innerArr,'<tr><td>test1</td></tr>')
      })
    </script>
    

    注意:不要 append(<tr>test1</tr>),规范写法是 append(<tr><td>test1</td></tr>)


    2、像之前的文章一样,我们自定义 append() 方法

      let ajQuery={}
      jQuery.each({
          //例:'<p>Test</p>'
          //源码6011行-6019行
          // 在被选元素的结尾插入指定内容
          /*append的内部的原理,就是通过创建一个文档碎片,把新增的节点放到文档碎片中,通过文档碎片克隆到到页面上去,目的是效率更高*/
          append: function(nodelist, arguments) {
            //node是由domManip处理得到的文档碎片documentFragment,里面包含要插入的DOM节点
            let callbackOne=function( node ) {
              console.log(node,'node149')
              //this指的就是$("xxx")
              //1:元素节点,11:DocumentFragment,9:document
              if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                //table插入tr的额外判断
                //target默认情况是selector,即document.querySelectorAll(".inner")
                let target = manipulationTarget( this, node )
                console.log(target,node.childNodes,'node147')
                //append的本质即使用原生appendChild方法在被选元素内部的结尾插入指定内容
                target.appendChild( node );
              }
            }
    
            console.log(nodelist,arguments,'this120')
            return domManip( nodelist, arguments, callbackOne );
          },
    
        },
    
        function(key, value) {
          ajQuery[key] = function(nodelist, arguments) {
            console.log(nodelist,'nodelist128')
              return  value(nodelist, arguments);
            }
          }
        )
    

    3、可以看到,append() 内部调用了 domManip 的方法,接下来重点介绍下该方法
    (1)什么是 domManip ?
    domManip() 是 jQuery DOM 的核心函数。dom 即 Dom 元素,Manip 是Manipulate 的缩写,连在一起就是 Dom 操作的意思。

    (2)它的作用是?
    domManip() 是用来处理 $().append(xxx)$().after(xxx) 等操作 DOM 方法的参数的,统一将其处理为 DOM 类型节点,并交由 callback 函数处理,即上图的 callbackOne

    注意:本文暂不考虑参数包含 <script> 的情况,如:

    ajQuery.append(innerArr,"<script>alert('append执行script')")


    4、domManip() 的三个参数:nodelist, arguments, callbackOne
    nodelist:即 document.querySelectorAll(".inner")

    arguments:即字符串 '<tr><td>test1</td><tr>'

    callbackOne:回调函数,在 nodelist、arguments 被相应逻辑处理后会返回一个文档碎片documentFragment,该方法会对 该文档碎片进行处理

    注意:domMainp 函数讲解在 第 8 点。


    5、callbackOne()
    作用:
    将 domManip 返回的 documentFragment 插入到 selector 的内部末尾。

    也就是说 $().append() 的本质是 DOM节点.appendChild(处理过的documentFragment(里面包含插入的DOM节点))

    源码:

            //node是由domManip处理得到的文档碎片documentFragment,里面包含要插入的DOM节点
            let callbackOne=function( node ) {
              console.log(node,'node149')
              //this指的就是$("xxx")
              //1:元素节点,11:DocumentFragment,9:document
              if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                //table插入tr的额外判断
                //target默认情况是selector,即document.querySelectorAll(".inner")
                let target = manipulationTarget( this, node )
                console.log(target,node.childNodes,'node147')
                //append的本质即使用原生appendChild方法在被选元素内部的结尾插入指定内容
                target.appendChild( node );
              }
            }
    

    6、callbackOne() 中的函数:manipulationTarget()
    作用:
    额外判断,当选择器是table,并且插入的元素是tr时,会查找到table下的tbody,并返回tbody

    源码:

      //源码5724行-5733行
      //额外判断,当选择器是table,并且插入的元素是tr时,会查找到table下的tbody,并返回tbody
      //this, node
      function manipulationTarget( selector, node ) {
        console.log(node.childNodes,node.firstChild,'node73')
        // 如果是table里面插入行tr
        if ( nodeName( selector, "table" ) &&
          nodeName( node.nodeType !== 11 ? node : node.firstChild, "tr" ) ) {
          return jQuery( selector ).children( "tbody" )[ 0 ] || selector
        }
        return selector
      }
    

    7、manipulationTarget() 中的函数:nodeName()
    作用:
    判断两个参数的nodename是否相等

    源码:

      //源码2843行-2847行
      //判断两个参数的nodename是否相等
      function nodeName( selector, name ) {
        return selector.nodeName && selector.nodeName.toLowerCase() === name.toLowerCase();
      }
    

    8、jQueryDOM 核心函数:domManip()
    作用:
    将传入的参数(dom节点元素、字符串、函数)统一转化为符合要求的DOM节点

    源码:

      //源码5597行-5586行
      //作用是将传入的参数(dom节点元素、字符串、函数)统一转化为符合要求的DOM节点
      //例:$('.inner').append('<tr><td>Test</td></tr>')
      //nodelist即$('.inner')
      //args即<tr><td>Test</td></tr>
      function domManip( nodelist, args, callback ) {
        console.log(nodelist,args,'ignored5798')
        //数组深复制成新的数组副本
        //源码是:args = concat.apply( [], args ),这里没有用arguments,而是传参就改了
        let argsArr = []
        argsArr.push(args)
        console.log(argsArr,'args31')
        //l 长度,比如类名为.inner的li有两组,2
        let fragment,
          first,
          node,
          i = 0,
          //l 长度,比如类名为.inner的li有两组,2
          l = nodelist.length,
          iNoClone = l - 1
        //l=2
        console.log(l,'lll45')
        if ( l ) {
          console.log(argsArr,nodelist[0].ownerDocument,nodelist,'firstChild40')
          //argsArr:<tr><td>test1</td></tr>
          //nodelist[0].ownerDocument:目标节点所属的文档
          fragment = buildFragment(argsArr,nodelist[0].ownerDocument,false,nodelist );
          first=fragment.firstChild
          console.log(fragment.childNodes,'firstChild42')
          //即<tr><td>test1</td></tr>
          if (first) {
            //=====根据nodelist的长度循环操作========
            for ( ; i < l; i++ ) {
              console.log(node,fragment.childNodes,'childNodes49')
              node = fragment;
              if ( i !== iNoClone ) {
                /*createDocumentFragment创建的元素是一次性的,添加之后就不能再操作了,
                所以需要克隆iNoClone的多个节点*/
                node = jQuery.clone( node, true, true );
              }
              console.log(nodelist[i], node.childNodes,'node50')
              //call(this,param)
              callback.call( nodelist[i], node);
            }
            //====================
          }
        }
        console.log(nodelist,'nodelist58')
        return nodelist
      }
    

    解析:
    我们可以看到在 目标节点的个数 >=1 的情况下(if(l){xxx}),
    调用了 buildFragment() 方法,该方法作用是 创建文档碎片documentFragment,以便高效地向 目标节点 插入元素,然后根据 目标节点个数 循环地调用 callback 方法,即调用 原生 appendChild 方法插入元素。

    注意:
    由于 createDocumentFragment 创建的元素是一次性的,添加之后就成只读的了,所以需要克隆 createDocumentFragment创建的元素,以便再次操作。

    关于 documentFragment,请看文章: jQuery之documentFragment


    9、domManip() 中的函数 buildFragment()
    作用:
    创建文档碎片

    源码:

      //源码4857行-4945行
      /*创建文档碎片,原因是一般情况下,我们向DOM中添加新的元素或者节点,DOM会立刻更新。
      如果向DOM添加100个节点,那么就得更新100次,非常浪费浏览器资源。
      解决办法就是:我们可以创建一个文档碎片(documentFragment),
      documentFragment类似于一个小的DOM,在它上面使用innerHTML并在innerHTML上插入多个节点,速度要快于DOM(2-10倍),
      比如:先将新添加的100个节点添加到文档碎片的innerHTML上,再将文档碎片添加到DOM上。*/
      //args, collection[ 0 ].ownerDocument, false, collection
      function buildFragment( arr, context, truefalse, selection ) {
        let elem,tmp, nodes = [], i = 0, l = arr.length,wrap,tag,j
        // createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。
        //相当于document.createDocumentFragment()
        let fragment = context.createDocumentFragment()
        //l=1
        console.log(l,'l87')
        //==============
        for ( ; i < l; i++ ) {
          //'<tr><td></td></tr>'
          elem = arr[ i ];
          console.log(i,elem,'elem90')
          if ( elem || elem === 0 ) {
            /*创建div是为了处理innerHTML的缺陷(IE会忽略开头的无作用域元素),
              让所有的元素都被div元素给包含起来,包括script,style等无作用域的元素*/
            tmp=fragment.appendChild( context.createElement( "div" ) )
            //就是匹配div不支持的标签,如 tr、td等
            /*不支持innerHTML属性的元素,通过正则单独取出处理*/
            tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
    
            /*作用就是利用wrapMap让不支持innerHTML的元素通过包装wrap来支持innerHTML*/
            //ie对字符串进行trimLeft操作,其余是用户输入处理
            //很多标签不能单独作为DIV的子元素
            /*td,th,tr,tfoot,tbody等等,需要加头尾*/
            wrap = wrapMap[ tag ] || wrapMap._default // tr: [ 2, "<table><tbody>", "</tbody></table>" ]
            console.log(wrap,'wrap152')
            //将修正好的element添加进innerHTML中
            //jQuery.htmlPrefilter:标签转换为闭合标签,如<table> --> <table></table>
            /*div不支持tr、td所以需要添加头尾标签,如<div><table><tbody>xxxx</tbody></table>*/
            tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
            // 因为warp被包装过,需要找到正确的元素父级
            j = wrap[ 0 ]; //2
            while ( j-- ) {
              tmp = tmp.lastChild;
            }
            //temp:<tbody></tbody>
            //tmp.childNodes:tr
            //nodes:[]
            //jQuery.merge:将两个数组合并到第一个数组中
            jQuery.merge( nodes, tmp.childNodes );
          }
        }
        //================
        // Remove wrapper from fragment
        fragment.textContent = "";
        //需要将i重置为0
        i=0
        while ( ( elem = nodes[ i++ ] ) ) {
          fragment.appendChild( elem )
        }
    
        console.log(fragment.childNodes,'fragment105')
        return fragment;
      }
    

    解析:
    (1)创建文档碎片 documentFragment

    let fragment = context.createDocumentFragment()
    

    (2)在 待插入的元素存在的情况下,先在 documentFragment 内部插入 <div></div> 标签

    创建div是为了处理innerHTML的缺陷(IE会忽略开头的无作用域元素),所以让所有的元素都被div元素给包含起来,包括script,style等无作用域的元素

    (3)但是 <div> 也有不支持的子元素,通过 wrap 筛选并包装这些子元素

    比如<tr>标签,会被 wrap 转为 <table><tbody></tbody></table>,再成功添加到 documentFragment 的 innerHTML 中。

    (4)documentFragment 在成功添加完子元素后,再卸磨杀驴,去掉包裹的节点,如上例的<div><table><tbody></tbody></table></div>,保留待插入的节点<tr><td>test1</td></tr>

    (5)最后返回 处理好的文档碎片 fragment


    10、rtagName
    作用:
    匹配div不支持的标签,如 tr、td等。

    源码:

      let rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i )
    

    11、wrapMap
    作用:
    div 不支持的标签表

    源码:

      let wrapMap = {
        // Support: IE <=9 only
        option: [ 1, "<select multiple='multiple'>", "</select>" ],
        // XHTML parsers do not magically insert elements in the
        // same way that tag soup parsers do. So we cannot shorten
        // this by omitting <tbody> or other required elements.
        thead: [ 1, "<table>", "</table>" ],
        col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
        tr: [ 2, "<table><tbody>", "</tbody></table>" ],
        td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
        _default: [ 0, "", "" ]
      };
    
      // Support: IE <=9 only
      wrapMap.optgroup = wrapMap.option;
      wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
      wrapMap.th = wrapMap.td;
    

    12、jQuery.htmlPrefilter()
    作用:
    标签转换为闭合标签,如<table>--><table></table>

    源码:

        htmlPrefilter: function( html ) {
          return html.replace( rxhtmlTag, "<$1></$2>" );
        }
    

    13、综上,当我调用了$('.inner').append('<tr><td>test1</td></tr>')后,jQuery内部发生的事件如下

    14、本篇文章的所有代码
    github:
    https://github.com/AttackXiaoJinJin/jQueryExplain/blob/master/%E9%80%8F%E8%BF%87%24().append%E7%9C%8BdomMainp%E5%92%8CbuildFragment.html

    代码:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>jQuery的遍历结构设计之节点操作</title>
    
    </head>
    <body>
    <script src="jQuery.js"></script>
      <button id="test1">append操作</button>
    
      <table class="inner">
        <!--<tbody></tbody>-->
      </table>
    
    
    <script>
      //匹配div不支持的标签,如 tr、td等
      let rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i );
      //================================
      let wrapMap = {
        // Support: IE <=9 only
        option: [ 1, "<select multiple='multiple'>", "</select>" ],
        // XHTML parsers do not magically insert elements in the
        // same way that tag soup parsers do. So we cannot shorten
        // this by omitting <tbody> or other required elements.
        thead: [ 1, "<table>", "</table>" ],
        col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
        tr: [ 2, "<table><tbody>", "</tbody></table>" ],
        td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
        _default: [ 0, "", "" ]
      };
    
      // Support: IE <=9 only
      wrapMap.optgroup = wrapMap.option;
      wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
      wrapMap.th = wrapMap.td;
      //================================
    
      //源码5597行-5586行
      //作用是将传入的参数(dom节点元素、字符串、函数)统一转化为符合要求的DOM节点
      //例:$('.inner').append('<tr><td>test1</tr></td>')
      //nodelist(collections)即$('.inner')
      //args即<tr><td>test1</td></tr>
      function domManip( nodelist, args, callback ) {
        console.log(nodelist,args,'ignored5798')
        //数组深复制成新的数组副本
        //源码是:args = concat.apply( [], args ),这里没有用arguments,而是传参就改了
        let argsArr = []
        argsArr.push(args)
        console.log(argsArr,'args31')
        //l 长度,比如类名为.inner的li有两组,2
        let fragment,
          first,
          node,
          i = 0,
          //l 长度,比如类名为.inner的li有两组,2
          l = nodelist.length,
          iNoClone = l - 1
        //l=2
        console.log(l,'lll45')
        if ( l ) {
          console.log(argsArr,nodelist[0].ownerDocument,nodelist,'firstChild40')
          //argsArr:<p>Test</p>
          //nodelist[0].ownerDocument:目标节点所属的文档
          fragment = buildFragment(argsArr,nodelist[0].ownerDocument,false,nodelist );
          first=fragment.firstChild
          console.log(fragment.childNodes,'firstChild42')
          //即<p>Test</p>
          if (first) {
            //=====根据nodelist的长度循环操作========
            for ( ; i < l; i++ ) {
              console.log(node,fragment.childNodes,'childNodes49')
              node = fragment;
              if ( i !== iNoClone ) {
                /*createDocumentFragment创建的元素是一次性的,添加之后再就不能操作了,
                所以需要克隆iNoClone的多个节点*/
                node = jQuery.clone( node, true, true );
              }
              console.log(nodelist[i], node.childNodes,'node50')
              //call(this,param)
              callback.call( nodelist[i], node);
            }
            //====================
          }
        }
        console.log(nodelist,'nodelist58')
        return nodelist
      }
    
      //源码5724行-5733行
      //额外判断,当选择器是table,并且插入的元素是tr时,会查找到table下的tbody,并返回tbody
      //this, node
      function manipulationTarget( selector, node ) {
        console.log(node.childNodes,node.firstChild,'node73')
        // 如果是table里面插入行tr
        if ( nodeName( selector, "table" ) &&
          nodeName( node.nodeType !== 11 ? node : node.firstChild, "tr" ) ) {
          return jQuery( selector ).children( "tbody" )[ 0 ] || selector
        }
        return selector
      }
    
      //源码2843行-2847行
      //判断两个参数的nodename是否相等
      function nodeName( selector, name ) {
        return selector.nodeName && selector.nodeName.toLowerCase() === name.toLowerCase();
      }
    
      //源码4857行-4945行
      /*创建文档碎片,原因是一般情况下,我们向DOM中添加新的元素或者节点,DOM会立刻更新。
      如果向DOM添加100个节点,那么就得更新100次,非常浪费浏览器资源。
      解决办法就是:我们可以创建一个文档碎片(documentFragment),
      documentFragment类似于一个小的DOM,在它上面使用innerHTML并在innerHTML上插入多个节点,速度要快于DOM(2-10倍),
      比如:先将新添加的100个节点添加到文档碎片的innerHTML上,再将文档碎片添加到DOM上。*/
      //args, collection[ 0 ].ownerDocument, false, collection
      function buildFragment( arr, context, truefalse, selection ) {
        let elem,tmp, nodes = [], i = 0, l = arr.length,wrap,tag,j
        // createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。
        //相当于document.createDocumentFragment()
        let fragment = context.createDocumentFragment()
        //l=1
        console.log(l,'l87')
        //==============
        for ( ; i < l; i++ ) {
          //'<tr><td></td></tr>'
          elem = arr[ i ];
          console.log(i,elem,'elem90')
          if ( elem || elem === 0 ) {
            /*创建div是为了处理innerHTML的缺陷(IE会忽略开头的无作用域元素),
              让所有的元素都被div元素给包含起来,包括script,style等无作用域的元素*/
            tmp=fragment.appendChild( context.createElement( "div" ) )
            //就是匹配div不支持的标签,如 tr、td等
            /*不支持innerHTML属性的元素,通过正则单独取出处理*/
            tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
    
            /*作用就是利用wrapMap让不支持innerHTML的元素通过包装wrap来支持innerHTML*/
            //ie对字符串进行trimLeft操作,其余是用户输入处理
            //很多标签不能单独作为DIV的子元素
            /*td,th,tr,tfoot,tbody等等,需要加头尾*/
            wrap = wrapMap[ tag ] || wrapMap._default // tr: [ 2, "<table><tbody>", "</tbody></table>" ]
            console.log(wrap,'wrap152')
            //将修正好的element添加进innerHTML中
            //jQuery.htmlPrefilter:标签转换为闭合标签,如<table> --> <table></table>
            /*div不支持tr、td所以需要添加头尾标签,如<div><table><tbody>xxxx</tbody></table>*/
            tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
            // 因为warp被包装过,需要找到正确的元素父级
            j = wrap[ 0 ]; //2
            while ( j-- ) {
              tmp = tmp.lastChild;
            }
            //temp:<tbody></tbody>
            //tmp.childNodes:tr
            //nodes:[]
            //jQuery.merge:将两个数组合并到第一个数组中
            jQuery.merge( nodes, tmp.childNodes );
          }
        }
        //================
        // Remove wrapper from fragment
        fragment.textContent = "";
        //需要将i重置为0
        i=0
        while ( ( elem = nodes[ i++ ] ) ) {
          fragment.appendChild( elem )
        }
    
        console.log(fragment.childNodes,'fragment105')
        return fragment;
      }
    
      let ajQuery={}
      jQuery.each({
          //例:'<tr><td>test1</td></tr>'
          //源码6011行-6019行
          // 在被选元素的结尾插入指定内容
          /*append的内部的原理,就是通过创建一个文档碎片,把新增的节点放到文档碎片中,通过文档碎片克隆到到页面上去,目的是效率更高*/
          append: function(nodelist, arguments) {
            //node是由domManip处理得到的文档碎片documentFragment,里面包含要插入的DOM节点
            let callbackOne=function( node ) {
              console.log(node,'node149')
              //this指的就是$("xxx")
              //1:元素节点,11:DocumentFragment,9:document
              if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                //table插入tr的额外判断
                //target默认情况是selector,即document.querySelectorAll(".inner")
                let target = manipulationTarget( this, node )
                console.log(target,node.childNodes,'node147')
                //append的本质即使用原生appendChild方法在被选元素内部的结尾插入指定内容
                target.appendChild( node );
              }
            }
    
            console.log(nodelist,arguments,'this120')
            return domManip( nodelist, arguments, callbackOne );
          },
    
        },
    
        function(key, value) {
          ajQuery[key] = function(nodelist, arguments) {
            console.log(nodelist,'nodelist128')
              return  value(nodelist, arguments);
            }
          }
        )
    
      $('#test1').click(function(){
        let innerArr=document.querySelectorAll(".inner")
        ajQuery.append(innerArr,'<tr><td>test1</td></tr>')
      })
    </script>
    </body>
    </html>
    
    

    (完)

    相关文章

      网友评论

          本文标题:当我调用了$().append()后,jQuery内部发生了什么

          本文链接:https://www.haomeiwen.com/subject/klyivqtx.html