evantianx / Bloooooooog

Place to record what I thought and learned
0 stars 0 forks source link

jQuery1.7.1源码分析 #15

Open evantianx opened 7 years ago

evantianx commented 7 years ago

总体结构

(function ( window, undefined ) {
  //构造JQ对象
  var jQuery = (function() {
    var jQuery = function( selector, context ) {
      return new jQuery.fn.init( selector, context, rootjQuery );
    }, 
    //其他局部变量声明
    jQuery.fn = jQuery.prototype = {
      constructor: jQuery,
      init: function( selector, context, rootjQuery ) { ... },
      //其他原型属性和方法
    };
    jQuery.fn.init.prototype = jQuery.fn;
    jQuery.extend = jQuery.fn.extend = function() { ... };
    jQuery.extend({
      //静态属性和方法
    });
    return jQuery
  })();
  //工具方法 Utilities
  //回调函数列表 Callbacks Object
  //异步队列 Deferred Object
  //浏览器功能测试 Support
  //数据缓存 Data
  //队列 Queue
  //属性操作 Attributes
  //事件系统 Events
  //选择器 Sizzle
  //DOM遍历 Traversing
  //DOM操作 Manipulation
  // 样式操作 CSS(计算样式,内联样式)
  //异步请求 Ajax
  //动画 Effects
  //坐标 Offset,尺寸Dimensions
  window.jQuery = window.$ = jQuery
})(window)

有几个点需要注意一下:

evantianx commented 7 years ago

构造jQuery对象(一)

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

evantianx commented 7 years ago

构造jQuery对象(二)

jQuery.buildFragment( args, nodes, scripts )

evantianx commented 7 years ago

构造jQuery对象(三)

jQuery.clean( elems, context, fragment, scripts )

evantianx commented 7 years ago

构造jQuery对象(四)

jQuery.extend()/jQuery.fn.extend()

用于合并两个或多个对象的属性到第一个对象

evantianx commented 7 years ago

构造jQuery对象(五)

其他原型属性和方法

jQuery.fn = jQuery.prototype = {
    constructor: jQuery,
    init: function(selector, context, rootjQuery){},
    selector: "",
    jQuery: "1.7.1",
    length: 0,
    size: function(){},
    toArray: function(){},
    get: function(){},
    pushStack: function(elems, name, selector) {},
    each: function(callback, args){},
    ready: function(fn){},
    eq: function(){},
    first: function(){},
    last: function(){},
    slice: function(){},
    map: function(){},
    end: function(){},
    push: push,
    sort: [].sort,
    splice: [].splice
}

.selector,.jquery,.length,.size()

selector: "",

jquery: "1.7.1",

length: 0,

size: function() {
  return this.length
}

.toArray()/.get([index])

map(callback(index, domElement))/jQuery.map(arrayOrObject, callback(value, indexOrKey))

pushStack(element, name, arguments)

创建一个新的空jQuery对象,然后把DOM元素集合放入这个jQuery对象中,并保留对当前jQuery对象的引用。 是很多方法如find()add()等的支持。

pushStack : function( elems, name, selector ) {
  //创建一个空jQuery对象
  var ret = this.constructor();

  if(jQuery.isArray(elems)) {
    push.apply(ret, elems)
  }else {
    jQuery.merge( ret, elems )
  }

  //保留引用
  ret.prevObject = this;

  ret.context = this.context;

  if(name === "find") {
    ret.selector = this.selector + (this.selector? " ": "") + selector;
  }else if (name) {
     ret.selector = this.selector + "." + name + "(" + selector + ")";
  }

  return ret;
}

end()

结束当前链条中最近的筛选操作,并将匹配元素集合还原为之前状态。

end: function() {
  return this.prevObject || this.constructor(null)
}

总结: pushStack()用于入栈,end()用于出栈。

eq(index)/first()/last()/slice(start[, end])

方法调用链: first()/last() ——>eq(index)——>slice(start[,end])——>pushStack(elements, name, arguments)

eq: function (i) {
   //将字符串转换为数字
   i = +i;
   return i === -1?
              this.slice(i):
              this.slice(i, i+1);
},

first: function() {
   return this.eq(0);
},

last: function() {
  return this.eq(-1);
} ,

slice: function() {
  //先借用数组方法slice()从当前jQuery对象中获取指定范围的子集(数组),
  //再调用方法pushStack()把子集转换为jQuery对象
  //同时也通过prevObject保留了对当前jQ对象的引用
  return this.pushStack(slice.apply (this, arguments), "slice", slice.call(arguments).join(","))
}

push(value,...)/sort([orderfunc])/splice(start,deleteCount, value,...)

//仅在内部使用
push: push,
sort: [].sort(),
splice: [].splice
evantianx commented 7 years ago

静态属性和方法

重要的静态属性和方法。

