源码共读06:JQuery源码 打造属于自己的js类库

139 阅读4分钟

jquery Github

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是学习源码整体架构系列第一篇,链接: juejin.cn/post/684490…

自执行匿名函数

外部访问不了内部的变量和函数,内部可以访问到外部的变量,但里边定义了自己的变量,也不会访问到外界的变量。匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。

自执行函数表达式(IIEF)

(function(global, factory) {

})(typeof window !== "undefined" ? window : this, function(window, noGloabl){
});

浏览器环境下,最后把$jQuery函数挂载在window上,所以外界就可以访问到$和jQuery了。

if (!noGlobal) {
  window.jQuery = window.$ = jQuery;
}
// 其中`noGlobal`参数只有在这里用到。

支持多种环境下使用 比如 commonjs、amd规范

commonjs 规范支持

Commonjs 实现 主要代表 nodejs

// global是全局变量,factory 是函数
(function(gobal, factory) {

  // 使用严格模式
  "use strict";
  // CommonJS或者CommonJS-like
  if (typeof module === "object" && typeof module.exports === 'object') {
    // 如果存在global.document 则返回factory(global, true);
    module.exports = global.document ?
      factory(global, true) :
      function(w) {
`       if(!w.document) {
            throw new Error( "jQuery requires a window with a document" );  
         }
         return factory(w)
      };
  } else {
    factory( global );
  }
  
  // Pass this if window is not defined yet
  第一个参数判断window,存在返回window,不存在返回this
})( typeof window !== 'undefined' ? window : this, function(window, Global) {} )

amd 规范 主要代表 requirejs

if (typeof define === "function" && define.amd) {
 define('jQuery', [], function() {
   return jQuery; 
 });
}

cmd 规范 主要代表 seajs

无new 构造

实际上也是可以new的,因为jQuery是函数。而且和不用new效果是一样的。new 显示返回对象,所以和直接调用jQuery函数作用是一样的。

源码:

var version = "@VERSION"

// Define a local copy of jQuery
jQuery = function(selector, context) {
  // 返回new之后的对象
  return new jQuery.fn.init(selector, context)
}
jQuery.fn = jQuery.prototype = {
  // jQuery当前版本
  jQuery: version,
  // 修正构造器为jQuery
  contructor: jQuery,
  length: 0,
};
init = jQuery.fn.init = function( selector, context, root ) {
  ...
  if(!selector) {
    return this;
  }
  // ...
};
init.prototype = jQuery.fn;
jQuery.fn === jQuery.prototype; // true
init = jQuery.fn.init;
init.prototype = jQuery.fn;
// 相当于
jQuery.fn.init.prototype === jQuery.fn; // true
jQuery.fn.init.prototype === jQuery.prototype; // true

jQuery原型关系图

<sciprt src="https://unpkg.com/jquery@3.4.1/dist/jquery.js">
</script>
console.log(jQuery);
/ 在谷歌浏览器控制台,可以看到jQuery函数下挂载了很多静态属性和方法,在jQuery.fn 上也挂着很多属性和方法。

Vue源码中,也跟jQuery类似,执行的是Vue.prototype._init方法

function Vue (options) {
  if (!(this instanceof Vue)  ) {
     warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}
initMixin(Vue);
function initMixin (Vue) {
  Vue.prototype._init = function (options) {};
};

核心函数之一:extend

用法:

jQuery.extend( target [, object1] [objectN] )   // Returns: Object
jQuery.extend( [deep ], target, object1 [, objectN] )

示例:

// 1.jQuery.extend(target)
var result1 = $.extend({
    job: 前端工程师
})
console.log(result1, result1.job); // $fn 加了一个job属性 前端工程师
// 2.jQuery.extend(target, object1)
var result2 = $.extend(
    { name: 'sam' },
    { job: '前端工程师' }
)
console.log(result2); // {name: 'sam', job: '前端工程师'}
// deep 深拷贝
// 3.jQuery.extend([deep], target, object1 [, objectN])
var result3 = $.extend(true, {
    name: 'sam',
    other: {
        mac: 0,
        ubuntu: 1,
        windows: 1 
    }
}, {
    job: '前端工程师',
    other: {
      mac: 1,
      linux: 1,
      windows: 0
    }
})
console.log(result3);
/**
* deep: true
* { 
*    name: 'sam',
*    other: {
*        mac: 1,
*        ubuntu: 1,
*        windows: 0,
*        linux: 1      
*   },
*.  job: '前端工程师'
* }
**/
/**
* deep: false
* { 
*    name: 'sam',
*    other: {
*        mac: 1,
*        linux: 1  
*        windows: 0,
*   },
*.  job: '前端工程师'
* }
**/

总结:extend函数既可以实现给jQuery函数实现浅拷贝、也可以使用深拷贝。可以给jQuery上添加静态方法和属性,也可以像jQuery.fn(也就是jQuery.prototype)上添加属性和方法,这个功能归功于this,jQuery.extend被调用时指向JQuery,jQuery.fn.extend被调用时指向jQuery.fn。

浅拷贝实现

// 浅拷贝实现
jQuery.extend = function() {
  // options 是扩展对象object1、object2...
  var options,
  // object对象上的键
  name,
  // copy object对象上的值,也就是需要拷贝的值
  copy,
  // 扩展目标对象,可能不是对象,所以或对象
  target = arguments[0] || {},
  // 定义1为1
  i = 0,
  // 定义实参个数length
  length = arguments.length;
  
  // 只有一个参数
  if (i === length) {
    target = this;
    i--;
  }
  for (; i< length; i--) {
    // 不是undefined,也是不是null
    if ((options = arguments[i]) !== null) {
      if (name in options) {
        copy = options[name]
        // 防止死循环,continue 跳出当前此循环
        if (name === "__proto__" || target === copy) {
          continue;
        }
        if (copy !== undefined) {
          target[name] = copy;
        }
      }
    }
  }
  // 最后返回目标对象
  return target;
}
// 深拷贝则主要在以下代码做判断。可能是数组或对象引用类型的值,做判断
if (copy !== undefined) {
  target[name] = copy;
}

深拷贝实现

$.extend = function(){
  // options 是扩展的对象object1、object2...
  var options,
  // object对象上的键
  name,
  // copy object对象上的值,也就是需要拷贝的值
  copy,
  // 深拷贝新增的四个变量 deep, src, copyIsArray、clone
  deep = false,
  // 源目标,需要往上面赋值的
  src,
  // 需要拷贝的值的类型是函数
  copyIsArray,
  
  clone,
  // 源目标对象,可能不是对象,所以或空对象
  target = arguments[0] || {},
  // 定义i为1
  i = 1,
  // 定义实参个数length
  length = arguments.length;
  
  // 处理深拷贝情况
  if (typeof target === 'boolean') {
    deep = target;
    
    // Skip the boolean and the target
    // target 目标对象开始后移
    target = arguments[i] || {}
    i++;
  }
  
  // Handle case when target is a string or something (possible in deep copy)
  // target不等于对象,且target不是函数的情况下,强制将其赋值为空对象
  if (typeof target !== 'object' && isFunction(target)) {
    target = {}
  }
  
  // 只有一个参数时
  if (i === length) {
    target = this;
    i--;
  }
  if (; i < length; i--) {
    // 不是undefined,也不是null
    if ((target = arguments[i]) !== null) {
      for (name in target) {
          copy = target[name];
          // 防止死循环,continue 跳出当前此循环
          if (name === '__proto__' || target === copy) {
            continue;
          }
          
          // Handle case when target is a string or something (possible in deep copy)
          // 这里deep为true,并且需要拷贝的值有值,并且是纯粹的对象
          // 或者需要拷贝的值是数组
          if (deep && copy && (jQuery.isPainObject(copy) ||
            (copyIsArray = Array.isArray(copy)))) {
           
            // 源目标,需要往上面赋值的
            src = target[name];
            
            // Ensure proper type for the source value
            // 拷贝的值为数组,并且src不是数组,clone对象改为空数组
            if (copyIsArray && !Array.isArray(src)) {
              clone = [];
            // 拷贝的值不是数组,并且src不是纯粹的对象,clone赋值为空对象
            } else if (!copyIsArray && jQuery.isPlainObject(src)) {
              clone = {};
            } else {
              // 否则 clone = src
              clone= src;
            }
            // 把下一次循环时,copyIsArray需要重新赋值为false
            copyIsArray = false;
            
            // Never move original objects, clone them
            // 递归调用自己
            targte[name] = jQuery.extend(deep, clone, object);
            
            // Don't bring in undefined values
          } else if (copy !== undefined) {
            target[name] = copy;          
          }
      }
    }
  }
  // 最后返回目标对象
  return target;
}

深拷贝衍生的函数 isFunction

判断函数是否是函数

var isFunction = function isFunction(.obj ) {
 // Support: Chrome <=57, Firefox <=52
 // In some browsers, typeof returns "function" for HTML <object> elements
 // (i.e., `typeof document.createElement( "object" ) === "function"`). 
 // We don't want to classify *any* DOM node as a function.
  return typeof obj === 'function' && typeof obj.nodeType !== 'number';
}

深拷贝衍生的函数isPlainObject

jQuery.isPlainObject(obj)测试对象是否为纯粹的对象(通过"{}"或者"new Object"创建)

jQuery.isPlainObject({}) // true
jQuery.isPlainObject("test") // false

var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString();
var hasOwn = class2type.hasOwnPrototype;
var fnToString = hasOwn.toString();
var ObjectFunctionString = fnToString.call( object );

jQuery.extend({
  isPlainObject: function(obj) {
    var proto, Ctor;
    // Detect obvious negatives
    // Use toString instead of jQuery.type to catch host objects
    // !obj 值为true 或者不为[object object]
    // 直接返回 false
    if (!obj || toString.call(obj) !== "[object object]") {
      return false;
    }
    
    proto = getProto(obj);
    
    // Objects with no prototype (e.g., `Object.create( null )`) are plain
    // 原型不存在 比如Object.create(null) 直接返回true
    if (!proto) {
      return true;
    }
    
    // Objects with prototype are plain iff they were constructed by a global Object function
    Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
    // 构造器是函数,并且fnToString.call(Ctor) === fnToSTring.call( Object );
    return typeof Ctor === function && ObjectFunctionString;                                                
  }
})

链式调用

JQuery能够链式调用是因为一些函数执行结束后return this。比如jQuery源码中的addClass、removeClass、toggleClass。

jQuery.fn.extend({
  addClass: function() {
    // ...
    return this;
  },
  removeClass: function() {
     // ...
     return this;
  },
  toggleClass: function() {
    // ...
    return this;
  },
})

Jquery.noConflict 很多js库都会有的防冲突函数

jQuery.noConflict API

// 用法:
<script>
var $ = '我是其他$,jQuery不要覆盖我';
</script>
<script src="./jquery-3.4.1.js"></script>
<script>
$.noConflict()
console.log($); // 我是其他$,jQuery不要覆盖我
</script>

jQuery.noConflict源码

import { jQuery } from "../core.js";
var
// Map over jQuery in case of overwrite
_jQuery = window.jQuery,

// Map over the $ in case of overwrite
_$ = window.$;

jQuery.noConflict = function( deep ) {
  // 如果已经存在$ === jQuery
  // 把已经存在的_$赋值给window.$
  if (window.$ === jQuery) {
    window.$ = _$;
  }
  
  // 如果是deep为true,并且已经存在window.jQuery === jQuery
  // 把已经存在的_jQuery赋值给window.jQuery
  if (deep && window.jQuery === jQuery) {
    window.jQuery = _jQuery;
  }
  
  // 最后返回jQuery
  return jQuery;
}

总结

jQuery整体结构:自执行匿名函数、无new构造、支持多种规范、核心函数之extend、链式调用、jQuery noConflict等。

// 重新梳理文中浅析源码结构
// 源码结构
(function(global, factory) {
  'use strict';
  if (typeof module === 'object' && typeof module.exports === 'object') {
    module.export = window.document ?
        factory(global, true) :
        function(w) {
          if (!w.document) {
            throw new Error('jQuery requires a window with a document');
          }
          return factory(w)
        }
  } else {
    factory(global);
  }
})(typeof window !== 'undefined' ? window : this, function(window, noGlobal) {
  var version = '3.4.1';
  
  // Define a local copy of jQuery
  jQuery = function(selector, context) {
    return new jQuery.fn.init(selector, context);
  }
  
  jQuery.fn = jQuery.prototype = {
    jquery: version,
    constructor: jQuery,
    length: 0,
    ...
  }
  
  jQuery.extend = jQuery.fn.extend = function() {}
  
  jQuery.extend({
   isPlainObject: function () {
     // ...
   }
   ...
  })
  
  init = jQuery.fn.init = function(selector, context, root) {}
  
  init.prototype = jQuery.fn;
  
  if (typeof define === 'function' && define.amd) {
    define("jquery", [], function() {
      return jQuery;
    });
  }
  
  jQuery.noConflict = function(deep) {};
  
  if (!noGlobal) {
    window.jQuery = window.$ = jQuery;
  }
  
  return jQuery;
});

此文章为2024年02月Day1源码共读,每一次脑海里闪过努力的念头,都是未来的你在向你求救。