用jQuery风格重新封装DOM
要说的话
本文主要用来理解jQuery的闭包和链式操作核心思想,以及一些函数的实现。要知道是怎么来的。–资料来源于饥人谷
推荐阅读文章
阮一峰jQuery设计思想。本文源代码
为什么学习jQuery
jQuery有多牛X
它是目前前端最长寿的库,2006年发布
它是世界上使用最广泛的库,到2020年11月也有全球80%的网站在用
我应该学习设计模式吗?
- 设计模式不是用来学的
你看了这些代码,但你并不知道这代码用来解决什么问题等于看了白看 - 设计模式是用来总结的
你只管去写代码,把你的代码尽量写好,不断重写。
总结你的代码,把写得好的地方抽象出来,看看符合哪个设计模式,你就可以告诉别人你用到了这几个设计模式,显得你特别高端
有人说不用学jQuery
- 真相
jQuery这么简单、经典的库为什么不学?
通过 jQuery可以学会很多封装技巧,为什么不学?
连jQuery都理解不了, Vue / React肯定学不好
- 学习路线
理解 jQuery原理
使用jQuery做一两个项目
学Vue / React,找工作要紧
设计模式?
- jQuery 用到了哪些设计模式
不用new的构造函数,这个模式没有专门的名字
$(支持多种参数),这个模式叫做重载
用闭包隐藏细节,这个模式没有专门的名字
$div.text()即可读也可写,getter / setter
$.fn是$.prototype 的别名,这叫别名
jQuery针对不同浏览器使用不同代码,这叫适配器
- 设计模式是啥
设计模式就是对通用代码取个名字
理解jQuery闭包和链式风格
链式风格也叫 jQuery风格
window.jQuery()是我们提供的全局函数
- 特殊函数jQuery
jQuery(选择器)用于获取对应的元素,但它却不返回这些元素,相反,它返回一个对象,称为jQuery构造出来的对象简称jQuery对象
(jQuery对象,不是说「jQuery 这个对象」,一定要记清楚,jQuery对象代指jQuery 函数构造出来的对象,口头约定)
这个对象可以操作对应的元素
听不懂?直接写代码!
上代码理解链式
在html里
<div class="test">
你好1
</div>
<div class="test">
你好2
</div>
<div class="test">
你好3
</div>
<script src="jQuery.js"></script>
<script src="main.js"></script>
jQuery.js
//核心思想:jQuery接受一个选择器,然后根据选择器得到一些元素,然后返回一个对象,这个对象有个方法去操作这个元素
window.jQuery = function (selector) {
//jquery获取elements后,声明个api,这个api可以操作这个elements
const elements = document.querySelectorAll(selector);
//elements就是选择器对应的元素
//---api 可以操作elements---
const api = {
//---闭包函数访问外部的变量---。(addClass访问elements,elements是外部的变量)
addClass(className) {
for (let i = 0; i < elements.length; i++) {
elements[i].classList.add(className);
}
return api;//返回api对象方便继续调用---链式---
},
};
return api; //jQuery返回一个可以操作elements的api对象。
};
main.js
const api = jQuery('.test')//不返回元素们,返回api对象
api.addClass('red')//遍历所有刚才获取的元素,添加 .red
api.addClass('red').addClass('blue')//链式操作
//用api调了addClass函数,这个函数返回了前面的api,于是可以继续调用addClass。只需要return 那个对象
深入链式
先来看一下this
obj.fn(p1) //函数里的this就是obj
obj.fn.call(obj,p1)
main.js里意思是api就是this。那可以直接ruturn this。
jQuery里代码简化
window.jQuery = function (selector) {
//jquery获取elements后,声明个api,这个api可以操作这个elements
const elements = document.querySelectorAll(selector);
//elements就是选择器对应的元素
//---api 可以操作elements---
return {
//---闭包:函数访问外部的变量---。(addClass访问elements,elements是外部的变量)
addClass(className) {
for (let i = 0; i < elements.length; i++) {
elements[i].classList.add(className);
}
return this;
},
};
};
main.js代码简化
//可以不声明api。简写成
jQuery(".test")
.addClass('red')
.addClass('blue')
.addClass('green')
示例闭包和链式风格核心思想:
jQuery提供一个函数,这个函数接受一个css选择器,这个选择器会获取到这些元素,但不会反回elements这些元素,会返回一个对象,对象里可能有一些方法(函数),这些方法会来操作这些元素(用闭包维持elements)。这个函数能猜到你在调用addClass的时候,肯定是通过得到的api的,所以会return this,希望把api.addClass中.前面的东西作为addClass的返回值(传的什么就是this),这样的话就相当于api从前面传递到了后面,传到了后面又可以.addClass…(链式操作))
jQuery是构造函数吗?
- 是
因为jQuery函数确实构造出了一个对象
- 不是
因为不需要写new jQuery()就能构造一个对象,以前讲的构造函数都要结合new才行
- 结论
jQuery是一个不需要加new的构造函数
jQuery不是常规意义上的构造函数
这是因为jQuery 用了一些技巧
术语
- 举例
Object是个函数,
Object对象表示 Object构造出的对象。
Array是个函数,
Array对象/数组对象表示Array构造出来的对象。
Function是个函数,
Function对象/函数对象表示Function构造出来的对象。
一些实现
实现find和返回新的api
实现find
jQuery.js
window.jQuery = function (selector) {
const elements = document.querySelectorAll(selector)
//---api 可以操作elements---
return {
//---闭包:函数访问外部的变量---。(addClass访问elements,elements是外部的变量)
addClass(className) {
for (let i = 0; i < elements.length; i++) {
elements[i].classList.add(className);
}
return this;
},
find(selector) {
//找到当前元素所有匹配选择器的元素,然后把元素放到数组里,在返回这个数组
let array = []; //思路:我要在test1里找child,可以通过elements去找,但elements是多个元素,可以认为是一个数组,数组是不能querySelectorAll的。只能先声明一个临时的数组,用这个数组去储存新的child元素
for (let i = 0; i < elements.length; i++) {
const elements2 = Array.from(elements[i].querySelectorAll(selector)); //concat是个伪数组,要变成数组。用之前的空数组连接上新的元素,然后把新的元素的到的新数组在放回array。
array = array.concat(elements2);
}
return array;
},
main.js
const x1 = jQuery('.test').find('.child')//获取.test1,查找类为child的元素
console.log(x1)//x1是个数组,不是一个纯函数,没有链式。
- 遇到的问题:能不能写成链式。
按照理解链式来看,就算 return this 也是return{}对象,这个对象操作的是elements,就不能操作array。如jQuery(’.test’).find(’.child’).addClass(‘red’)现在return this,加到的是test上,find前面的api是test。
写成链式,返回新的api对象。
第一步,find函数里return一个新的api,靠jQuery构造出来
const newApi = jQuery(array);
//如果直接用同一个api,那每次得到新的元素都会污染之前的api,所以要得到一个新的对象
return newApi;
//return jQuery(array)//两行代码简写
第二步,jQuery不能只接受一个选择器,还得接受一个数组:把selector改成selectorOrArray
第三步,声明elements值为空,然后根据选择器是string还是Array分别赋予不同的值,然后返回一个api去操作它(重载)
window.jQuery = function (selectorOrArray) {
//重载
let elements
if (typeof selectorOrArray === 'string') {
elements = document.querySelectorAll(selectorOrArray);
} else if (selectorOrArray instanceof Array) {
//对象用instanceof
elements = selectorOrArray;
}
在main.js里试试
//去变量
jQuery('.test')
.find('.child')
.addClass('red')
.addClass('blue')
.addClass('green')
最后结果,链式成功
那想返回test再操作呢
要用到end返回
实现end
jQuery.js
oldApi: selectorOrArray.oldApi,
···
find(selecor) {
...
array.oldApi = this; //这里this就是旧api
//把oldApi放到了数组身上并没有放到api身上,插入最上面的代码这样api也有oldApi
return jQuery(array);
},
end() {
return this.oldApi// 这里this就是当前的新的api
},
main.js
//命个名方便理解
const api1 = jQuery('.test')
const api2 = api1.find('.child').addClass('red').addClass('blue').addClass('green')
const oldApi = api2.end().addClass('yellow')
end()实现
实现each
jQuery.js
each(fn) {
//遍历当前所有元素
for (let i = 0; i < elements.length; i++) {
fn.call(null, elements[i], i);
}
return this//this就是当前api
},
main.js
const x = jQuery('.test')
.find('.child')
x.each((div) => console.log(div))
实现each()
实现爸爸
jQuery.js
//实现parent
//parent不需要参数,直接什么什么.parent
parent(){
//获取对应元素的爸爸
const array = []
this.each((node)=>{//每一个元素我们要得到一个节点
if(array.indexOf(node.parentNode) === -1){//push的时候判断一下,不在里面就是等于-1
array.push(node.parentNode)//把这个节点的爸爸放到数组里
}
})
return jQuery(array)
//array没有什么可操作性,所以要封装一个操纵数组的对象,jQuery会返回一个对象,这个对象会操作这些爸爸
},
print(){//实现print方法把当前elements元素打印出来
console.log(elements)
},
main.js
//爸爸
const x =jQuery('.test')
x.parent().print()
//直接用获取到的api去print一下,它就会操作这些爸爸
实现儿子
jQuery.js
children(){
const array = []//准备好一个数组
this.each((node)=>{
array.push(...node.children)//...是把里面的东西拆开,第一个元素当做第一个参数,第二个元素当做第二个参数。
//等价于(node.children[0], node.children[1],node.children[2]...等等)
})//遍历刚才的元素,
return jQuery(array)
},
main.js
const x =jQuery('.test')
x.children().print()
//获取children并打印出来
jQuery的增删改查…
链式风格-查
-
jQuery(’#xxx’)返回值并不是元素,而是一个api对象
-
jQuery(’#xxx’).find (’.red’)查找#xxx里的.red元素
-
jQuery(’#xxx’).parent()获取爸爸
-
jQuery(’#xxx’).children()获取儿子
-
jQuery(’#xxx’).siblings()获取兄弟
-
jQuery(’#xxx’).index()获取排行老几(从O开始)
-
jQuery(’#xxx’).next()获取弟弟
-
jQuery(’#xxx’).prev()获取哥哥
-
jQuery(’.red’).each(fn)遍历并对每个元素执行fn
命名风格
嫌弃jQuery太长了?
- 用$替代jQuery
代码中所有$开头的变量,都是jQuery 对象
这是约定,除非特殊说明
window.$ = window.jQuery = function(selectorOrArray){
//等号赋值从右忘左执行,也可以在最后写成window.$ = window.jQuery
...
}
- 下面的代码令人误解
const div = $(‘div#test’)
我们会误以为div是一个DOM
实际上div是 jQuery构造的api对象
怎么避免这种误解呢?
- 改成这样
const $div = $(‘div#test’)
$div.appendChild不存在,因为它不是DOM对象
$div.find存在,因为它是jQuery对象
链式风格·删
-
$div.remove()
-
$div.empty()
链式风格·增
-
$ (‘body’)获取document.body
-
$(‘body’ ).append($ (’<div>1</div>’))添加小儿子
-
$(‘body’).append(’<div>1</div>’)更方便
-
$(‘body’).prepend(div或$div)添加大儿子
-
$(’#test’).after(div或 $div)添个弟弟
-
$ (’#test’).before(div或 $div)添个哥哥
增
$ ('<div><span>1</span></div>')
返回值并不是新增的元素,而是api对象
$ ('<div><span>1</span></div>').appendTo(...)
appendTo可以把新增的元素放到另一个元素里
- 这是一种什么感觉
就感觉DOM是不可见的,你不需要知道DOM的任何细节,只需要使用简洁的API即可
一个好的封装,能让使用者完全不知道内部细节
这是通过闭包实现的
我就是想知道细节咋办
- 举例1
const $div = $(‘div#test’)
$div并不是 DOM对象,而是jQuery构造的api对象
我现在就是想从 $div得到div元素,行不行?
- 满足你
$div.get(0)获取第0个元素//div
$div.get(1)获取第1个元素// undefined
$div.get(2)获取第2个元素// undefined
$div.size()得到元素的个数
$div.length也可以得到元素的个数
$div.get()获取所有元素组成的数组// [div]
- 举例2
const $div= $(’.red’) //假设有3个div.red
$div不是DOM对象们,而是jQuery构造的api对象
我现在就是想从$div得到3个div元素,行不行?
- 满足你
$div.get(0)获取第0个元素//div
$div.get(1)获取第1个元素// div
$div.get(2)获取第2个元素//div
$div.size得到元素的个数
$div.length 也可以得到元素的个数
$div.get()获取所有元素组成的数组//[div,div,div]
链式风格·改
-
$div.text(?)读写文本内容
-
$div.html(?)读写HTML内容
-
$div.attr(‘title’,?)读写属性
-
Sdiv.css({color: 'red"})读写style // $div.style更好
-
$div.addClass(‘blue’) / removeClass / hasClass
-
$div.on(‘click’, fn)
-
$div.off(‘click’, fn)
- 注意
$div可能对应了多个div元素
使用原型
把共用属性(函数)全都放到$.prototype
$.fn = $.prototype//名字太长不爽
然后让api.__proto__指向$.fn
--continueapi一开始就把原型链链上,共有属性不用操心了,特有属性就放到自己身上。节约了内存。
注意的问题:变量访问不到了,用一个api作为桥梁,用this去访问api。constructor记得补上