当前位置: 首页 > 工具软件 > jQuery Notify > 使用案例 >

JQuery原理解析

步浩壤
2023-12-01

原文博客链接

藤原拓鞋的博客

开始

本文仅对 jQuery 基本的 API 及其原理进行分析,源代码一万多行并没有完整分析,仅作参考

jQuery 无 new 创建实例

jQuery 共享原型的设计思想,将 jQuery 原型对象共享,然后通过扩展实例方法属性以及添加静态属性以及静态方法实现 jQuery 的灵活扩展

实现方法:创建一个 jQuery 对象, 返回 jQuery 原型对象的 init 方法, 然后共享原型, 将 jQuery 挂载到 windows 上起别名 , 实 现 通 过 , 实现通过 ,来访问 jQuery 的构造函数.同理通过$.fn 来替代 jQuery.prototype

// 立即调用
(function(root){
    var jQuery = function (selector, context) {
        // jQuery对象实际上只是init构造函数
        // 如果调用了jQuery,则需要init
        return new jQuery.prototype.init(selector, context);
    };

    jQuery.fn = jQuery.prototype = {
        init:function(selector, context){};
    };

    // 共享原型对象
    jQuery.fn.init.prototype = jQuery.fn;
    root.$ = root.jQuery = jQuery;
})(this)

extend 方法

使用示例:

// 任意对象扩展
var obj = $.extend({}, { name: "james" });
// jQuery本身扩展
$.extend({
  work: function () {},
});

使用 jQuery 时,用 extend 方法进行扩展

  1. 先判断深浅复制的情况以及复制的是什么类型的变量,
  2. 然后要复制到 target 上,target 有可能是 jQuery 本身或者用户外部定义的变量,如果只传入一个值,则是扩展 jQuery 本身,target=this=$=jQuery
  3. 否则则是扩展用户定义的变量,target=arguments[0],即传入的第一个变量
  4. 最后,再进行拷贝,把扩展拷贝到 target 上并返回
// 扩展的方法
jQuery.extend = jQuery.fn.extend = function () {
  // 声明变量
  var options,
    name,
    copy,
    src,
    copyIsArray,
    clone,
    target = arguments[0] || {},
    length = arguments.length,
    // 从第1个参数开始解析,因为第0个是我们targer,用来接收解析过的数据的
    i = 1,
    // 是否是深拷贝,外界传过来的第一个参数
    deep = false;

  // 处理深层复制情况
  if (typeof target === "boolean") {
    // extender(deep,{},obj1,obj2)
    deep = target;
    target = arguments[i] || {};
    i++;
  }
  // 判断 targer不是对象也不是方法
  if (typeof target !== "object" && !isFunction(target)) {
    target = {};
  }

  // 如果只传递一个参数,则扩展jQuery本身
  if (length === i) {
    target = this;
    // 此时把i变为0
    i--;
  }

  for (; i < length; i++) {
    // 仅处理非null /未定义的值
    if ((options = arguments[i]) != null) {
      // 仅处理非null /未定义的值
      for (name in options) {
        copy = options[name];
        src = target[name];

        // 防止Object.prototype污染
        // 防止死循环循环
        if (name === "__proto__" || target == copy) {
          continue;
        }

        //如果我们要合并普通对象或数组,请递归
        // 此时的copy必须是数组或者是对象
        if (
          deep &&
          (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))
        ) {
          // 确保源值的正确类型  源值只能是数组或者对象
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && jQuery.isArray(src) ? src : [];
          } else {
            clone = src && jQuery.isPlainObject(src) ? src : {};
          }
          //永远不要移动原始对象,克隆它们
          target[name] = jQuery.extend(deep, clone, copy);

          //不要引入未定义的值
        } else if (copy !== undefined) {
          // 浅拷贝
          target[name] = copy;
        }
      }
    }
  }
  //返回修改后的对象
  return target;
};

// jQuery本身扩展属性和方法,这里并不是用户调用扩展,而是源码里面调用,扩展一些源码里用的的方法
// 以下方法都在后面的分析中有用到,而Deferred等API也在此扩展,后面进行单独分析
jQuery.extend({
  // 随机数
  expando: "jQuery" + (version + Math.random()).replace(/\D/g, ""),
  guid: 1, //计数器

  // 判断elem元素在不在arr数组中
  inArray: function (elem, arr) {
    return arr == null ? -1 : [].indexOf.call(arr, elem);
  },
  // 类型检测,判断是不是对象
  isPlainObject: function (obj) {
    // "[object Object]" 第二个O一定是大写,坑了我好几个小时.......
    return toString.call(obj) === "[object Object]";
  },
  // 类型检测,判断是不是数组
  isArray: function (obj) {
    return toString.call(obj) === "[object Array]";
  },
});

