DOM、jQuery、事件

963 阅读6分钟

博文整理自饥人谷课程

博文整理自网道(WangDoc.com)

DOM

Document Object Model 文档对象模型

网页其实是一棵树,JS如何操作这棵树,首先JS不能直接操作网页,而是用document操作网页,即浏览器往window上加了document,就可以获取整个网页了。

window.document

获取元素

// 最便捷获取元素的方法
<input type="text" id="kw">

window.kw  // 可获取这个 input 元素
kw  // 省略 window 也可直接获取这个 input 元素
document.querySelector('div>span:nth-child(2)')
document.querySelectorAll('div>span:nth-child(2)')[0]
// 其他获取元素的方法(兼容IE)
document.getElementById('kw')
document.getElementsByTagName('div')[0]  // 所有标签名为 div
document.getElementsByClassName('red')[0] // 所有类名为 red
// 获取特定元素
document.documentElement // 获取 html 元素
document.head  // 获取 head 元素
document.body  // 获取 body 元素
window.onclick = () => {console.log('hi')}  // 获取窗口 添加点击事件
if(document.all){console.log('ie 浏览器')}  // 判别 ie 浏览器
else{console.log('其他浏览器')}
document.all    // 是第六个 falsy 值

元素的6层原型链

let div = document.getElementsByTagName('div')[9]
console.dir(div)  // 打印出 div 的结构

div.__proto__ === HTMLDivElement.prototype
HTMLDivElement.prototype.__proto__ === HTMLElement.prototype
HTMLElement.prototype.__proto__ === Element.prototype
Element.prototype.__proto__ === Node.prototype
Node.prototype.__proto__ === EventTarget.prototype
EventTarget.prototype__proto__ === Object.prototype
Node.nodeType // 区分不同类型的节点
// 节点Node包括:元素、文本、注释、文档、文档片段

image.png

节点的增删改查

// 增
// 创建的标签默认处于 JS线程 中
let div1 = document.createElement('div') // 创建一个 标签 节点
let text1 = document.createTextNode('你好')   // 创建一个 文本 节点
div1.appendChild(text1)   // 标签里插入文本 方法1
div1.innerText = '你好'    // 标签里插入文本 方法2
div1.textContent = '你好'  // 标签里插入文本 方法3
xx div1.appendChild('你好') // 不可以用
// 节点插入到 页面 中
document.body.appendChild(div)
已在页面中的元素.appendChild(div)
// 删
parentNode.removeChild(childNode)
childNode.remove()
// 改
div.className = 'red blue' // 改 class
div.classList.add('red')
div2.setAttribute('data-x', 'test')  // 设置 div2 的data-x属性值为test
div2.getAttribute('data-x')  // test
div2.dataset.x               // test

div.innerText = 'xxx'     // 改文本内容
div.textContent = 'xxx'   // 改文本内容
// 改事件处理函数
div.onclick = null // 默认为空
div.onclick = fn
// 点击div时,浏览器调用fn.call(div, event) 
// div 会被当做 this ;event包含了点击事件的所有信息
div.addEventListener  // onclick 升级版
// 查
node.parentNode             // 查爸爸
node.parentElement          // 查爸爸
node.parentNode.parentNode  // 查爷爷
node.childNodes             // 查子代
node.parentNode.childNodes  // 查兄弟姐妹

DOM跨线程操作

浏览器功能内容备注
渲染引擎渲染HTML和CSS
不能操作JS
只能操作页面
线程1
JS引擎执行JS
不能操作页面
线程2
document.body.appendChild(div1)
// 理论上只能添加到内存中,JS 是如何添加到页面中的?

跨线程通信

当浏览器发现 JS 在body里面加了div1对象

浏览器就会通知渲染引擎在页面里也新增一个div1元素

新增的div元素所有属性都照抄div1对象

image.png

对象风格 封装DOM操作

创建window.dom全局对象

