总结Ⅳ-框架篇之DOM

204 阅读5分钟

DOM

1. 前端中的DOM事件流

HTML中与javascript交互是通过事件驱动来实现的,例如鼠标点击事件onclick、页面的滚动事件onscroll等等,可以向文档或者文档中的元素添加事件侦听器来预订事件。想要知道这些事件是在什么时候进行调用的,就需要了解一下“事件流”的概念。


什么是事件流

事件流描述的就是从页面中接收事件的顺序。 而早期的IE和Netscape提出了完全相反的事件流概念,IE事件流是事件冒泡,而Netscape的事件流就是事件捕获。


事件冒泡&事件捕获

IE提出的事件流是事件冒泡,即从下至上,从目标触发的元素逐级向上传播,直到window对象。

image.png

而Netscape的事件流就是事件捕获,即从document逐级向下传播到目标元素。由于IE低版本浏览器不支持,所以很少使用事件捕获。

image.png 后来ECMAScript在DOM2中对事件流进行了进一步规范,基本上就是上述二者的结合。

DOM2级事件规定的事件流包括三个阶段:

(1)事件捕获阶段
(2)处于目标阶段
(3)事件冒泡阶段

image.png


事件处理 image.png 其中DOM0级只支持冒泡阶段,DOM1级事件处理标准中并没有定义相关的内容,所以没有所谓的DOM1事件处理;DOM3级事件在DOM2级事件的基础上添加了更多的事件类型。

重点关注DOM2,其中定义了两个方法:

  1. addEventListener() ---添加事件侦听器
  2. removeEventListener() ---删除事件侦听器

函数均有3个参数, 第一个参数是要处理的事件名 第二个参数是作为事件处理程序的函数 第三个参数是一个boolean值,默认false表示使用冒泡机制,true表示捕获机制。代码举例:btn.addEventListener("click",hello,false);

有时候我们需要点击事件不再继续向上冒泡,需要加上stopPropagation函数,阻止程序冒泡。

2. 事件委托

如果有多个DOM节点需要监听事件的情况下,给每个DOM绑定监听函数,会极大的影响页面的性能,因为我们通过事件委托来进行优化,事件委托利用的就是冒泡的原理。

总结:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。

举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。

简陋版:

ul.addEventListener('click',function(e){
    if(e.target.tagName.toLowerCase()==='li'){
        console.log('点击了li')
    }
})

Bug:如果li里有span(如下图),我点击了span, e.target就是span元素,不会触发函数。但是实际上是应该触发的,因为span也在li里,点击span就是点击了li

image.png

优化:针对 如果点击了li里边的子代元素的情况 进行判断。拿到点击的元素e.target ,只要它不是li,就循环往上一直查找它的父元素,看它的父元素是不是li,直到触顶到了ul。如果在其中某一次找到了li,就退出循环,执行函数;如果直到循环到ul都没有li,就说明不需要触发函数

function delegate(element,eventType,selector,fn){
    element.addEventListener(eventType,e=>{
        let el=e.target
        while(!el.matches(selector)){
            if(el===element){
                el=null
                break
            }
            el=el.parentNode
        }
        el && fn.call(el,e,el)
    })
    return element
}

两种获取目标元素的方式,target和currentTarget,上述代码使用了target,那么他们有什么区别呢:

  • target返回触发事件的元素,不一定是绑定事件的元素
  • currentTarget返回的是绑定事件的元素

总结一下事件委托的优点:

  1. 提高性能:每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少。
  2. 动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以一样具有和其他元素一样的事件。

3. mouseover和mouseenter的区别

mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout

mouseenter:当鼠标移入元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave

4. 用 mouse 事件写一个可拖曳的 div

<div id="xxx"></div>

因为要移动div,所以必定会改变它的位置,使用绝对定位,之后改变left top

div{
  border: 1px solid red;
  position: absolute;
  top: 0;
  left: 0;
  width: 100px;
  height: 100px;
}

*{margin:0; padding: 0;}

监听三个事件:mousedown 鼠标按下,标志着开始拖动了。mouseup 鼠标弹上去,拖动结束。mousemove 移动鼠标,改变div位置。

所以首先需要一个拖动标志,鼠标按下置为true,鼠标弹上去置为false,只有为true的时候,才能拖动。

var dragging = false
var position = null

xxx.addEventListener('mousedown',function(e){
  dragging = true
  position = [e.clientX, e.clientY]//记录初始位置
})

document.addEventListener('mousemove', function(e){
  if(dragging === false){return}
  const x = e.clientX
  const y = e.clientY
  const deltaX = x - position[0]//计算出X移动距离
  const deltaY = y - position[1]//计算出Y移动距离
  const left = parseInt(xxx.style.left || 0)//xxx.style.left自动带px,所以需要parseInt
  const top = parseInt(xxx.style.top || 0)
  xxx.style.left = left + deltaX + 'px'//别忘了加单位
  xxx.style.top = top + deltaY + 'px'
  position = [x, y]
})
document.addEventListener('mouseup', function(e){
  dragging = false
})

mousemove和mouseup事件要绑定到document上,如果监听了div,如果鼠标移动太快,div跟不上就掉下去了。

根据上一次position和当前位置计算出距离,把属性left和top也增加同样距离即可。这里的e.clientX是数字,所以deltaX也是数字,但是 xxx.style.left是字符串'1px' ,所以要把上一次的left先转换成数字parseInt,还要加一个保底值0