$() 选择器的封装

根据 jQuery 共享原型的设计,$()实际上调用的是 jQuery.prototype.init()

示例:

// 传入字符串
console.log($("a")); //创建DOM节点包装成jQuery对象
// 传入HTML
console.log($("<div>")); // //创建DOM节点包装成jQuery对象
// 传入对象
console.log($(document));
// 传入选择器
console.log($(".box"));
// 传入对象
console.log($(this)); // 把传入的对象包装成jQuery对象
// 传入方法
$(function () {
  console.log(11111); //这个是在页面加载完成后加载执行的,等效于在DOM文档加载完成后执行了$(document).read()方法
});

分析:
根据$()传过来的 selector 不同的数据类型,分析不同数据类型的行为,有以下几种情况:

  1. 如果传过来的数据是字符串:那么要分析字符串是否是 HTML 标签,如果是 HTML 那么就通过正则提取关键字并创建一个 HTM 标签输出
  2. 如果传过来的数据是不是 html 元素,那么要通过 querySelectorAll 来查询过滤,如果可以查询到是 DOM 中的选择器,那么就遍历输出他的值
  3. 如果传过来的元素是 DOM 节点,直接返回
  4. 如果传过来的数据是一个对象方法,那么要通过$(document).read()方法,监听拦截 DOMContentLoaded 方法,改变对象方法的指针然后依次加入到数组中,输出
/**
         * [selector] 传入的参数
         * [context] DOM 查询的限定范围
         * **/
        // 选择器函数
        init: function (selector, context) {
            context = context || document;
            var match, elem, index = 0;
            if (!selector) {
                return this;
            }

            // 传来的选择器数据selector是字符串
            if (typeof selector === "string") {

                if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) {
                    // 传过来的是html内容
                    match = [selector];
                }
                // 匹配html或确保没有为#id指定上下文
                if (match) {
                    // parseHTML(selector,context) 生成DOM元素      merge合并到jQuery数组
                    jQuery.merge(this, jQuery.parseHTML(selector, context));

                } else {
                    // 传过来的是选择器查询DOM节点
                    elem = document.querySelectorAll(selector);
                    // 转化为真数组
                    var elems = Array.prototype.slice.call(elem);
                    this.length = elem.length;

                    for (; index = elem.length; index++) {
                        this[index] = elems[index];
                    }
                    this.context = context;
                    this.selector = selector;
                }

            } else if (selector.nodeType) {
                // 传过来的选择器数据是DOM节点
                this.context = this[0] = selector;
                this.length = 1;
                // 直接返回此节点
                return this;

            } else if (isFunction(selector)) {
                // 传过来的选择器数据是函数,则文档加载完时调用它
                jQuery(document).ready(selector);

            }
            return jQuery.makeArray(selector, this);
        },
        ready: function (fn) {
            // 检测dom是否加载完毕,加载完毕则调用jQuery.ready,会遍历调用readylist
            document.addEventListener("DOMContentLoaded", jQuery.ready, false)
            if (jQuery.isReady) {
                // 调用此个init()时文档已经加载完,直接调用函数fn
                fn.call(document)
            } else {
                // 文档没有加载完,加入数组
                jQuery.readylist.push(fn);
            }
        }

而 init()中用到的 ready,merge,parseHTML,makeArray 方法则扩展在jQuery.extend({})

