Jquery 源码浅谈

开题吐槽

有人上来就说了:啊啊这都 0202 年了,怎么还学 JQuery 呢,远古时代么?

其实写这篇文章的目的呢,主要就是目前在复习 面向对象 的知识,通过分析 JQuery 源码的思想,可以帮助我更好的理解面向对象~

而且我原先还真没看过JQ的源码,借此跟大家分享一下,也请大家批评指正!

教程是基于开课吧的课程,自己整理出的笔记!!!


Hello Jquery

首先看一个例子:我想给 div 元素添加点击事件,那么我们可以用 JQ 这样写

1
2
3
$('div').click(function () {
console.log('点击了box1')
})

我们有没有思考过,Jquery 怎样帮助我们实现这个功能的呢?

我们先来分析一下:$('.box1') 是一个 Jquery 对象,然后调用对象中的 click 方法,方法里传递了一个匿名函数!

知道了这些,就可以开始写一个自己的 jq 了来,vscode上号!

首先新建一个自己的 myJquery.js,然后 cv 大法。

1
2
3
4
5
6
7
8
9
10
11
12
// myJquery.js
function $(arg) {
// $() 函数执行后,返回一个对象
// 对象中有 click 方法
return {
click(fn) {
fn()
// 这里应该写点击事件的逻辑
// 但是我并没有添加监听事件
},
};
}

当然这个是不完整的,因为我还没有绑定事件呢,别着急,咱们一点一点来~

首先我要做一个简单的优化,因为这样写感觉有点乱,写成类的话看起来更清爽些(写成类的话注意兼容性的问题,我这里没有考虑兼容性)

1
2
3
4
5
6
7
8
9
10
11
12
// myJquery.js
class myJquery{
constructor(){}
click(){
console.log("点击事件")
}
}
function $(ele){
// 这里返回一个实例化对象,本质还是一个对象~
// 之后我们把焦点放在处理构造函数即可
return new myJquery(ele)
}

然后咱们完善一下点击事件的逻辑:

  • 首先要获取到元素
  • 然后添加绑定事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// myJquery.js
class myJquery{
constructor(ele){
// 通过元素
this.ele = document.querySelectorAll(ele);
}
click(){
// 添加绑定事件
this.ele.forEach(item=>{
item.addEventListener('click',function(){
console.log(this)
})
})
}
}

myjq
这样虽然能够实现功能,但是 Jquery 本身并不是这样搞的

我们可以打印一下 Jquery 对象本身,然后再打印一下我们写的 myJquery 对象本身,对比一下

1
console.log($("div"));

在这里插入图片描述
在这里插入图片描述

我们看到,Jquery 把每个元素挂到了对象上,像一个伪数组对吧~,并且有 lengthprevObject 属性

看到学霸的答案了,咱们边抄边想为啥这么设计~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// myJquery.js
class myJquery {
constructor(ele) {
let arr = document.querySelectorAll(ele);
// 把每个节点挂到 this 上
arr.forEach((item, index) => {
this[index] = item;
});
this.length = arr.length;
}
}
function $(ele) {
return new myJquery(ele);
}

写完之后,打印一下咱们的节点:

在这里插入图片描述

看,是不是比较像jQuery对象了,不过还少一个属性 prevObject,待会儿会讲到的(在 end 封装 中)

接着把添加节点的那一坨代码提取到 addElement 中去,(为了使代码更清晰),然后再把点击事件的逻辑补充一下~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// myJquery.js
class myJquery {
constructor(ele) {
let arr = document.querySelectorAll(ele);
this.addElement(arr);
}
addElement(arr) {
// 把每个节点挂到this上
// 这里的 this 就是实例化的对象,就是下面 $ 函数返回的实例化对象
arr.forEach((item, index) => {
this[index] = item;
});
this.length = arr.length;
}
click(fn) {
// 绑定事件
for (let i = 0; i < this.length; i++) {
this[i].addEventListener("click", fn, false);
}
}
}
function $(ele) {
return new myJquery(ele);
}

在这里插入图片描述
看!JQuery 简单的源码咱就搞定了!

至于为什么这么设计呢,我个人理解的哈,有以下的好处

  • 方便查找:我们把找到的节点挂到实例化的对象上,以后想查一下这个 Jquery对象选了谁,直接 console.log($("节点")) 即可,而你不挂到this上,this上就是空空的,没有东西的。
  • 方便源码的开发:比方说eqon,这类方法,我们开发源码的时候,如果this上有对象的话,直接for 循环遍历添加功能即可。

当然这只是我的愚见,多年以后回过头来再看看估计是另外一种感悟吼吼吼。

鸡汤总结:研究某一个框架,先去研究它 宏观如何实现,理解它的实现思想,再去研究如何实现的


链式操作基础

jq 是有链式调用的,就是跟链条一样点啊点的,举个例子

1
2
3
4
// 选择所有 div 中的下标为 0 的,然后绑定点击事件
$("div").eq(0).click(function(){
console.log(2222);
})

那我们怎样实现呢?