// index.html
<body>
<div id="test"></div>
<script src="dom.js"></script>
<script src="main.js"></script>
</body>
// dom.js 封装dom
window.dom = {
  create(string){
    const container = document.createElement('template');
    container.innnerHTML = string.trim();
    return container.content.firstChild;
  },
  after(node, node2){
    node.parentNode.insertBefore(node2, node.nextSibling);
  }
};
// 增
const div = dom.create(`<div>newDiv</div>`)  //创建节点
dom.after(node, node2);  // 用于新增弟弟
dom.append(parent, child)  // 用于新增儿子

// 删
dom.remove(node)  // 删除节点

// 改
dom.attr(node, 'title', ?)  // 用于读写属性
dom.text(node, ?) // 用于读写文本内容
dom.on(node, 'click', fn) // 用于添加事件监听

// 查
dom.find('选择器')  // 用于获取标签或标签们
dom.siblings(node)  // 用于获取兄弟姐妹元素
dom.each(nodes, fn)  // 用于遍历所有节点

链式风格 封装DOM操作

jQuery于2006年发布,是目前最长寿的库,也是世界上使用最广泛的库,全球80%网站在用

jQuery函数实现过程

// index.html
<body>
 <div class="test">你好</div>
 <script src="jquery.js"></script>
 <script src="main.js"></script>
</body>

一、api做为中间对象

jQuery核心代码:根据接收的选择器,得到一些元素,但它却不返回这些元素。相反,它返回一个对象api,这个api对象有方法可以操作元素。

// jquery.js
window.jQuery = function(selector){  // window.jQuery() 是全局函数
  const elements = document.querySelectorAll(selector)
  // api 对象可以操作 elements
  const api = {
    "addClass":function(){  // api对象的 key 为addClass、value 为函数,简写为下一行
    addClass(className){  //  闭包: addClass函数访问外部变量 elements          
      for(let i=0; i<elements.length; i++){  // 拿到className 遍历elements 
        elements[i].classList.add(className)   // 每个element都添加className
      }
      return api  // 这是 addClass函数的 return
    }
  }
  return api  // 这是 jQuery函数的 return
}

链式操作:用api对象调用addClass函数,这个函数返回api,于是就可以继续调用addClass函数,以上的操作是基于 addClass函数定义里面 return的是api对象。

// main.js
// jQuery是全局变量,省略window.
const api = jQuery(.test) // 先获取到 .test 对应的所有元素,jQuery 不返回元素们,而返回 api 对象
api.addClass('red')     // 遍历所有刚才获取的元素,添加 .red 再添加 .blue

// addClass()函数的返回值为 api 也就可以继续 .addClass()函数
api.addClass('red').addClass('blue') // 链式操作 

// 输出结果为 class为test的元素属性 都添加上了 <div class="test red blue">

二、删掉 api

函数如果用对象来调用,那么这个函数里的this就是调用这个函数的对象。

obj.fn(p1)  // 函数里的 this 就是 obj
// 等价于
obj.fn.call(obj, p1)

也就是说步骤一里的addClass里面的this就是api

const api = jQuery(.test)
api.addClass('red')     // this 就是 api

既然如此,就可以return this 而不 return api,这样做的目的是删掉根本没用到的中间变量api,简化步骤一中的如下代码:

// jquery.js
window.jQuery = function(selector){  
  const elements = document.querySelectorAll(selector)
  // api 对象可以操作 elements
  const api = {
    addClass(className){        
      for(let i=0; i<elements.length; i++){  
        elements[i].classList.add(className)   
      }
      return this  // 原来的 api 改为 this
    }
  }
  return api  // 这是 jQuery函数的 return
}

再进一步的,声明了api,又return了这个对象,就可以把根本没用到的api删掉,直接return这个不具名的对象:

// jquery.js
window.jQuery = function(selector){  
  const elements = document.querySelectorAll(selector)
  // api 对象可以操作 elements
  return {
    addClass(className){        
      for(let i=0; i<elements.length; i++){  
        elements[i].classList.add(className)   
      }
      return this  // 原来的 api 改为 this
    }
  }
}

使用jQuery时,就改为:

jQuery('test').addClass('red')
              .addClass('blue')
              .addClass('green')              

jQuery对象

jQuery是一个不需要加new的构造函数,jQuery对象(api)代指jQuery函数构造出来的对象,

Object对象 表示 Object 构造出的对象
Array数组对象 表示 Array 构造出的对象
Function函数对象 表示 Function 构造出来的对象

实现find函数

// index.html
<body>
 <div class="test1">
  你好1
  <div class="child">child1</child>
  <div class="child">child2</child>
  <div class="child">child3</child>
 </div>
 <script src="jquery.js"></script>
 <script src="main.js"></script>
</body>
// jquery.js
window.jQuery = function(selector){  
  const elements = document.querySelectorAll(selector)
  // api 对象可以操作 elements
  return {
    addClass(className){        
      for(let i=0; i<elements.length; i++){  
        elements[i].classList.add(className)   
      }
      return this  // 原来的 api 改为 this
    },
    find(selector){
      let array = []  // 新数组储存新查找的元素
      for(let i=0; i<elements.length; i++){
        const elements2 = elements[i].querySelectorAll(selector)
      }
    }
  }
}
const x1 = jQuery('.test1').find('.child')
console.log(x1)

事件

事件的本质是程序各个组成部分之间的一种通信方式,DOM支持大量的事件。DOM节点的事件操作(监听和触发),都定义在EventTarget接口。

JS不支持事件,而是DOM事件,只是浏览器的功能,不是JS功能,JS只是调用了DOM提供的addEventListener而已。

EventTarget接口

所有节点对象也都部署了这个接口,还有一些需要事件通信的浏览器内置对象也部署了这个接口。比如:小黄人XMLHttpRequest

EventTarget接口提供三个实例方法:

实例方法内容备注
addEventListener()绑定事件的监听函数事件绑定API
removeEventListener()移除事件的监听函数
dispatchEvent()触发事件

监听函数

浏览器的事件模型:就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。这是事件驱动编程模式的主要编程方式。

JS有三种为事件绑定监听函数的方法:

方法内容备注
HTML 的 on- 属性
(不推荐使用)
元素的事件监听属性
onclick 表示click事件的监听代码
属性的值为将会执行的代码
使用这个方法指定的监听代码,只会在冒泡阶段触发
元素节点的事件属性
(不推荐使用)
属性值为函数名使用这个方法指定的监听函数,也是只会在冒泡阶段触发
addEventListener()
(推荐使用)
用来为该节点定义事件的监听函数所有DOM节点实例和其他对象window、XMLHttpRequest都有此方法
// on- 属性
<div onclick="console.log(2)">
  <button onclick="console.log(1)">点击</button>
</div>
// 点击结果是先输出1,再输出2,即事件从子元素开始冒泡到父元素

<Element onclick="doSomething()">
// 属性的值是将会执行的代码,而不是一个函数
// 等价于
el.setAttribute('onclick', 'doSomething()');
// 元素节点的事件属性
window.onload = doSomething;

div.onclick = function (event) {
  console.log('触发事件');
};
// 在当前节点或对象上定义特定事件的监听函数,事件发生就执行
EventTarget.addEventListener('load', doSomething, false)
// 接受3个参数,'load'事件名称,'doSomething'监听函数,默认为 false 
// 如果设为 true ,表示监听函数将在捕获阶段(capture)触发

事件传播

一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

  • 第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。
  • 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
  • 第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。

这种三阶段的传播模型,使得同一个事件会在多个节点上触发。

事件委托

也叫事件代理。由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方法叫做事件的委托

Event对象

事件发生以后,会产生一个事件对象,作为参数传给监听函数。浏览器原生提供一个Event对象,所有的事件都是这个对象的实例,或者说继承了Event.prototype对象。

Event对象本身就是一个构造函数,可以用来生成新的实例。

事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意事件都有两个与事件相关的节点,一个是事件的原始触发节点(Event.target),另一个是事件当前正在通过的节点(Event.currentTarget)。前者通常是后者的后代节点。

标题内容备注
Event.currentTarget属性
(程序员监听的元素)
返回事件当前所在的节点随着事件传播,该属性值会变
Event.target属性
(用户操作的元素)
返回原始触发事件的节点属性不会随事件传播而改变
<div><span>文字</span></div>
// 用户点击文字,e.target就是span    e.currentTarget就是div