jQuery.extend({
  /**
   *  合并数组
   *  [first] jQuery的实例对象  this
   *  [second] DOM 节点
   */
  merge: function (first, second) {
    var l = second.length, // 1
      i = first.length, // 0
      j = 0;
    if (typeof l === "number") {
      for (; j < l; j++) {
        // 遍历DOM节点
        first[i++] = second[j];
      }
    } else {
      while (second[j] !== undefined) {
        first[i++] = second[j++];
      }
    }
    first.length = i;
    // 返回jQuery的实例对象
    return first;
  },
  /**
   * 解析HTML
   * [data] 传入的数据
   * [context] 返回的值
   * **/
  parseHTML: function (data, context) {
    if (!data || typeof data !== "string") {
      return null;
    }
    /**
     * exec() 是正则方法 返回为数组
     * [0] 为正则表达式相匹配的文本
     * [1] 表达式相匹配的文本
     * **/
    // 过滤掉符号,只提取标签 "<a>" ==> "a"
    var parse = rejectExp.exec(data);
    // 返回一个创建DOM的元素
    return [context.createElement(parse[1])];
  },
  /**
   * 将一个类数组对象转换为真正的数组对象
   * [arr] 传入的数组
   * [result] 返回的数组
   * **/
  makeArray: function (arr, result) {
    var ret = result || [];
    if (arr != null) {
      if (isArrayLike(Object(arr))) {
        jQuery.merge(ret, typeof arr === "string" ? [arr] : arr);
      } else {
        [].push.call(ret, arr);
      }
    }
    return ret;
  },

  isReady: false,
  readylist: [], // init()事件函数列表,文档加载后调用
  ready: function () {
    // 事件函数
    jQuery.isReady = true;
    // 遍历调用
    jQuery.readylist.forEach(function (callback) {
      callback.call(document);
    });
    // 清空
    jQuery.readylist = null;
  },
});

Callback API

$.callbacks用于管理函数队列,通过 add 添加处理函数到队列中,通过 fire 去执行这些函数,$.callbacks是在 jQuery 内部使用的,如为.ajax,$.Deffed 等组件提供基础功能函数

四种调用模式:

  • once:函数队列只执行一次
  • unique:往内部队列添加的函数保持唯一,不能重复添加
  • stopOnFalse:内部队列里的函数是依次执行的,当某个函数的返回值是 false 时,停止继续执行剩下的函数
  • memory:当参数队列 fire 一次过后,内部会记录当前 fire 的参数。当下次调用 add 的时候,会把记录的参数传递给新添加的函数并立即执行这个新添加的函数

原理:

  1. 首先是add()方法:将穿过来的 options 先把他们转为真数组,然后将数组遍历出来挑选出类型为"Function"的数据,将数据添加到一个空数组中,等待执行
  2. fire()其实就是把添加到队列中的方法依次按规则输出执行,需要一个中间件 fireWith 提供上下文
  3. 实现 stopOnFalse,fire()在遍历方法的时候,如果结果为 false,来判定 options 是否有 stopOnFalse 参数,如果有立马退出
  4. 实现 once,定义一个参数来记录第一次执行fire()的方法,然后在调用执行fire()这个方法判断是否传入有 once 参数,如果有,那么就不会再去执行fire()方法
  5. 实现 memory,add()阶段要记录传入的 options 是否有 memory 这个参数,其次在执行fire()的阶段记录它的 index 值
  6. 实现 unique,add()阶段进行判定 unique 和方法列表中是否有当前 function,判断通过才 push 到列表中
/**
         * [Callbacks] Callbacks回调方法
         * [options] 外界传进来的参数 可以是多个
         * **/
        Callbacks: function (options) {
            // 检测options的类型
            options = typeof options === "string" ? (optionsCache[options] || createOpeions(options)) : {};
            // 定义一个数组用来存放add将来的方法
            var list = [],
                length,
                index,
                startAdd,
                memory,
                start,
                memorySarts;
            var fire = function (data) {
                // memory
                memory = options.memory && data;
                // 为了防止memory再次调用一次定义了starts
                index = memorySarts || 0;
                start = 0;
                length = list.length;
                startAdd = true;        // 用来记录fire()方式是否执行 便于"once"方法操作
                // 遍历循环list
                for (; index < length; index++) {
                    // 通过遍历查找list[index]的值为false 且options有stopOnfalse这个参数时遍历终止返回
                    if (list[index].apply(data[0], data[1]) == false && options.stopOnfalse) {
                        break;
                    }
                }
            }
            var self = {
                // 添加函数的方法
                add: function () {
                    // Array.prototype.slice.call(arguments) 伪数组转真数组
                    var args = Array.prototype.slice.call(arguments);
                    start = list.length;
                    // 遍历args 找出里面的Function
                    args.forEach(function (fn) {
                        // 检索fn是是否是Function
                        if (toString.call(fn) === "[object Function]") {
                            // unique 不存在 且fn在list中 那么可以把fn添加到队里中
                            if (!options.unique || !self.has(fn, list)) {
                                list.push(fn);
                            }
                        }
                    });
                    // memory
                    if (memory) {
                        memorySarts = start;
                        fire(memory);
                    }
                    // 方便链式编程
                    return this;
                },
                // 定义一个上下文绑定函数
                fileWith: function (context, arguments) {
                    var args = [context, arguments];
                    // 非fire做限制调用
                    if (!options.once || !startAdd) {        // startAdd记录函数是否被执行过一次
                        fire(args);
                    }
                },
                fire: function () {
                    self.fileWith(this, arguments);
                },
                has: function (fn, array) {
                    return arr = jQuery.inArray(fn, array) > -1;
                }
            }
            return self;
        },

