• jQuery 3.0 的 setter/getter 模式


    jQuery 的 setter/getter 共用一个函数,通过是否传参来表明它是何种意义。简单说传参它是 setter,不传它是 getter。

    一个函数具有多种意义在编程语言中并不罕见,比如函数重载:一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载的好处是减少了函数名的数量,避免了名字空间的污染,对于程序的可读性也大有裨益。

    函数重载主要体现的两个方面,一是参数的类型、相同个数的参数类型不同可称为函数重载;二是参数的个数,个数不同也称为函数重载。注意,重载与函数的返回值并无关系。

    由于 JS 弱类型的特征,想模拟函数重载就只能通过第二种方式:参数的个数来实现。因此函数内的 arguments 对象就显得非常重要。

    以下是一个示例

    function doAdd() {
    	var argsLength = arguments.length
    	if (argsLength === 0) {
    		return 0
    	} else if (argsLength === 1) {
    		return arguments[0] + 10
    	} else if (argsLength === 2) {
    		return arguments[0] + arguments[1]
    	}
    }
    
    doAdd()  // 0
    doAdd(5) // 15
    doAdd(5, 20) // 25

    doAdd 通过判断函数的参数个数重载实现了三种意义,argsLength 为 0 时,直接返回 0; argsLength 为 1 时,该参数与 10 相加;argsLength 为 2 时两个参数相加。


    利用函数重载特性可以实现 setter/getter

    function text() {
    	var elem = this.elem
    	var argsLength = arguments.length
    
    	if (argsLength === 0) {
    		return elem.innerText
    	} else if (argsLength === 1) {
    		elem.innerText = arguments[0]
    	}
    }
    

    以上简单的解释了函数重载及利用它实现 setter/getter。即"取值器"与"赋值器"合一。到底是取值还是赋值,由函数的参数决定。jQuery 的很多 API 设计大量使用了这种模式。

    下图汇总了 jQuery 中采用这种模式的所有 API,共 14 个函数

    所有这些函数内部都依赖另一个函数 access, 毫不夸张的说 access 是所有这些函数的核心,是实现 setter/getter 的核心。下面是这个函数的源码,它是一个私有的函数,外部是调用不到它的。

    access 的源码如下

    // Multifunctional method to get and set values of a collection
    // The value/s can optionally be executed if it's a function
    var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
    	var i = 0,
    		len = elems.length,
    		bulk = key == null;
    
    	// Sets many values
    	if ( jQuery.type( key ) === "object" ) {
    		chainable = true;
    		for ( i in key ) {
    			access( elems, fn, i, key[ i ], true, emptyGet, raw );
    		}
    
    	// Sets one value
    	} else if ( value !== undefined ) {
    		chainable = true;
    
    		if ( !jQuery.isFunction( value ) ) {
    			raw = true;
    		}
    
    		if ( bulk ) {
    			// Bulk operations run against the entire set
    			if ( raw ) {
    				fn.call( elems, value );
    				fn = null;
    			// ...except when executing function values
    			} else {
    				bulk = fn;
    				fn = function( elem, key, value ) {
    					return bulk.call( jQuery( elem ), value );
    				};
    			}
    		}
    
    		if ( fn ) {
    			for ( ; i < len; i++ ) {
    				fn(
    					elems[ i ], key, raw ?
    					value :
    					value.call( elems[ i ], i, fn( elems[ i ], key ) )
    				);
    			}
    		}
    	}
    
    	return chainable ?
    		elems :
    		// Gets
    		bulk ?
    			fn.call( elems ) :
    			len ? fn( elems[ 0 ], key ) : emptyGet;
    };
    

      

    该函数的注释提到:这是一个多功能的函数,用来获取和设置一个集合元素的属性和值。value 可以是一个可执行的函数。这个函数一共不到 60 行代码。从上往下读,第一个 if 是设置多个 value 值,是一个递归调用。刨去这个递归调用,设置单个值的代码也就不到 50 行了。写的非常简练、耐读。

    为了理解 access 函数,我画了两个图

    access 内部两个主要分支

    access 内部的执行流程

    access 定义的形参有 7 个

    1. elems 元素集合,实际调用时传的都是 this,这里的 this 是 jQuery 对象,我们知道 jQuery 对象本身是一个集合,具有 length 属性和索引。必传。
    2. fn 实现 setter/getter 的函数,就是说这个函数里需要有条件能判断哪部分是 setter,哪部分是 getter。必传。
    3. key 比如 attr 和 prop 方法要传,设置或获取哪个 key 的值。有的则不用传,但为了占位用以 null 替代,比如 text、html 方法。可选。
    4. value 仅当 setter 时要传,即 value 为 undefined 时是 getter,否则是 setter。可选。
    5. chainable 当为 true 时,进入 setter 模式,会返回 jQuery 对象。false 则进入 getter模式。调用时通过 arguments.length 或 arguments.length>1 传入。
    6. emptyGet 当 jQuery 对象为空时,返回的结果,默认不传为 undefined,data 方法调用时传的是 null。
    7. raw 当 value 为函数类型时 raw 为 false,否则为 true。

    上面提到了 access 是 jQuery 所有 setter/getter 函数的核心,换句话说所有 14 个函数 setter/getter 函数内部都会调用 access。这也是为什么 access 有 7 个参数,里面分支众多。因为它要处理的各种条件就很多呢。但所有这些 setter/getter 有很多类同的代码,最后还是提取一个公共函数。

    为了便于理解,我把 access 的调用分类以下,便于我们理解。

    1. 调用 access 时,第三个参数 key 传值为 null,分别是 text/html 方法

    text: function( value ) {
    	return access( this, function( value ) {
    		return value === undefined ?
    			jQuery.text( this ) :
    			this.empty().each( function() {
    				if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
    					this.textContent = value;
    				}
    			} );
    	}, null, value, arguments.length );
    },
    
    
    html: function( value ) {
    	return access( this, function( value ) {
    		var elem = this[ 0 ] || {},
    			i = 0,
    			l = this.length;
    
    		if ( value === undefined && elem.nodeType === 1 ) {
    			return elem.innerHTML;
    		}
    
    		// See if we can take a shortcut and just use innerHTML
    		if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
    			!wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
    
    			value = jQuery.htmlPrefilter( value );
    
    			try {
    				for ( ; i < l; i++ ) {
    					elem = this[ i ] || {};
    
    					// Remove element nodes and prevent memory leaks
    					if ( elem.nodeType === 1 ) {
    						jQuery.cleanData( getAll( elem, false ) );
    						elem.innerHTML = value;
    					}
    				}
    
    				elem = 0;
    
    			// If using innerHTML throws an exception, use the fallback method
    			} catch ( e ) {}
    		}
    
    		if ( elem ) {
    			this.empty().append( value );
    		}
    	}, null, value, arguments.length );
    },
    

    图示这两个方法在 access 内部执行处

    为什么 key 传 null,因为 DOM API 已经提供了。text 方法使用 el.innerText 设置或获取;html 方法使用 innerHTML 设置或获取(这里简单说,实际还有一些异常处理)。

    2. 与第一种情况相反,调用 access 时 key 值传了且不为 null。除了 text/html 外的其它 setter 都是如此

    attr: function( name, value ) {
    	return access( this, jQuery.attr, name, value, arguments.length > 1 );
    },
    
    prop: function( name, value ) {
    	return access( this, jQuery.prop, name, value, arguments.length > 1 );
    },
    
    
    
    // Create scrollLeft and scrollTop methods
    jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
    	var top = "pageYOffset" === prop;
    
    	jQuery.fn[ method ] = function( val ) {
    		return access( this, function( elem, method, val ) {
    			var win = getWindow( elem );
    
    			if ( val === undefined ) {
    				return win ? win[ prop ] : elem[ method ];
    			}
    
    			if ( win ) {
    				win.scrollTo(
    					!top ? val : win.pageXOffset,
    					top ? val : win.pageYOffset
    				);
    
    			} else {
    				elem[ method ] = val;
    			}
    		}, method, val, arguments.length );
    	};
    } );
    
    
    css: function( name, value ) {
    	return access( this, function( elem, name, value ) {
    		var styles, len,
    			map = {},
    			i = 0;
    
    		if ( jQuery.isArray( name ) ) {
    			styles = getStyles( elem );
    			len = name.length;
    
    			for ( ; i < len; i++ ) {
    				map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
    			}
    
    			return map;
    		}
    
    		return value !== undefined ?
    			jQuery.style( elem, name, value ) :
    			jQuery.css( elem, name );
    	}, name, value, arguments.length > 1 );
    }
    
    
    // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
    jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
    	jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name },
    		function( defaultExtra, funcName ) {
    
    		// Margin is only for outerHeight, outerWidth
    		jQuery.fn[ funcName ] = function( margin, value ) {
    			var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
    				extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
    
    			return access( this, function( elem, type, value ) {
    				var doc;
    
    				if ( jQuery.isWindow( elem ) ) {
    
    					// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
    					return funcName.indexOf( "outer" ) === 0 ?
    						elem[ "inner" + name ] :
    						elem.document.documentElement[ "client" + name ];
    				}
    
    				// Get document width or height
    				if ( elem.nodeType === 9 ) {
    					doc = elem.documentElement;
    
    					// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
    					// whichever is greatest
    					return Math.max(
    						elem.body[ "scroll" + name ], doc[ "scroll" + name ],
    						elem.body[ "offset" + name ], doc[ "offset" + name ],
    						doc[ "client" + name ]
    					);
    				}
    
    				return value === undefined ?
    
    					// Get width or height on the element, requesting but not forcing parseFloat
    					jQuery.css( elem, type, extra ) :
    
    					// Set width or height on the element
    					jQuery.style( elem, type, value, extra );
    			}, type, chainable ? margin : undefined, chainable );
    		};
    	} );
    } );
    
    
    data: function( key, value ) {
    	var i, name, data,
    		elem = this[ 0 ],
    		attrs = elem && elem.attributes;
    
    	// Gets all values
    	if ( key === undefined ) {
    		if ( this.length ) {
    			data = dataUser.get( elem );
    
    			if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) {
    				i = attrs.length;
    				while ( i-- ) {
    
    					// Support: IE 11 only
    					// The attrs elements can be null (#14894)
    					if ( attrs[ i ] ) {
    						name = attrs[ i ].name;
    						if ( name.indexOf( "data-" ) === 0 ) {
    							name = jQuery.camelCase( name.slice( 5 ) );
    							dataAttr( elem, name, data[ name ] );
    						}
    					}
    				}
    				dataPriv.set( elem, "hasDataAttrs", true );
    			}
    		}
    
    		return data;
    	}
    
    	// Sets multiple values
    	if ( typeof key === "object" ) {
    		return this.each( function() {
    			dataUser.set( this, key );
    		} );
    	}
    
    	return access( this, function( value ) {
    		var data;
    
    		// The calling jQuery object (element matches) is not empty
    		// (and therefore has an element appears at this[ 0 ]) and the
    		// `value` parameter was not undefined. An empty jQuery object
    		// will result in `undefined` for elem = this[ 0 ] which will
    		// throw an exception if an attempt to read a data cache is made.
    		if ( elem && value === undefined ) {
    
    			// Attempt to get data from the cache
    			// The key will always be camelCased in Data
    			data = dataUser.get( elem, key );
    			if ( data !== undefined ) {
    				return data;
    			}
    
    			// Attempt to "discover" the data in
    			// HTML5 custom data-* attrs
    			data = dataAttr( elem, key );
    			if ( data !== undefined ) {
    				return data;
    			}
    
    			// We tried really hard, but the data doesn't exist.
    			return;
    		}
    
    		// Set the data...
    		this.each( function() {
    
    			// We always store the camelCased key
    			dataUser.set( this, key, value );
    		} );
    	}, null, value, arguments.length > 1, null, true );
    },
    

    图示这些方法在 access 内部执行处

    各个版本的实现差异

    1.1 ~ 1.3 各个 setter/getter 独自实现,没有抽取一个公共函数。
    1.4 ~ 1.9 抽取了独立的 jQuery.access 这个核心函数为所有的 setter/getter 服务。
    1.10 ~ 2.24 同上一个版本区间,但在内部使用了一个私有的 access 函数,不使用公开的 jQuery.access,即弱化了 jQuery.access。
    3.0 ~ 未来 去掉了 jQuery.access ,内部直接使用私有的 access 。

  • 相关阅读:
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    线性表——数组实现
    this指针与const成员函数
    类对象拷贝是不是赋值操作??
    你真的理解内联函数吗?
    名字查找先于类型检查:函数重载与作用域
    谈谈函数调用
    推荐形参使用常量引用:void func(const T &);
  • 原文地址:https://www.cnblogs.com/snandy/p/5644994.html
Copyright © 2020-2023  润新知