jQuery.extend({
  noConflict: function( deep ) {},
  isReady: false,
  readyWait: 1,
  holdReady: function(hold) {},
  ready: function(wait){},
  bindReady: function(){},
  isFunction: function(obj){},
  isArray: Array.isArray || function(obj) {},
  isWindow: function(obj) {},
  isNumeric: function(obj){},
  type: function(obj){},
  isPlainObject: function(obj){},
  isEmptyObject: function(obj){},
  error: function(msg){},
  parseJSON: function(data) {},
  parseXML: function(data) {},
  noop: function(){},
  globalEval: function(data){},
  camelCase: function(string){},
  nodeName: function(elem, name) {},
  each: function(object, callback, args),
  //trim为String.prototype.trim
  trim: trim?function(text){}: function(text){},
  makeArray: function(array, results){},
  inArray: function(elem, array,i){},
  merge: function(first, second){},
  qrep: function(elems, callback, inv){},
  map: function(elems, callback, arg){},
  quid: 1,
  proxy: function (fn, context) {},
  access: function( elems, key, value, exec, fn, pass) {},
  now: function() {},
  uaMatch: function(ua) {},
  sub: function(){},
  browser: {}
})

jQuery.noConflict([removeAll])

用于释放jQuery对全局变量$的控制权,可选参数表示是否释放对全局变量jQuery的控制权。

//保留当前引用,是在jQuery被赋给全局之前调用
_jQuery = window.jQuery,
_$ = window.$,

jQuery.extend({
  noConflict: function(deep) {
     if(window.$ === jQuery) {
       window.$ = _$;
     }

     if(deep && window.jQuery === jQuery) {
       window.jQuery = _jquery;
     }

     return jQuery;
  }
})

//代码最后将jQuery和$暴露给全局
window.jQuery = window.$ = jQuery;

类型检测

jQuery.isFunction(obj)/jQuery.isArray(obj)

这两个方法都依赖于jQuery.type(obj)

isFunction: function(obj) {
  return jQuery.type(obj) === "function";
},

isArray: Array.isArray || function(obj) {
  return jQuery.type(obj) === "array";
}

jQuery.type(obj)用于判断参数类型:当参数是undefined或null时,返回"undefined"或"null";当参数是JS内部对象,则返回对应的字符串名称;其他情况一律返回“object”。

type: function(obj) {
  return obj === null ?
             String(obj):
             class2type[toString.call(obj)] || "object";
}

String(obj)相当于调用toString(obj),下面来分析下class2type这一行:

toString = Object.prototypr.toString,

class2type = {};

jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name){
    class2type["[object" + name + "]"] = name.toLowerCase();
})

jQuery.isWindow(obj)

判断传入的参数是否为window对象(通过检测是否存在特征属性setInterval来实现):

isWindow: function(obj) {
  return obj && typeof obj === "object" && "setInterval" in obj;
}

在1.7.2版本中改为了对window属性的检测,该属性为Window对象对自身的引用。

jQuery.isNumeric(value)

用于判断传入的参数是否为数字或者“类数字”:

isNumeric: function (obj) {
  return !isNaN(parseFloat(obj)) && isFinite(obj)
}

jQuery.isPlainObject(object)

用于判断传入的参数是否为纯粹的对象(即由对象直接量{}或new Object()创建的对象)

isPlainObject: function(obj) {
  //obj可转换为false || toString结果为[object Object] || obj为DOM元素 || obj为window对象
  if( !obj || jQuery.type(obj) != "object" || obj.nodeType || jQuery.isWindow(obj)) {
    return false;
  }

  try {
      //存在constructor属性
    if(obj.constructor &&
      //constructor为继承属性
       !hasOwn.call(obj, "constructor") &&
      //obj.constructor.prrototype的isPrototypeOf为继承属性
       !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")   
    ){
      return false;
    }
  }catch(e) {
     //IE8,9会抛出错误
     return false
  }
  var key;
  for (key in obj) {}

  //在枚举的时候会先枚举非继承属性,最后枚举继承属性;
  //所以检测最后一个属性是否为继承属性即可
  return key === undefined || hasOwn.call(obj, key);
}

jQuery.isEmptyObject(object)

用于检测对象是否为空(即不包含属性)

isEmptyObject: function(obj) {
  for(var name in obj) {
    return false;
  }
  return true;
}

解析JSON和XML

jQuery.parseJSON(data)

接受JSON字符串,返回解析后的JS对象。

parseJSON: function(data){
  //非法参数一律返回null
  if( typeof data !== "string" || !data ) {
    return null
  }

  //去除首尾空格(在IE6,7中不移除则无法正确解析)
  data = jQuery.trim(data);

  //若存在JSON.parse()方法则调用此方法解析
  if(window.JSON && window.JSON.parse) {
    return window.JSON.parse(data)
  }

  //不支持JSON.parse()的浏览器
  if(rvalidchars.test (data.replace(rvalidescape, "@")
                                      .replace(rvalidtokens, "]")
                                      .replace(rvalidbraces, ""))) {
     //注意这里的写法
     return (new Function("return " + data)) ();
  }

  jQuery.error("Invalid JSON: " + data);
}

