jq源码 | 2020-12-22 16:20:20 170次 0次
入口
// 几个方法大致都是调用了 domManip 方法 jQuery.fn.extend({ append: function(){ return domManip( this, arguments, function( elem ) { ... } ); } ... })
buildFragment
进入 domManip 之前先看创建 fragment 的方法,比如 appen 方法追加内容时候,通过文档碎片的形式处理完,再一次性添加,避免多次的回流。
这个过程中需要经过两次处理:
第一次遍历,将 append 传入的字符串内容通过 innerHTML 赋值转为 dom 对象,因为 createDocumentFragment appendChild 方法只接受 dom 对象;同时 innerHTML 插入的一些内容表现有问题,比如一些标签不能作为div的子元素,tr,tb等。
第二次遍历,将第一步处理完的对象再追加到 fragment 中,返回给 domManip 使用。
function buildFragment( elems, context, scripts, selection, ignored ) { // 创建文档碎片 var fragment = context.createDocumentFragment() // 第一次遍历 for ( ; i < l; i++ ) { elem = elems[ i ]; ... // 先创建一个div给后面元素包裹 处理作用域问题 tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); ... // wrap 中定义了 tr td 等对应的父节点,修补 innerHTML 的一些问题 while ( --j > -1 ) { // 文档碎片中补全结构 比如追加了 <tr> 这里补全为 table-tbody-tr // 后面去这个节点时候,碎片中环境是完整的,所以取出去的目标节点不会有问题了 tmp = tmp.appendChild( context.createElement( wrap[ j ] ) ); } // 通过这种方式将传入的字符串转为 dom tmp.innerHTML = jQuery.htmlPrefilter( elem ); // 存入 node 中下一轮遍历使用,注意是 childNodes jQuery.merge( nodes, tmp.childNodes ); // 缓存 避免重复创建div tmp = tmp || ... tmp = fragment.firstChild; ... } // 文档碎片中清空内容 fragment.textContent = ""; i = 0; // 第二轮遍历 while ( ( elem = nodes[ i++ ] ) ) { ... tmp = getAll( fragment.appendChild( elem ), "script" ); ... } return fragment; }
domManip
这里拿到文档碎片之后,开始执行回调,回调中对应不同的 api 操作,并且如果插入的是一个 script 则需要手动调用并执行,但是目前看有个问题,提了 issue 之后准备 pr,结果被人捷足先登了:
<div class="test"> </div> <div class="test"> </div> // 正常来说应该执行两次,但是jq 中的处理只能执行一次,提了一个 issue 还没有回复 $('.test').append('<script type="text/javascript">alert(1)')
看下 domManip 方法的大体代码,分为三个步骤:
1. 获取上面步骤处理之后的文档碎片
2. 遍历选择器,并处理回调,同时注意此时的节点需要 clone 处理
3. 遍历过程中如果是 append script 标签,则收集,回调执行完毕动态执行插入的 script 内容
function domManip( collection, args, callback, ignored ) { ... if ( l ) { // 第一步 获取文档碎片 fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); ... if ( first || ignored ) { scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); ... // 第二步 for ( ; i < l; i++ ) { node = fragment; if ( i !== iNoClone ) { // 多个选择器中追加内容,节点需要克隆处理下 node = jQuery.clone( node, true, true ); // 存入 scripts if ( hasScripts ) { jQuery.merge( scripts, getAll( node, "script" ) ); } } // 执行对应的 api 回调 callback.call( collection[ i ], node, i ); } // 有 script 标签插入 手动执行 // 这里我认为多个dom同时插入script 时应该被执行多次,但是这里的判断很明显只执行一次 // 如果需要执行多次 需要重新获取 scripts 长度 if ( hasScripts ) { ... // Evaluate executable scripts on first document insertion for ( i = 0; i < hasScripts; i++ ) { node = scripts[ i ]; if ( rscriptType.test( node.type || "" ) && ... if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { ... } else { DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); } } } } } } return collection; }
另外有一个 clone 方法,jq 处理后可以拷贝监听事件和非内联的事件也可以一起拷贝。
看下回调函数,就是入口那里传入的不同 api 的处理:
jQuery.fn.extend({ ... append: function() { ... target.appendChild( elem ); ... }, prepend: function() { ... target.insertBefore( elem, target.firstChild ); ... }, before: function() { ... this.parentNode.insertBefore( elem, this ); ... }, after: function() { ... this.parentNode.insertBefore( elem, this.nextSibling ); ... }, replaceWith: function() { ... // 替换的时候 一些事件也会执行移除 避免内存泄漏 // removeEventListener jQuery.cleanData( getAll( this ) ); if ( parent ) { parent.replaceChild( elem, this ); } ... } });
0人赞