Deferred API

Deferred 是 jQuery 提出的回调函数解决方案,主要依赖 Callbacks 回调,主要解决的问题是:当一个异步依赖于另一个异步请求的结果时,或者某个操作需要等另外几个操作都结束后才开始等

API:

  • $.Deferred() 生成一个 deferred 对象
  • deferred.done() 指定操作成功时的回调函数
  • deferred.fail() 指定操作失败时的回调函数
  • .promise()返回一个 Promise 对象来观察当某种类型的所有行动绑定到结合,排队与否还是已经完成
  • deferred.resolve() 手动改变 deferred 对象的运行状态为"已完成",从而立即触发done()方法
  • deferred.reject() 这个方法与deferred.resolve()正好相反,调用后将 deferred 对象的运行状态变为"已失败",从而立即触发fail()方法
  • $.when() 为多个操作指定回调函数
  • deferred.then()可以把done()fail()合在一起写,参考 Promise
  • deferred.progress()当 Deferred 延迟对象生成时,调用已添加的处理程序

实现原理

  1. 首先定义了 tuples 的数据结构,用来组装存储异步延迟三种不同状态信息的描述
  2. 然后定义一个 promise 用来封装 state,then,promise 对象
  3. 定义一个延迟对象 deferred = {};
  4. 遍历封装好的 tuples 数组队列,把 tuples 数组里第二个元素映射到对应的 Callbacks,将数组的第三个元素记录的最终状态信息给到 stateString,然后把数组第一个元素即延时对象的状态映射到 Callbacks 的 add 方法上,定义辅助方法
  5. 最后调用 Callbacks 的 fireWith 方法实现队列的回调
/**
         * Deferred  异步回调解决方案
         *
         * **/
        Deferred: function (func) {
            /**
             *  tuples     定义一个数组来存储三种不同状态信息的描述
             *  第一个参数  延时对象的状态
             *  第二个参数  往队列里添加处理函数
             *  第三个参数  创建不同状态的队列
             *  第四个参数  记录最终状态信息
             * **/
            var tuples = [
                ["resolve", "done", jQuery.Callbacks("once memory"), "resolved"],
                ["reject", "fail", jQuery.Callbacks("once memory"), "rejected"],
                ["notify", "progress", jQuery.Callbacks("memory")]
            ],
                state = "pending",  // 进行中的状态
                promise = {
                    state: function () {
                        return state;
                    },
                    then: function () {
                    },
                    promise: function (obj) {
                        console.log(promise);
                        debugger
                        return obj != null ? jQuery.extend(obj, promise) : promise;
                    }
                },
                // 延迟对象   属性 方法
                deferred = {};
            // 遍历 tuples
            tuples.forEach(function (tuple, i) {
                // 创建Callbacks队列
                var list = tuple[2],
                // 拿到当前最终信息的描述
                var stateString = tuple[3];
                // promise[done | fail |progress]  将这三个状态都拿到Callbacks
                promise[tuple[1]] = list.add;

                // 如果有最终状态,成功或者失败
                if (stateString) {              // 第一个处理程序改变state状态
                    list.add(function () {
                        // state = [resolved | rejected]
                        state = stateString;        // 改变状态
                    });
                }

                // deferred [resolve | reject | notify ]  延时对象的状态拿到函数的引用
                deferred[tuple[0]] = function () {
                    deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments);
                    return this;
                };
                // list.fireWith 执行队列并且传参
                // 调用队列中的处理函数 并且给他们传参 绑定执行时的上下文对象
                deferred[tuple[0] + "With"] = list.fireWith;
            });

            // 调用promise.promise,使得deferred扩展添加promise中的方法
            promise.promise(deferred);
            // 返回
            return deferred;
        },
        // 执行一个或多个对象的延迟函数的回调函数
        when: function (subordinate) {
            return subordinate.promise();
        },

