jQuery1.11源码分析(3)-----Sizzle源码中的浏览器兼容性检测和处理

上一章讲了正则表达式,这一章继续我们的前菜,浏览器兼容性处理。

先介绍一个简单的沙盒测试函数。

/**
 * Support testing using an element
 * @param {Function} fn Passed the created div and expects a boolean result
 */
//特性检测的一个技巧,造一个div,最后将其删掉,提供一种简单的沙盒(如果该div不加入到DOM树上,则部分属性如currentStyle会没有)
function assert( fn ) {
    var div = document.createElement("div");

    try {
        return !!fn( div );
    } catch (e) {
        return false;
    } finally {
        // Remove from its parent by default
        if ( div.parentNode ) {
            //为了不造成影响,最后如果该div还在DOM树上,就将其移除掉
            div.parentNode.removeChild( div );
        }
        // release memory in IE
        // IE下对于不在DOM树里的DOM节点必须手动置为null,关于IE内存泄露的文章很多,不再赘述
        div = null;
    }
}

嗯,说明如注释,接下来我们先处理push函数,有些浏览器Nodelist是不能使用push的,我们先检测一下在当前浏览器下push能不能支持Nodelist,不能的话则把push换成自己的方法,使push能够支持Nodelist

// Optimize for push.apply( _, NodeList )
//居然还可以对push进行优化,看看push支不支持Nodelist
//因为Nodelist不是数组,所以在某些浏览器下没有Push方法,需要人工造一个支持nodelist的push方法。
try {
    push.apply(
        (arr = slice.call( preferredDoc.childNodes )),
        preferredDoc.childNodes
    );
    // Support: Android < 4.0
    // Detect silently failing push.apply
    //防止安卓4.0以下版本静默失败(失败了但不报错)。
    arr[ preferredDoc.childNodes.length ].nodeType;
} catch ( e ) {
    //如果arr有值,即arr.length > 0,说明上述push方法是有效的,使用原生API,否则换成自己的方法
    push = { apply: arr.length ?
        // Leverage slice if possible
        function( target, els ) {
            //这里一定要用apply,否则会把整个els给Push进去而不是拆成一个个push
            push_native.apply( target, slice.call(els) );
        } :

        // Support: IE<9
        // Otherwise append directly
        //这里不明白为什么。。
        //因为在IE下对Nodelist执行slice会报错找不到JScript对象,所以arr.length为0
        //http://www.jb51.net/article/24182.htm
        function( target, els ) {
            var j = target.length,
                i = 0;
            // Can‘t trust NodeList.length
            //为什么IE8下不能相信长度?
            while ( (target[j++] = els[i++]) ) {}
            target.length = j - 1;
        }
    };
}

说明如注释,然后再来三发对于html元素向DOM元素转换时,部分属性会遇到的bug以及处理

// Support: IE<8
// Prevent attribute/property "interpolation"
// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
//在IE<8下会自动串改部分属性,比如下面的href会改成当前地址+#,大家可以试试。
//出现这种情况后,当我们要取某些属性时,交给代理函数进行处理,比如Expr.attrHandle
if ( !assert(function( div ) {
    div.innerHTML = "<a href=‘#‘></a>";
    return div.firstChild.getAttribute("href") === "#" ;
}) ) {
    addHandle( "type|href|height|width", function( elem, name, isXML ) {
        if ( !isXML ) {
            //getAttribute我搜了一下。。没有第二个参数,还没翻标准。。暂且认为是错的。
            return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
        }
    });
}

// Support: IE<9
// Use defaultValue in place of getAttribute("value")
if ( !support.attributes || !assert(function( div ) {
    div.innerHTML = "<input/>";
    div.firstChild.setAttribute( "value", "" );
    return div.firstChild.getAttribute( "value" ) === "";
}) ) {
    addHandle( "value", function( elem, name, isXML ) {
        if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
            return elem.defaultValue;
        }
    });
}

// Support: IE<9
// Use getAttributeNode to fetch booleans when getAttribute lies
//使用getAttribute获取那些非attrName=attrValue形式的attr属性时,会出错,所以换成使用getAttributeNode,然后判断获得的Attr节点的specified属性,看看是否指定。
if ( !assert(function( div ) {
    return div.getAttribute("disabled") == null;
}) ) {
    addHandle( booleans, function( elem, name, isXML ) {
        var val;
        if ( !isXML ) {
            return elem[ name ] === true ? name.toLowerCase() :
                    (val = elem.getAttributeNode( name )) && val.specified ?
                    val.value :
                null;
        }
    });
}

说明如注释,接下来我们先声明一个对象,这个对象类似于配置文件(config)的作用,嗯。。配置文件通常很长

Expr = Sizzle.selectors = {

    // Can be adjusted by the user
    //缓存的长度
    cacheLength: 50,
    //用来标识奇葩函数
    createPseudo: markFunction,
    //存放用来匹配的表达式
    match: matchExpr,
    //前面说过的,存放提取部分特殊属性的handle
    attrHandle: {},
    //查找过程所用的函数就存在这,后面会进行声明
    find: {},
    //前面说个token中有一种类型是表示两个元素的关系,这个关系处理函数用relative保存起来,first用来表示是否是查找到的第一个元素。
    //例如假设tokens[i]是{type:‘>‘,value:‘>‘},调用body[Expr.relative[tokens[i].type][dir]],
    relative: {
        ">": { dir: "parentNode", first: true },
        " ": { dir: "parentNode" },
        "+": { dir: "previousSibling", first: true },
        "~": { dir: "previousSibling" }
    },

在Expr里还有三个部分:preFilter(用来对捕获组进行预处理),filter(返回一个个matcher,最后将多个matcher编译成一个),pseudo(其实就是filter的一种),后续可能留出篇幅来进行讲解。

接下来我们休息一下,再一口气啃掉一个超长的初始化函数。这个初始化的主要作用是从健壮性的考虑出发,设置一下文档节点,检查一下是不是HTML,检查一下各个原生API是不是好用,检查一下querySelectorAll是不是好用,初始化查找过程用的函数(比如Expr.find[‘ID‘]);

/**
 * Sets document-related variables once based on the current document
 * @param {Element|Object} [doc] An element or document object to use to set the document
 * @returns {Object} Returns the current document
 */
//基于当前文档节点设置一些文档相关的内容,如支持性什么的。
setDocument = Sizzle.setDocument = function( node ) {
    console.log(‘setDocument in‘);
    var hasCompare,
        doc = node ? node.ownerDocument || node : preferredDoc,         //获得文档节点的一种方式,这样写的好处是保持健壮性
        parent = doc.defaultView;

    // If no document and documentElement is available, return
    //nodeType不等于9说明不是文档节点
    //做这么多判断。。嗯。。还是为了健壮性
    if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
        return document;
    }

    // Set our document
    document = doc;
    docElem = doc.documentElement;

    // Support tests
    documentIsHTML = !isXML( doc );

    // Support: IE>8
    // If iframe document is assigned to "document" variable and if iframe has been reloaded,
    // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
    // IE6-8 do not support the defaultView property so parent will be undefined
    //这里是一个jQuery的bug修复
    if ( parent && parent !== parent.top ) {
        // IE11 does not have attachEvent, so all must suffer
        if ( parent.addEventListener ) {
            parent.addEventListener( "unload", function() {
                setDocument();
            }, false );
        } else if ( parent.attachEvent ) {
            parent.attachEvent( "onunload", function() {
                setDocument();
            });
        }
    }
    /* Attributes
    ---------------------------------------------------------------------- */

    // Support: IE<8
    // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
    //如果这里检测通过的话,后面获取属性值用的就是getAttribute这个API,否则用的就是getAttributeNode
    support.attributes = assert(function( div ) {
        div.className = "i";
        return !div.getAttribute("className");
    });

    /* getElement(s)By*
    ---------------------------------------------------------------------- */

    // Check if getElementsByTagName("*") returns only elements
    //检查getElementsByTagName是否会包含注释节点,最好不要包含。
    support.getElementsByTagName = assert(function( div ) {
        div.appendChild( doc.createComment("") );
        return !div.getElementsByTagName("*").length;
    });

    // Check if getElementsByClassName can be trusted
    //?????细节
    support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) {
        div.innerHTML = "<div class=‘a‘></div><div class=‘a i‘></div>";

        // Support: Safari<4
        // Catch class over-caching
        div.firstChild.className = "i";
        // Support: Opera<10
        // Catch gEBCN failure to find non-leading classes
        //Opera<10 的时候当一个元素有多个class的时候,获得第二个会出错。
        return div.getElementsByClassName("i").length === 2;
    });
    // Support: IE<10
    // Check if getElementById returns elements by name
    // The broken getElementById methods don‘t pick up programatically-set names,
    // so use a roundabout getElementsByName test
    //????所以迂回使用getElementsByName来检测?不明原理
    support.getById = assert(function( div ) {
        docElem.appendChild( div ).id = expando;
        return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
    });
    //Expr里的函数分为查找和过滤两种功能类型。
    //接下来分别进行这两种函数功能性的检测和支持
    // ID find and filter
    if ( support.getById ) {

        Expr.find["ID"] = function( id, context ) {
            //??????为什么要转换成字符串形式的undefined
            if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
                var m = context.getElementById( id );
                // Check parentNode to catch when Blackberry 4.6 returns
                // nodes that are no longer in the document #6963
                //!!!!在黑莓4.6的浏览器中会返回那些不在DOM树中的节点,所以通过该节点是否有父节点来判断该节点是否在DOM树中,bug修正,并且转换为数组形式。
                return m && m.parentNode ? [m] : [];
            }
        };
        //这里返回一个后面用来做matcher的函数
        Expr.filter["ID"] = function( id ) {
            //对id做编码转换,这是一种闭包类型。
            var attrId = id.replace( runescape, funescape );
            return function( elem ) {
                return elem.getAttribute("id") === attrId;
            };
        };
    } else {
        // Support: IE6/7
        // getElementById is not reliable as a find shortcut
        //???getElementById不可靠??
        //!!!!删掉以后,Sizzle就不再通过ID来获取节点,获取属性的方式也由getAttribute变为getAttributeNode
        delete Expr.find["ID"];

        Expr.filter["ID"] =  function( id ) {
            var attrId = id.replace( runescape, funescape );
            return function( elem ) {
                //每个DOM元素下有隐含的属性节点,通过查看其属性节点的方式来过滤
                var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
                return node && node.value === attrId;
            };
        };
    }

    // Tag
    Expr.find["TAG"] = support.getElementsByTagName ?
        function( tag, context ) {
            console.log(‘find tag begin‘);
            if ( typeof context.getElementsByTagName !== strundefined ) {
                return context.getElementsByTagName( tag );
            }

        } :
        //否则采用过滤掉注释节点的写法。
        function( tag, context ) {
            console.log(‘find tag begin‘);
            var elem,
                tmp = [],
                i = 0,
                results = context.getElementsByTagName( tag );

            // Filter out possible comments
            //必须过滤注释节点
            if ( tag === "*" ) {
                //赋值判断写法。
                while ( (elem = results[i++]) ) {
                    if ( elem.nodeType === 1 ) {
                        tmp.push( elem );
                    }
                }

                return tmp;
            }
            return results;
        };

    // Class
    Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
        console.log(‘find class begin‘);
        if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
            return context.getElementsByClassName( className );
        }
    };

    /* QSA/matchesSelector
    ---------------------------------------------------------------------- */

    // QSA and matchesSelector support

    // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
    rbuggyMatches = [];

    // qSa(:focus) reports false when true (Chrome 21)
    // We allow this because of a bug in IE8/9 that throws an error
    // whenever `document.activeElement` is accessed on an iframe
    // So, we allow :focus to pass through QSA all the time to avoid the IE error
    // See http://bugs.jquery.com/ticket/13378
    //用来存放有bug的QSA字符串,最后用|连接起来当作正则表达式,用来检测选择符是否有bug
    rbuggyQSA = [];

    if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
        // Build QSA regex
        // Regex strategy adopted from Diego Perini
        //这两个assert没有返回值,主要是把有bug的QSA字符串检测出来。
        assert(function( div ) {
            // Select is set to empty string on purpose
            // This is to test IE‘s treatment of not explicitly
            // setting a boolean content attribute,
            // since its presence should be enough
            // http://bugs.jquery.com/ticket/12359
            //这个bug检测很简练
            div.innerHTML = "<select>
<option selected="selected"></option>




</select>";

            // Support: IE8, Opera 10-12
            // Nothing should be selected when empty strings follow ^= or $= or *=
            //!!!空白字符串不应该跟在 ^=、$=、*=这样的字符后面,否则逻辑上是走不通的,你想想啊,^=‘‘的意思是匹配开头为空字符的字符串。。哪个字符串开头是空字符?,剩下同理
            if ( div.querySelectorAll("[t^=‘‘]").length ) {
                rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:‘‘|\"\")" );
            }

            // Support: IE8
            // Boolean attributes and "value" are not treated correctly
            //!!!IE8中的QSA不能正确识别非key=value形式的属性选择符
            if ( !div.querySelectorAll("[selected]").length ) {
                rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
            }

            // Webkit/Opera - :checked should return selected option elements
            // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
            // IE8 throws error here and will not see later tests
            //!!!!:checked伪类选择器理论应该返回被选择的option元素,但IE8下有bug。。
            if ( !div.querySelectorAll(":checked").length ) {
                rbuggyQSA.push(":checked");
            }
        });

        assert(function( div ) {
            // Support: Windows 8 Native Apps
            // The type and name attributes are restricted during .innerHTML assignment
            //????卧槽。。win8不能直接用innerHTML来创建type和name属性?
            var input = doc.createElement("input");
            input.setAttribute( "type", "hidden" );
            div.appendChild( input ).setAttribute( "name", "D" );

            // Support: IE8
            // Enforce case-sensitivity of name attribute
            //!!!!增强对name大小写的敏感性,如果大小写不敏感,则不能使用带有name=.的属性选择符
            if ( div.querySelectorAll("[name=d]").length ) {
                rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
            }

            // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
            // IE8 throws error here and will not see later tests
            //!!!!理论上hidden的元素还应该能用:enabled伪类选择符选择到的,但IE8和FF3.5下有bug
            if ( !div.querySelectorAll(":enabled").length ) {
                rbuggyQSA.push( ":enabled", ":disabled" );
            }

            // Opera 10-11 does not throw on post-comma invalid pseudos
            //?????
            div.querySelectorAll("*,:x");
            rbuggyQSA.push(",.*:");
        });
    }
    if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector ||
        docElem.mozMatchesSelector ||
        docElem.oMatchesSelector ||
        docElem.msMatchesSelector) )) ) {

        assert(function( div ) {
            // Check to see if it‘s possible to do matchesSelector
            // on a disconnected node (IE 9)
            //!!!!检测是否matchesSelector会匹配到没有连接的节点
            support.disconnectedMatch = matches.call( div, "div" );

            // This should fail with an exception
            // Gecko does not error, returns false instead
            //?????
            matches.call( div, "[s!=‘‘]:x" );
            rbuggyMatches.push( "!=", pseudos );
        });
    }

    rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
    rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );

    /* Contains
    ---------------------------------------------------------------------- */
    hasCompare = rnative.test( docElem.compareDocumentPosition );

    // Element contains another
    // Purposefully does not implement inclusive descendent
    // As in, an element does not contain itself
    //这里和懒函数的方式异曲同工,如果有原生compareDocumentPosition或contains
    contains = hasCompare || rnative.test( docElem.contains ) ?
        function( a, b ) {
            console.log(‘contains begin‘);
            var adown = a.nodeType === 9 ? a.documentElement : a,
                bup = b && b.parentNode;
            return a === bup || !!( bup && bup.nodeType === 1 && (
                adown.contains ?
                    adown.contains( bup ) :
                    //这里&上16仅仅是判断a是否包含bup,b如果是a本身,则两者应该不算包含关系
                    //http://www.2cto.com/kf/201301/181075.html
                    a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
            ));
        } :
        //否则就用土办法不断遍历DOM树
        function( a, b ) {
            if ( b ) {
                while ( (b = b.parentNode) ) {
                    if ( b === a ) {
                        return true;
                    }
                }
            }
            return false;
        };

    /* Sorting
    ---------------------------------------------------------------------- */

    // Document order sorting
    //用来传给sort函数的排序方法
    sortOrder = hasCompare ?
    function( a, b ) {
        // Flag for duplicate removal
        if ( a === b ) {
            hasDuplicate = true;
            return 0;
        }

        // Sort on method existence if only one input has compareDocumentPosition
        //!undefined === true
        //!function (){} === false
        //if(-1){console.log(1);} -->1
        //用以检查是否两个输入都有该API
        var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
        if ( compare ) {
            return compare;
        }

        // Calculate position if both inputs belong to the same document
        compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
            a.compareDocumentPosition( b ) :

            // Otherwise we know they are disconnected
            1;

        // Disconnected nodes
        if ( compare & 1 ||
            (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {

            // Choose the first element that is related to our preferred document
            if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
                return -1;
            }
            if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
                return 1;
            }

            // Maintain original order
            return sortInput ?
                //最后这里处理了给字符串调用sort的情况
                //后面有一句代码
                //support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
                ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
                0;
        }

        return compare & 4 ? -1 : 1;
    } :
    //如果没有contains这样的原生API使用
    function( a, b ) {
        // Exit early if the nodes are identical
        if ( a === b ) {
            hasDuplicate = true;
            return 0;
        }

        var cur,
            i = 0,
            aup = a.parentNode,
            bup = b.parentNode,
            ap = [ a ],
            bp = [ b ];

        // Parentless nodes are either documents or disconnected
        //只要其中有一个元素没有父元素
        if ( !aup || !bup ) {
            return a === doc ? -1 :
                b === doc ? 1 :
                aup ? -1 :
                bup ? 1 :
                sortInput ?
                ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
                0;

        // If the nodes are siblings, we can do a quick check
        //或者两个元素的父元素是兄弟,就可以立马判定
        } else if ( aup === bup ) {
            return siblingCheck( a, b );
        }

        // Otherwise we need full lists of their ancestors for comparison
        //否则遍历出所有祖先路径,然后一一对比,直到找到分歧点,再比较分歧点的序数即可
        cur = a;
        while ( (cur = cur.parentNode) ) {
            ap.unshift( cur );
        }
        cur = b;
        while ( (cur = cur.parentNode) ) {
            bp.unshift( cur );
        }

        // Walk down the tree looking for a discrepancy
        while ( ap[i] === bp[i] ) {
            i++;
        }

        return i ?
            // Do a sibling check if the nodes have a common ancestor
            siblingCheck( ap[i], bp[i] ) :

            // Otherwise nodes in our document sort first
            ap[i] === preferredDoc ? -1 :
            bp[i] === preferredDoc ? 1 :
            0;
    };
    //最后返回文档节点
    return doc;
};

上面这个初始化函数实在太长。。再调用一下就好。

// Initialize against the default document
setDocument();

jQuery1.11源码分析(3)-----Sizzle源码中的浏览器兼容性检测和处理[原创],古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。