开始
本文仅对 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 方法进行扩展
- 先判断深浅复制的情况以及复制的是什么类型的变量,
- 然后要复制到 target 上,target 有可能是 jQuery 本身或者用户外部定义的变量,如果只传入一个值,则是扩展 jQuery 本身,
target=this=$=jQuery
- 否则则是扩展用户定义的变量,
target=arguments[0]
,即传入的第一个变量 - 最后,再进行拷贝,把扩展拷贝到 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 不同的数据类型,分析不同数据类型的行为,有以下几种情况:
- 如果传过来的数据是字符串:那么要分析字符串是否是 HTML 标签,如果是 HTML 那么就通过正则提取关键字并创建一个 HTM 标签输出
- 如果传过来的数据是不是 html 元素,那么要通过 querySelectorAll 来查询过滤,如果可以查询到是 DOM 中的选择器,那么就遍历输出他的值
- 如果传过来的元素是 DOM 节点,直接返回
- 如果传过来的数据是一个对象方法,那么要通过$(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 的时候,会把记录的参数传递给新添加的函数并立即执行这个新添加的函数
原理:
- 首先是
add()
方法:将穿过来的 options 先把他们转为真数组,然后将数组遍历出来挑选出类型为"Function"的数据,将数据添加到一个空数组中,等待执行 -
fire()
其实就是把添加到队列中的方法依次按规则输出执行,需要一个中间件 fireWith 提供上下文 - 实现 stopOnFalse,
fire()
在遍历方法的时候,如果结果为 false,来判定 options 是否有 stopOnFalse 参数,如果有立马退出 - 实现 once,定义一个参数来记录第一次执行
fire()
的方法,然后在调用执行fire()
这个方法判断是否传入有 once 参数,如果有,那么就不会再去执行fire()
方法 - 实现 memory,
add()
阶段要记录传入的 options 是否有 memory 这个参数,其次在执行fire()
的阶段记录它的 index 值 - 实现 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 延迟对象生成时,调用已添加的处理程序
实现原理
- 首先定义了 tuples 的数据结构,用来组装存储异步延迟三种不同状态信息的描述
- 然后定义一个 promise 用来封装 state,then,promise 对象
- 定义一个延迟对象 deferred = {};
- 遍历封装好的 tuples 数组队列,把 tuples 数组里第二个元素映射到对应的 Callbacks,将数组的第三个元素记录的最终状态信息给到 stateString,然后把数组第一个元素即延时对象的状态映射到 Callbacks 的 add 方法上,定义辅助方法
- 最后调用 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"]);
原理:
- jQuery 并没有将事件处理函数直接绑定到 DOM 元素上,而是通过 jQuery.event.add 存储在 cache 缓存对象上。
- 最后通过在 DOM 元素上通过 addEventListener/attachEvent 绑定事件。
- 当事件触发时,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
: {};
}
},
};
结语
以上分析,谢谢阅读
网友评论