正则匹配表达式如下:

rvalidchars = /^[\],:{}\s]*$/,
rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,

此处是参照json2.js的判断规则

jQuery.parseXML(data)

接受XML字符串,返回解析后的XML文档 使用浏览器原生XML解析函数实现: 在IE9+及其他浏览器中使用DOMParser对象解析;在IE9以下浏览器中使用ActiveXObject对象解析

parseXML: function(data) {
  var xml, tmp;
  try {
    if(window.DOMParser) {
      tmp = new DOMParser();
      xml = tmp.parseFromString(data, "text/xml");
    }else {
      xml = new ActiveXObject("Microsoft.XMLDOM");
      xml.async = "false";
      //成功返回true;
      //失败返回false,不抛出异常,并设置根节点documentElement为null
      xml.loadXML(data);
    }
  }catch(e) {
    xml = undefined;
  }
  //在IE以外的浏览器中,若DOMParser解析失败,不会抛出异常
  //而是返回一个包含了错误信息的文档对象<parsererror>
  if(!xml || !xml.documentElement || xml.getElementsByTagName("parsererror").length) {
    jQuery.error("Invalid XML: " + data)
  }
  return xml;
}

jQuery.globalEval(code)

用于在全局作用域中执行JS代码。 在IE中可以调用方法execScript()让JS代码在全局作用域中执行;在其他浏览器中,则需要在一个自调用匿名函数中调用eval()执行JS代码(确保执行环境为全局作用域)

globalEval: function (data) {
  if(data && rnotwhite.test( data )) {
    (window.execScript || function(data) {
      window["eval"].call(window, data);
    })(data);
  }
}

为何这样写?Eval JavaScript in a global context Blog

jQuery.camelCase(string)

转换连字符式的字符串为驼峰式:

rdashAlpha = /-([a-z]|[0-9])/ig,
//在IE中"-ms-transform"对应"msTransform"而非MsTransform
rmsPrefix = /^-ms-/,

fcamelCase = function(all, letter) {
  return (letter + "").toUpperCase()
},

camelCase: function(string){
  return string.replace(rmsPrefix, "ms-").replace(rdashAplpha, fcamelCase)
}

jQuery.nodeName(elem, name)

用于检查DOM元素的节点名称(即属性nodeName)与指定的值是否相等,检查时忽略大小写。

nodeName: function (elem, name) {
  //HTML文档元素的nodeName始终返回大写形式;
  //XML文档区分大小写,返回源码中的形式
  //所以要使用toUpperCase()
  return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
}

jQuery.trim(str)

用于移除字符串开头和结尾的空白符。 传入参数是null或undefined,则返回空字符串l;传入参数为对象,则首先获取对象的字符串表示,然后移除开头和结尾的空白符并返回。

rnotwhite = /\S/,

trimLeft = /^\s+/,
trimRoght = /\s+$/,

trim = String.prototype.trim,

//在IE9以下的浏览器中,\s不匹配不间断空格\xA0
//此时加上即可
if(rnotwhite.test("\xA0")) {
  trimLeft = /^[\s\xA0]+/;
  trimRight = /[\s\xA0]+$/
}

//存在原生方法trim时使用原生方法
//否则使用正则匹配替换
trim: trim?
        function(text) {
          return text == null?
                     "":
                     trim.call(text)
        }:
        function(text) {
          return text == null ?
                     "":
                     text.toString().replace(trimLeft, "").replace(trimRight, "")
        }

所谓的\xA0,即为不间断空格(non-breaking space) qq 20170113175307

数组操作方法

jQuery.makeArray(obj)

将一个类数组对象转换为真正的数组。 在jQuery内部调用时还可传入第二个参数,第一个参数中的元素会被并入第二个参数,最后返回第二个参数,此时返回的值不一定为数组。

push = Array.prototype.push,

makeArray: function(array, results) {
     var ret = results || [];

     if(array != null) {
       var type = jQuery.type(array);

       //满足下列条件之一时认为不是数组,也不是类数组对象
       //1.不存在length属性
       //2.字符串,length为其长度
       //3.函数,length为形参个数
       //4.window对象,length返回窗口中frame/iframe个数
       //5.正则对象,黑莓4.7中,正则对象也有length属性
       if(array.length == null
          || type === "string"
          || type === "function"
          || type === "regexp"
          || jQuery.isWindow(array)  ){
          //由于ret不一定为数组,故使用push.call而不是ret.push
          push.call(ret, array)
       }else {
          jQuery.merge (ret, array)
       }
     }
     return ret;
}

