JQuery原理分析

作者: 藤原拓鞋 | 来源:发表于2020-07-06 17:48 被阅读0次

    开始

    本文仅对 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
            : {};
        }
      },
    };
    

    结语

    以上分析,谢谢阅读

    相关文章

      网友评论

        本文标题:JQuery原理分析

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