首先看一下对象如何链式调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
fn1() {
console.log("执行了 fn1")
return this;
},
fn2() {
console.log("执行了 fn2")
return this;
}
}
obj.fn1().fn2().fn1();
// console.log("执行了 fn1")
// console.log("执行了 fn2")
// console.log("执行了 fn1")

观察代码,主要还是把 this 给返还了,this 是实例化对象,也就是 obj

接下来的几个封装方法会用到它的!


封装

eq 封装

功能描述:eq(index) 拿到集合中的某一个

1
2
3
4
// myJquery.js
eq(index) {
return new myJquery(this[index]); // 返还对象
}

end 封装

jQuery 中还有个 end 方法,作用是返回上一个操作节点
比方说我 $("div").eq(1) 之后还想拿到前面的$("div"),就可以这样操作:$("div").eq(1).end()

主要的思想就是:当前 jquery对象有个 prevObject 属性,用来保存上一个操作的节点,所以end 操作其实没干什么,就是把 prevObject 返还给你。

1
2
3
4
// myJquery.js
end() {
return this['prevObject'];
}

end 的这个操作,需要在初始化的时候处理一下,默认的前一个对象是 document

1
2
3
4
5
6
7
8
// myJquery.js
constructor(arg, root) {
if (typeof root === 'undefined') {
this['prevObject'] = [document];
} else {
this['prevObject'] = root;
}
}

on 封装

jQuery 中的 on 方法,可以写很多个事件,比方说

1
2
3
4
// html
$(".box1").on(" mouseover mousedown ",function(){
console.log("鼠标事件");
})

有个小优化,这里的字符串里可能有很多空格,就像上面的栗子那样

这里处理一下字符串,然后遍历绑定即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
// myJquery.js
on(eventName, fn) {
let reg = /\s+/g;
// 去掉开头和结尾的空格
eventName = eventName.trimStart().trimEnd().replace(reg, " ");
let arr = eventName.split(" ");
console.log(arr); // (2) ["mouseover", "mousedown"]
for (let i = 0; i < this.length; i++) {
for (let j = 0; j < arr.length; j++) {
this[i].addEventListener(arr[j], fn, false);
}
}
}

css 封装

jq 中调用 css 有如下几种方式

  • 获取属性:“$("div").css("background")
  • 设置属性1:$("div").css("width",200);
  • 设置属性2:$("div").css({"width":"200px","height":"200px",'opacity':0.5 });

可以看出他们的区别:

  • 获取属性:只需要传递一个字符串
  • 设置属性
    • 设置属性1:传递两个参数
    • 设置属性2:传递一个对象

我们可以根据区别,然后做不同的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
css(...arg) {
// 对于形参个数不固定,可以用 rest 参数来处理
if (arg.length === 1) {
if (typeof arg[0] === "string") {
// 方式一:获取属性
// ie6 的话可以用 currentStyle 解决兼容性问题
return window.getComputedStyle(this[0], null)[arg[0]];
} else {
// 方式三:设置属性
for (let i = 0; i < this.length; i++) {
for (let j in arg[0]) {
this.setStyle(this[i], j, arg[0][j]);
}
}
}
} else {
// 方式二:设置属性
for (let i = 0; i < this.length; i++) {
this.setStyle(this[i], arg[0], arg[1]);
}
}
}
setStyle(node, attr, val) {
node.style[attr] = val;
}

写好之后可以在 html 里调用一下

1
$("div").css({"width":"200px","height":"200px",'opacity':0.5});

在这里插入图片描述
嗯,看起来没啥大问题~


css 扩展:$.cssNumber

其实在 jquery 设置属性的时候,有些属性值你不写 px ,它也会帮你处理,比方说

1
2
// 200 并没有写单位,jquery 会帮你加上 px 的
$("div").css("width",200)

所以,我们的代码还可以优化一下,在设置样式属性的时候,判断一下是否是数字

1
2
3
4
5
setStyle(ele,styleName,styleValue){
if(typeof styleValue === 'number'){
styleValue = styleValue + "px";
}
}

这样处理了,到底好不好呢?

其实并不好,因为像 opacity 这样的属性,他就 没有单位

那么jquery是如何处理的呢?

这里设置了静态属性 $.cssNumber :用来存放一些不需要加单位的样式属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$.cssNumber = {
animationIterationCount: true,
columnCount: true,
fillOpacity: true,
flexGrow: true,
flexShrink: true,
fontWeight: true,
gridArea: true,
gridColumn: true,
gridColumnEnd: true,
gridColumnStart: true,
gridRow: true,
gridRowEnd: true,
gridRowStart: true,
lineHeight: true,
opacity: true,
order: true,
orphans: true,
widows: true,
zIndex: true,
zoom: true,
};

如果某一个属性不需要单位,我们可以这样判断一下

1
2
3
4
5
6
7
setStyle(node, attr, val) {
// 这个属性不在 不需要加单位 的阵列里
if(typeof val === 'number' && !(attr in $.cssNumber)){
val = val + 'px';
}
node.style[attr] = val;
}

然后在测试一下,莫得问题~

1
$("div").css({"width":200,"height":200,'opacity':0.5});

css 扩展:$.cssHooks

