【jq 源码解析3-Dom操作】

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人赞

分享到: