美文网首页jQuery源码笔记.jpg
jQuery源码#1 整体架构

jQuery源码#1 整体架构

作者: 柠檬果然酸 | 来源:发表于2020-04-15 22:36 被阅读0次

    本节内容
    1.jQuery的无new构建
    2.插件接口
    3.默认回调对象设计
    4.ownerDocument 和 documentElement 的区别
    5.jQuery.parseHTML()
    6.buildFragment()
    7.jQuery.fn.init(selector, context, root)

    jQuery的无new构建

    jQuery 通过 $() 返回实例

    var jQuery = function(selector, context) {
      return new jQuery();
    }
    jQuery.prototype = {
      name: function() {},
      age: function() {}
    }
    

    但是这样有个问题,代码陷入死循环了!所以改改

    var jQuery = function(selector, context) {
           return  jQuery.prototype.init();
    }
    jQuery.prototype = {
        init: function() {
            return this; // 这里的 this 指向原型对象
        },
        name: function() {},
        age: function() {}
    }
    

    js 中的 this 指向问题:
    1.在一般函数方法中使用 this 指代全局对象

    function test() {
      this.x = 1;  // 这里 this 指向 window
    }
    

    2.作为对象方法调用,this 指代上级对象

    var obj = {
      x: 1,
      m: function test() {
        alert(this.x); // 这里 this 指向 obj
      }
    };
    obj.m(); // 1
    

    3.作为构造函数调用,this 指代 new 出的对象

    function test() {
      this.x = 1;
      console.log(this.x);
    }
    var obj = new test();
    

    还有问题没解决,就是不论 $() 调用多少次,构建的都是同一个实例,这样是不行的,所以将代码改成这样

    var jQuery = function(selector, context) {
           return new jQuery.prototype.init();
    }
    jQuery.prototype = {
        init: function() {
            return this; // 这里的 this 指向 init 对象
        },
        name: function() {},
        age: function() {}
    }
    

    构建不同的实例这个问题解决了,然而新的问题又来了,创建出来的实例对象中没有 name()age(),那只能再改改代码,添加上一行代码

    jQuery.fn.init.prototype = jQuery.fn;
    

    插件接口

    jQuery.extend = jQuery.fn.extend = function() {
    

    通过这个接口能够
    1.扩展某个对象的属性或方法
    2.还能扩展 jQuery 自身的属性方法
    3.还支持深度拷贝

    由于 javascript 不支持方法的重载,所以只能在一个方法里面实现重载,就是通过在判断参数列表的不同来执行不同的逻辑

    jQuery.extend = jQuery.fn.extend = function() {
        var src, copyIsArray, copy, name, options, clone,
            target = arguments[0] || {},    // 被扩展的对象
            i = 1,                          // 第一个扩展对象的下标
            length = arguments.length,      // 参数列表的长度
            deep = false;                   // 深拷贝
    
        // 第一种重载
        // 如果第一个参数类型为 boolean,那就表示要设置是否深拷贝
        if ( typeof target === "boolean" ) {
            deep = target;
            target = arguments[1] || {};
            i = 2;
        }
    
        // 处理奇怪的情况
        if ( typeof target !== "object" ) {
            target = {};
        }
    
        // 第二种重载
        // 当传入参数只有一个的时候,就扩展 jQuery 自身
        if ( length === i ) {
            target = this; // jQuery.extend时,this指的是jQuery;jQuery.fn.extend时,this指的是jQuery.fn
            --i;
        }
    
        for ( ; i < length; i++ ) {
            if ( (options = arguments[ i ]) != null ) {
                for ( name in options ) {
                    copy = options[ name ];
    
                    // 防止自引用
                    if ( target === copy ) {
                        continue;
                    }
    
                    // 如果是深拷贝,且被拷贝的属性值本身是个对象或数组
                    if ( deep && copy && ( Object.prototype.toString.call(copy) === '[Object Object]' ||
                        ( copyIsArray = Array.isArray(copy) ) ) ) {
                        src = target[ name ];
                        
                        if ( copyIsArray && !Array.isArray( src ) ) {
                            clone = [];
                        } else if ( !copyIsArray && ( Object.prototype.toString.call(copy) !== '[Object Object]' ) ) {
                            clone = {};
                        } else {
                            clone = src;
                        }
    
                        copyIsArray = false;
                        target[ name ] = jQuery.extend( deep, clone, copy );  // 递归~
                    } else if ( copy !== undefined ) { // 浅拷贝,且属性值不为undefined
                        target[ name ] = copy;
                    }
                }
            }
        }
    
        // 返回扩展对象
        return target;
    }
    

    默认回调对象设计

    function Callbacks() {
      var list = [];
      var self;
      self = {
        add: function(fn) {
          list.push(fn)
        },
        fire: function(args) {
          list.forEach(function(fn) {
            fn(args);
          })
        }
      }
      return self;
    }
    
    function fn1(val) {
      console.info('fn1 says:' + val);
    }
            
    function fn2(val) {
      console.info('fn2 says:' + val);
    }
            
    var cbs = Callbacks();
    cbs.add(fn1);
    cbs.fire('foo');
    cbs.add(fn2);
    cbs.fire('bar');
    

    这里我有个迷惑的点就是 Callbacks() 函数中的 list 去哪了,打开控制台可以看到 cbs 内部并没 list

    用排除法分析:
    1.listCallbacks() 函数的局部变量,如果不用 var 声明就是全局变量,也就是说 list 只能存在于 Callbacks() 的内部。
    2.创造对象的两种方法,一种是使用 new 关键字创建,另一种是调用函数然后函数返回一个对象。
    3.很显然 Callbacks() 采用的是第二种方法创建对象,返回的是 self 这个对象,self 对象相当于一个闭包,在这个闭包内能够访问到 list 对象。
    4.这相当于将变量私有化的一种手段,无论在哪里都没法访问到 list,只能通过 self 内部的 add()fire() 两个方法才能访问到 list

    总结
    list 变量到底在哪里我还是不得而知,唯一肯定的是它是 Callbacks() 函数的局部变量。

    ownerDocument 和 documentElement 的区别

    通过google浏览器可以看到任何DOM对象都有 ownerDocument 属性,而 documentElement 是 document 对象独有的属性。



    documentElement 返回的是 html 对象
    ownerDocument 返回的是文档根节点也就是 document 对象

    jQuery.parseHTML()

    作用:将一串 html 字符转换为 DOM 对象

    // @data  html字符串
    // @context  HTML文档
    // @keepScripts  不知道
    jQuery.parseHTML = function( data, context, keepScripts ) {
        if ( typeof data !== "string" ) {
            return [];
        }
        if ( typeof context === "boolean" ) {
            keepScripts = context;
            context = false;
        }
    
        var base, parsed, scripts;
    
        // 前面的代码直接忽略
        // 如果没有 HTML 文档就创建一个
        if ( !context ) {
    
            if ( support.createHTMLDocument ) {
                // 创建 HTML 文档的核心代码
                // 对这个方法有什么疑问参见 https://developer.mozilla.org/zh-CN/docs/Web/API/DOMImplementation/createHTMLDocumen
                context = document.implementation.createHTMLDocument( "" );
    
                base = context.createElement( "base" );
                base.href = document.location.href;
                context.head.appendChild( base );
            } else {
                context = document;
            }
        }
    
        // 这个正则表达式放着儿不知道什么意思,应该是处理某种匪夷所思的 bug 吧
        // rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i );
        // 这个正则表达式是用来匹配像 <span></span>、<img /> 这种单个标签的
        parsed = rsingleTag.exec( data );
        scripts = !keepScripts && [];
    
        if ( parsed ) {
            return [ context.createElement( parsed[ 1 ] ) ];
        }
    
        // 将字符转换为 DOM 的操作在这儿
        // 这里返回的是 DocumentFragment 节点,它的 childNodes 就是转换好的 html 字符
        parsed = buildFragment( [ data ], context, scripts );
    
        if ( scripts && scripts.length ) {
            jQuery( scripts ).remove();
        }
    
        return jQuery.merge( [], parsed.childNodes );
    };
    

    buildFragment()

    // @elems  [ html 字符串, html 字符串, html 字符串, ... ]
    // @context  HTML文档
    // 剩余的参数就不知道什么意思了
    function buildFragment( elems, context, scripts, selection, ignored ) {
        var elem, tmp, tag, wrap, attached, j,
            // 这里用到了 DocumentFragment 的特性
            // DocumentFragment 节点不属于文档树
            // DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点
            fragment = context.createDocumentFragment(),
            nodes = [],
            i = 0,
            l = elems.length;
    
        for ( ; i < l; i++ ) {
            elem = elems[ i ];
    
            if ( elem || elem === 0 ) {
    
                if ( toType( elem ) === "object" ) {
                    jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
                } else if ( !rhtml.test( elem ) ) {
                    nodes.push( context.createTextNode( elem ) );
                } else { // 上面的两个 if 分支都不用看了,因为程序压根儿都没走过那儿
                    // 待会儿所有由字符转换的 DOM 对象会挂在 tmp 下作为它的 childNodes
                    tmp = tmp || fragment.appendChild( context.createElement( "div" ) );
    
                    // 下面3行代码不知道什么意思,应该是处理某种匪夷所思的 bug 吧
                    // rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i );
                    // 这个正则表达式其实就是用来匹配 TagName 的
                    // 其实我觉得 tmp.innerHTML = elem 就足够了
                    // 写那么复杂可能是处理 elems 中有多个 html 字符串的情况吧
                    tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
                    wrap = wrapMap[ tag ] || wrapMap._default;
                    tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
    
                    j = wrap[ 0 ];
                    while ( j-- ) {
                        tmp = tmp.lastChild;
                    }
    
                    // 此时 html 字符串已经转换为 tmp 的子节点
                    // 这一步是将 tmp 的子节点全部放到 nodes 数组中
                    jQuery.merge( nodes, tmp.childNodes );
    
                    tmp = fragment.firstChild;
    
                    tmp.textContent = "";
                }
            }
        }
    
        fragment.textContent = "";
    
        i = 0;
        while ( ( elem = nodes[ i++ ] ) ) {
    
            // 直接忽略
            if ( selection && jQuery.inArray( elem, selection ) > -1 ) {
                if ( ignored ) {
                    ignored.push( elem );
                }
                continue;
            }
    
            attached = isAttached( elem );
    
            // 这一步又将所有转换的 DOM 对象挂在 DocumentFragment 片段下作为子节点
            tmp = getAll( fragment.appendChild( elem ), "script" );
    
            // 忽略
            if ( attached ) {
                setGlobalEval( tmp );
            }
    
            // 忽略
            if ( scripts ) {
                j = 0;
                while ( ( elem = tmp[ j++ ] ) ) {
                    if ( rscriptType.test( elem.type || "" ) ) {
                        scripts.push( elem );
                    }
                }
            }
        }
    
        return fragment;
    }
    

    jQuery.fn.init(selector, context, root)

    这个方法处理的东西比较多,有些代码看不懂,不过影响不大,只要能理解整体思路就行了

    var rootjQuery,
    
        rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
    
        init = jQuery.fn.init = function( selector, context, root ) {
            var match, elem;
    
            // 处理非法参数
            if ( !selector ) {
                return this;
            }
    
            root = root || rootjQuery;
    
            // 传入的是字符串
            if ( typeof selector === "string" ) {
                if ( selector[ 0 ] === "<" &&
                    selector[ selector.length - 1 ] === ">" &&
                    selector.length >= 3 ) { // 匹配 '<' 开始 '>' 结尾的字符,如:<span></span>
    
                    match = [ null, selector, null ];
    
                } else {
                    match = rquickExpr.exec( selector );
                }
    
                if ( match && ( match[ 1 ] || !context ) ) {
    
                    // 传入的是html字符串
                    if ( match[ 1 ] ) {
                        context = context instanceof jQuery ? context[ 0 ] : context;
    
                        // 将html字符转换成DOM对象,并把它挂在jQuery对象上
                        jQuery.merge( this, jQuery.parseHTML(
                            match[ 1 ],
                            context && context.nodeType ? context.ownerDocument || context : document,
                            true
                        ) );
    
                        // HANDLE: $(html, props)
                        if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) {
                            for ( match in context ) {
    
                                // Properties of context are called as methods if possible
                                if ( isFunction( this[ match ] ) ) {
                                    this[ match ]( context[ match ] );
    
                                // ...and otherwise set as attributes
                                } else {
                                    this.attr( match, context[ match ] );
                                }
                            }
                        }
    
                        return this;
    
                    // 传入的是#id
                    } else {
                        // 直接用document.getElementById拿到DOM对象
                        elem = document.getElementById( match[ 2 ] );
    
                        if ( elem ) {
                            this[ 0 ] = elem;
                            this.length = 1;
                        }
                        return this;
                    }
    
                // HANDLE: $(expr, $(...))
                } else if ( !context || context.jquery ) {
                    return ( context || root ).find( selector );
    
                // HANDLE: $(expr, context)
                // (which is just equivalent to: $(context).find(expr)
                } else {
                    return this.constructor( context ).find( selector );
                }
    
            // 传入的是DOM对象
            } else if ( selector.nodeType ) {
                this[ 0 ] = selector;
                this.length = 1;
                return this;
    
            // HANDLE: $(function)
            // Shortcut for document ready
            } else if ( isFunction( selector ) ) {
                return root.ready !== undefined ?
                    root.ready( selector ) :
    
                    selector( jQuery );
            }
    
            return jQuery.makeArray( selector, this );
        };
    

    其他的else if不知道是处理哪种情况的,不过可以看出init这个接口可以处理不同种类的参数,可以向其中传入 html字符、id、className、DOM对象、jQuery对象等等。

    相关文章

      网友评论

        本文标题:jQuery源码#1 整体架构

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