比方说,未来的某一天,出现了新的样式属性 ,比方说 wh 吧,用来设置宽高的。我们希望通过
$("div").css("wh","200px"); 来一并设置div的宽高属性

jquery 也想到了这一点,为了扩展未来的属性,添加了静态属性:样式钩子$.cssHooks

通过上面的栗子我们知道 css 有两个核心功能:获取属性、设置属性

这个钩子主要也是处理这两个核心功能:分别是 getset 两种方法

其中getset方法是固定的,但是里面处理的逻辑,是开发者自己定义的,也就是 jquery 对未来单位的扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
// html
$.cssHooks['wh'] = {
get(ele){
// 获取属性
// console.log(ele);
return ele.offsetWidth + " " + ele.offsetHeight;
},
set(ele,value){
// 设置属性
ele.style.width = value;
ele.style.height = value;
}
}

然后可以尝试使用一下:

1
2
3
4
// html
// 走 get 方法
let res = $("div").css("wh");
console.log(res);
1
2
// 走 set 方法
$("div").css("wh","200px");

那这样的源码如何写呢?

首先我们找自己的代码中,哪里走get ,哪里走set

  • 走get:获取属性的时候
  • 走set:设置属性的时候

在这里插入图片描述

在这里插入图片描述
至此,css 的扩展也就结束啦~

当然,如果你想让自己写的wh 属性不加单位,可以在静态属性 $.cssNumber 中添加以下

1
2
// html
$.cssNumber['th'] = true;

复盘总结

  • Jquerythis 的处理
  • 对未来的扩展:静态属性
  • 链式调用的思想
  • 封装的思想

碎碎念

这个教程我看了好久,但是就是没时间整理,最近比较堕落哈哈哈
工作了快小半年了,有点迷茫,哈哈但是这是必经之路吧,越早经历这些或许以后的路就越好走,太顺风顺水也没啥意思(我也想顺风顺水呀呜呜呜),只是小矫情罢了~

如果对你有帮助的话,来个赞👍吧~辛苦您啦

在这里插入图片描述


代码

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// myJquery.js
class myJquery {
constructor(ele, root) {
if (typeof root === "undefined") {
this.prevObject = [document];
} else {
this.prevObject = root;
}
if (typeof ele === "string") {
// 如果传入的是字符串的话
let arr = document.querySelectorAll(ele);
this.addElement(arr);
} else {
// 传入的如果是原生节点
if (typeof ele.length == "undefined") {
this[0] = ele;
this.length = 1;
} else {
// 传入的是原生节点数组
this.addElement(arr);
}
}
}
addElement(arr) {
// 把每个节点挂到this上
// 这里的 this 就是实例化的对象,就是下面 $ 函数返回的实例化对象
arr.forEach((item, index) => {
this[index] = item;
});
this.length = arr.length;
}
eq(index) {
return new myJquery(this[index], this);
}
end() {
return this.prevObject;
}
click(fn) {
// 绑定事件
for (let i = 0; i < this.length; i++) {
this[i].addEventListener("click", fn, false);
}
}
on(eventName, fn) {
let reg = /\s+/g;
// 去掉开头和结尾的空格,并且把中间的空格代替成一个空格,方便后续处理
eventName = eventName.trimStart().trimEnd().replace(reg, " ");
let arr = eventName.split(" ");
// console.log(arr);
for (let i = 0; i < this.length; i++) {
for (let j = 0; j < arr.length; j++) {
this[i].addEventListener(arr[j], fn, false);
}
}
}
css(...arg) {
// 对于形参个数不固定,可以用 rest 参数来处理
if (arg.length === 1) {
if (typeof arg[0] === "string") {
// 方式一:获取属性
// ie6 的话可以用 currentStyle 解决兼容性问题
if (arg[0] in $.cssHooks) {
// 只返回第一个元素的宽高
return $.cssHooks[arg[0]].get(this[0]);
}
return window.getComputedStyle(this[0], null)[arg[0]];
} else {
// 方式三:设置属性
for (let i = 0; i < this.length; i++) {
for (let j in arg[0]) {
this.setStyle(this[i], j, arg[0][j]);
}
}
}
} else {
// 方式二:设置属性
for (let i = 0; i < this.length; i++) {
this.setStyle(this[i], arg[0], arg[1]);
}
}
}
setStyle(node, attr, val) {
if (typeof val === "number" && !(attr in $.cssNumber)) {
val = val + "px";
}
if (attr in $.cssHooks) {
$.cssHooks[attr].set(node, val);
} else {
node.style[attr] = val;
}
}
}

function $(ele) {
return new myJquery(ele);
}

$.cssNumber = {
animationIterationCount: true,
columnCount: true,
fillOpacity: true,
flexGrow: true,
flexShrink: true,
fontWeight: true,
gridArea: true,
gridColumn: true,
gridColumnEnd: true,
gridColumnStart: true,
gridRow: true,
gridRowEnd: true,
gridRowStart: true,
lineHeight: true,
opacity: true,
order: true,
orphans: true,
widows: true,
zIndex: true,
zoom: true,
};

$.cssHooks = {};