jQuery.inArray(value, array[,fromIndex])

在数组中查找指定的元素并返回下标,没有则返回-1(优先采用indexOf()方法)

inArray: function(elem, array, i) {
   var len;

   if( array ) {
     if(indexOf) {
       return indexOf.call(array, elem, i);
     }

     len = array.length;
     //修正i
     i = i? (i<0 ? Math.max(0, len+i) : i) : 0;

     for(; i< len; i++){
       if(i in array && array[i] === elem) {
         return i
       }
     }
   }

  return -1;
}

jQuery.merge(first, second)

用于合并两个数组的元素到第一个数组中(可以是数组或者类数组对象)

merge: function(first, second) {
  var i = first.length,
        j = 0;

  if(typeof second.length === "number") {
    for (var l = second.length; j<1; j++){
      first[i++] = second[j]
    }
  }else {
    //此时second为含有连续整型属性的对象如{0: 'a',1: 'b'}
    while (second[j] !== undefined){
      first[i++] = second[j++]
    }
  }

  //对于非数组需要手动修正其length
  first.length = i;

  return first;
}

jQuery.grep(array, function(elementOfArray, indexInArray)[, invert])

用于查找数组中满足过滤函数的元素,原数组不会受影响。 当未传入invert或传入invert且为false时,返回一个满足回调函数的元素数组;true时,返回一个不满足回调函数的元素数组。

grep: function(elems, callback, inv) {
   var ret = [], retVal;
   inv = !!inv;

   for( var i = 0, length = elems.length; i< length; i++) {
     retVal = !!callback(elems[i], i);
     if( inv !== retVal) {
       ret.push(elems[i])
     }
   }

   return ret;
}

jQuery.guid/jQuery.proxy(function, context)

jQuery.guid

全局计数器,用于jQuery事件模块和缓存模块。初始为1,使用时自增1。

guid: 1,
uuid: 1,

elem[internalKey] = id = ++jQuery.uuid;

handler.guid = jQuery.guid++;

jQuery.proxy(function, context)

接受一个函数,返回一个新函数。新函数总有特定的上下文。

jQuery.access(elems, key, value, exec, fn(elem, key, value), pass)

为集合中的元素设置一个或多个属性值,或者读取第一个元素的属性值。若设置的属性值为函数且exec为true,还会执行函数并取其返回值作为属性值。

attr()/prop()/css()提供支持

attr: function (name, value) {
  return jQuery.access(this, name, value , true, jQuery.attr)
},

prop: function (name, value) {
  return jQuery.access(this, name, value , true, jQuery.prop)
},

jQuery.fn.css = function(name, value) {
    //...
    return jQuery.access(this, name, value, true, function(elem, name, value) {
      return value !== undefined?
                 jQuery.style(elem, name, value):
                jQuery.css(elem, name);
    })
}

方法jQuery.access()源码如下:

access: function(elems, key,value, exec, fn, pass) {
  var length = elems.length;

  if(typeof key === "object"){
    for(var k in key){
      jQuery.access(elems, k, key[k], exec, fn, value);
    }
    return elems
  }

  //当key为对象时,value=undefined
  if(value !== undefined) {
    exec = !pass && exec && jQuery.isFunction(value);

    for(var i = 0; i< length; i++){
      fn(elems[i], key, exec? value.call(elems[i],i, fn(elems[i],key)):value, pass);
    }
    return elems
  }
  return length? fn(elems[0], key):undefined
}

jQuery.error(message)/jQuery.noop()/jQuery.now()

error: function(msg){
  throw new Error(msg)
},

noop: function(){},

now: function(){
  return (new Date()).getTime();
}

jQuery.uaMatch(ua)/jQuery.browser

jQuery.browser属性值结构如下:

//jQuery.browser
{
  webkit/opera/msie/mozilla: true,
  version: '版本号'
}

navigator是全局对象window的属性,指向一个Navigator对象,包含了正在使用的浏览器的信息;navigator.userAgent包含了浏览器用于HTTP请求的用户代理头(User-Agent)的值。

rwebkit = /(webkit)[ \/]([\w.]+)/,
ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/,
rmsie = /(msie)([\w.]+)/,
rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/,

userAgent = navigator.userAgent,

uaMatch: function(ua) {
   ua = ua.toLowerCase();
   var match = rwebkit.exec(ua) ||
                       ropera.exec(ua) ||
                       rmsie.exec(ua) ||
                       ua.indexOf("compatible") < 0 && rmozilla.exec(ua) ||
                       [];
   return {browser: match[1] || "", version: match[2] || "0"}; 
},

browser: {}

));

browserMatch = jQuery.uaMatch(userAgent);
if(browserMatch.browser) {
  jQuery.browser[browserMatch.browser] = true;
  jQuery.browser.version = browserMatch.version;
}