jQuery 事件绑定原理

示例:

// 多个事件绑定同一个函数:
$("#id").on("mouseover mouseout", function () {
  console.log("hello");
});
// 多个事件绑定不同函数:
$("#id").on({
  mouseover: function () {
    $("body").css("background-color", "red");
  },
  mourseout: function () {
    $("body").css("background-color", "yellow");
  },
  click: function () {
    $("body").css("background-color", "black");
  },
});
// 自定义事件:
$("#id").on("myOwnEvent", function (event, showName) {
  console.log("hello");
});
$("#id").trigger("myOwnEvent", ["james"]);

原理:

  1. jQuery 并没有将事件处理函数直接绑定到 DOM 元素上,而是通过 jQuery.event.add 存储在 cache 缓存对象上。
  2. 最后通过在 DOM 元素上通过 addEventListener/attachEvent 绑定事件。
  3. 当事件触发时,eventHandle 被执行,eventHandle 再去$.cache 中寻找曾经绑定的事件处理函数并执行
/**
 *  获取随机缓存值
 * **/
function Data() {
  this.expando = jQuery.expando + Math.random();
  this.cache = {};
}
Data.uid = 1;
Data.prototype = {
  key: function (elem) {
    var descript = {},
      unlock = elem[this.expando];
    if (!unlock) {
      unlock = Data.uid++;
      descript[this.expando] = {
        value: unlock,
      };
      Object.defineProperties(elem, descript);
    }
    // 确保缓存对象记录信息
    if (!this.cache[unlock]) {
      this.cache[unlock] = {};
    }
    return unlock;
  },
  get: function (elem, key) {
    var cache = this.cache[this.key[elem]];
    return key === undefined ? cache : cache[key];
  },
};
var data_priv = new Data();

// jQuery 事件模块
jQuery.event = {
  // 给选中元素注册事件处理函数
  add: function (elem, type, handler) {
    var eventHandle, events, handlers;
    // 事件缓存
    var elemData = data_priv.get(elem);
    //检测handler是否存在ID 如果没有那么就传给他一个ID
    //添加ID的目的是 用来寻找或者删除相应的事件
    if (!handler.guid) {
      handler.guid = jQuery.guid++;
    }
    // 给缓存增加事件处理语柄
    // 同一个元素,不同事件,不重复绑定
    if (!(events = elemData.events)) {
      events = elemData.event = {};
    }
    if (!(eventHandle = elemData.handler)) {
      // event 对象代表事件的状态 通过apply传递
      eventHandle = elemData.handler = function (e) {
        // 修复事件并执行
        return jQuery.event.dispatch.apply(eventHandle.elem, arguments);
      };
    }
    eventHandle.elem = elem;
    // 通过events 存储同一个元素上的多个事件
    if (!(handlers = events[type])) {
      handlers = events[type] = [];
      handlers.delegateCount = 0;
    }
    handlers.push({
      type: type,
      handler: handler,
      guid: handler.guid,
    });
    // 增加事件
    if (elem.addEventListener) {
      elem.addEventListener(type, eventHandle, false);
    }
  },

  // 修复事件对象event 从缓存体中的events对象取得对应的队列
  dispatch: function (event) {
    // IE兼容性处理: event,target or event.srcElement
    // event = jQuery.fix(event);
    // 提取当前元素在cache中的events属性值
    var handlers = data_priv.get(this, "events" || {})[eval.type] || [];
    event.delegateTarget = this;
    // 执行事件处理函数
    jQuery.event.handlers.call(this, event, handlers);
  },

  // 事件处理
  handlers: function (event, handlers) {
    handlers[0].handler.call(this, event);
  },

  // event兼容性处理
  fix: function (event) {
    if (event[jQuery.expando]) {
      return event;
    }
    var i,
      prop,
      copy,
      type = event.type,
      originalEvent = eval,
      fixHook = this.fixHook[type];

    if (!fixHook) {
      this.fixHook[type] = fixHook = rmouseEvent.test(type)
        ? this.mouseHook
        : rkeyEvent.test(type)
        ? this.keyHooks
        : {};
    }
  },
};

结语

以上分析,谢谢阅读

 类似资料: