浏览器 DOM 事件&事件绑定

190 阅读8分钟

DOM事件

总结

  • 事件是浏览器赋予元素的默认行为

    • 鼠标:click

    • 键盘:keydown

    • 手指:touchstart

    • 表单:submit、reset、select、change、input

    • 资源:load、error

    • CSS3动画事件:transitionend transition动画结束、transitionstart transition动画开始、transitionrun transition动画运行中

    • 视图:resize 元素(浏览器)大小改变、scroll 滚动条滚动

  • 事件绑定:给元素默认的事件行为绑定方法

    • DOM0级事件

      • 绑定语法:document.body.onclick=function(){}

      • 移除绑定:document.body.onclick=null

      • 多绑会覆盖

    • DOM2级事件绑定采用事件池机制

      • 绑定语法:[元素].addEventListener([事件],[方法],[捕获/冒泡]) document.body.addEventListener('click',fn1,false)

      • 移除绑定:[元素].removeEventListener([事件],[方法],[捕获/冒泡])

        • document.body.removeEventListener('click',fn1,false)
      • 原理:每一个DOM元素都会基于__proto__,EventTarget.prototype上的addEventListener/removeEventListener

image.png

  • 事件对象

    • 触发当前元素的某个事件行为绑定的方法时,会默认传递一个事件对象(实参)

    • 鼠标

      • ev.preventDefault() / ev.returnValue=false 阻止默认行为

      • ev.stopPropagation() / ev.cancelBubble=true 阻止冒泡传播

0. 什么是 DOM

  • DOM(Document Object Model,文档对象模型)是针对HTML文档和XML文档的一个API

1. 什么是事件?

click 事件过程

  • 用户点击该按钮 => 浏览器就检测btn.onclick有值 => 执行btn.onclick.call(btn,event)

    • 第一个指向调用当前方法的对象,也就是this

      • 指定的 this 值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象
    • 另一个参数则是事件对象 event

      • 可以通过 arguments[0] 来访问

      • 包含了事件相关的所有信息

2. 事件绑定

总结

var btn = document.getElementById('btn')

btn.onClick = () => {
  console.log('我是DOM0级事件处理程序')
}
btn.onClick = null

btn.addEventListener(
  'click',
  () => {
    console.log('我是DOM2级事件处理程序')
  },
  false
)
btn.removeEventListener('click', handler, false)

btn.attachEvent('onclick', () => {
  console.log('我是IE事件处理程序')
})
btn.detachEvent('onclicn', handler)
  • DOM2级的好处是可以添加多个事件处理程序;DOM0对每个事件只支持一个事件处理程序

  • 通过DOM2添加的匿名函数无法移除,上面写的例子就移除不了,addEventListenerremoveEventListener的handler必须同名

  • 作用域:DOM0的handler会在所属元素的作用域内运行,IE的handler会在全局作用域运行,this === window

  • 触发顺序:添加多个事件时,DOM2会按照添加顺序执行,IE会以相反的顺序执行;

  • 跨浏览器的事件处理程序

var EventUtil = {
  // element是当前元素,可以通过getElementById(id)获取
  // type 是事件类型,一般是click ,也有可能是鼠标、焦点、滚轮事件等等
  // handle 事件处理函数
  addHandler: (element, type, handler) => {
    // 先检测是否存在DOM2级方法,再检测IE的方法,最后是DOM0级方法(一般不会到这)
    if (element.addEventListener) {
      // 第三个参数false表示冒泡阶段
      element.addEventListener(type, handler, false)
    } else if (element.attachEvent) {
      element.attachEvent(`on${type}`, handler)
    } else {
      element[`on${type}`] = handler
    }
  },

  removeHandler: (element, type, handler) => {
    if (element.removeEventListener) {
      // 第三个参数false表示冒泡阶段
      element.removeEventListener(type, handler, false)
    } else if (element.detachEvent) {
      element.detachEvent(`on${type}`, handler)
    } else {
      element[`on${type}`] = null
    }
  },
}

// 获取元素
var btn = document.getElementById('btn')
// 定义handler
var handler = function (e) {
  console.log('我被点击了')
}
// 监听事件
EventUtil.addHandler(btn, 'click', handler)
// 移除事件监听
// EventUtil.removeHandler(button1, 'click', clickEvent)
  • 给元素默认的事件行为绑定方法

DOM0级事件

  • 绑定语法:[元素].on[事件]=[函数]

    • document.body.onclick=function(){}
  • 移除绑定:赋值为null或者其他非函数值皆可

    • document.body.onclick=null
  • 原理:给这些每一个DOM元素对象上代表事件的私有属性(onxxx)赋值

    • 如果没有对应事件的私有属性值(例如:DOMContentLoaded)则无法基于这种办法实现事件绑定

    • 只能给当前元素的某个事件行为绑定一个方法,绑定多个方法,最后一个操作会覆盖以往的

    • 好处是执行效率快,而且开发者使用起来方便

  • event 是作 window 对象的一个属性而存在的 => 访问事件对象需要通过 window.event

btn.onclick = function(){
   console.log(window.event)  // IE中事件对象    
}
  • 一个对象绑定多个函数
function fn1(){
    // do something
}
function fn2(){
    // do something
}
btn.onclick = function(e){
  fn1.call(this.xxx)
  fn2.call(this.yyy)
}

DOM2级事件

  • true 代表在捕获阶段调用事件处理程序,false 表示在冒泡阶段调用事件处理程序,默认为 false

  • 绑定语法:[元素].addEventListener([事件名],[方法],[捕获/冒泡])

    • document.body.addEventListener('click',fn1,false)
  • 移除绑定:[元素].removeEventListener([事件],[方法],[捕获/冒泡])

    • document.body.removeEventListener('click',fn1,false)
  • 原理:每一个DOM元素都会基于__proto__,基于EventTarget.prototype上的addEventListener/removeEventListener等方法实现事件的绑定和移除

  • DOM2事件绑定采用事件池机制 图片.png

  • 注意

    • 不能是匿名函数(移除事件绑定的时候使用)

    • 浏览器提供的事件行为才可以基于这种模式完成事件的绑定和移除

    • X:window.onDOMContentLoaded

    • :window.addEventListener('DOMContentLoaded',func)

IE 中 DOM2级事件

  • IE8 之前的只支持事件冒泡,所以通过attachEvent()添加的事件处理程序只能添加到冒泡阶段

  • attachEvent()detachEvent(),这两个方法接受相同的两个参数

    • 要处理的事件名

    • 作为事件处理程序的函数

兼容处理

if (typeof btn.addEventListener === 'function') {
  btn.addEventListener('click', fn)
} else if (typeof btn.attachEvent === 'function') {
  btn.attachEvent('onclick', fn)
} else {
  btn.onclick = function () {
    // do something
  }
}

事件对象

  • 给当前元素的某个事件行为绑定方法,当事件行为触发,不仅会把绑定的方法执行,而且还会给方法默认传递一个实参,而这个实参就是事件对象

    image.png

  • 事件对象:存储当前事件操作及触发的相关信息(浏览器本身记录的,记录的是当前这次操作的信息,和在哪个函数中无关)

  • event对象里需要关心的两个属性

    • target:target永远是被添加了事件的那个元素

    • eventPhase:调用事件处理程序的阶段,有三个值

      1:捕获阶段

      2:处于目标

      3:冒泡阶段

preventDefault与stopPropagation

  • preventDefault:比如链接被点击会导航到其href指定的URL,这个就是默认行为

  • stopPropagation:立即停止事件在DOM层次中的传播,包括捕获和冒泡事件

  • IE中对应的属性

    • srcElement => target

    • returnValue => preventDefaukt()

    • cancelBubble => stopPropagation()

  • IE 不支持事件捕获,只能取消事件冒泡,但stopPropagation可以同时取消事件捕获和冒泡

事件委托

// 页面结构
<ul id="myLinks">
  <li id="goSomewhere">Go somewhere</li>
  <li id="doSomething">Do something</li>
  <li id="sayHi">Say hi</li>
</ul>
var list = document.getElementById("myLinks");
EventUtil.addHandler(list, "click", function(event) {
  event = EventUtil.getEvent(event);
  var target = EventUtil.getTarget(event);
  switch(target.id) {
  case "doSomething":
      document.title = "I changed the document's title";
      break;
  case "goSomewhere":
      location.href = "http://www.wrox.com";
      break;
  case "sayHi": alert("hi");
    break; 
  }
}

举例

  • 鼠标事件对象 MouseEvent

    • clientX/clientY 鼠标触发点距离当前窗口的X/Y轴坐标

    • pageX/pageY 鼠标触发点距离BODY的X/Y轴坐标

    • type 事件类型

    • target / srcElement 获取当前事件源(当前操作的元素)

    • path 传播路径

    • ev.preventDefault() / ev.returnValue=false 阻止默认行为

    • ev.stopPropagation() / ev.cancelBubble=true 阻止冒泡传播

  • 键盘事件对象 KeyboardEvent (onkeydown)

    • key / code 存储按键名字

    • which / keyCode 获取按键的键盘码

      • 方向键 “左37 上38 右39 下40”

      • Space 32

      • BackSpace 8

      • Del 46  MAC电脑中没有BackSpace,delete键是 8

      • Enter 13

      • Shift 16  

      • Ctrl 17  

      • Alt 18

    • altKey 是否按下alt键(组合按键)

    • ctrlKey 是否按下ctrl键(组合按键)

    • shiftKey 是否按下shift键(组合按键)

  • TouchEvent 手指事件对象(移动端)(ontouchstart)

    • changedTouches / touches 都是用来记录手指的信息的,平时常用的是changedTouches

      • 手指按下、移动、离开屏幕 changedTouches都存储了对应的手指信息,哪怕离开屏幕后,存储的也是最后一次手指在屏幕中的信息;而touches在手指离开屏幕后,就没有任何的信息了;=>获取的结果都是一个TouchList集合,记录每一根手指的信息;
    • ev.changedTouches[0] 第一根手指的信息

      • clientX/clientY

      • pageX/pageY

  • Event 普通事件对象

    • 默认行为:浏览器会赋予元素很多默认的行为操作

      • 鼠标右键菜单

      • 点击A标签实现页面的跳转

      • 部分浏览器会记录输入记录,在下一次输入的时候有模糊匹配

      • 键盘按下会输入内容

    • 基于ev.preventDefault()来禁用这些默认行为

      • 阻止a标签的默认行为 <a href="javascript:;" id="link">哈哈</a>
<html>
<body>
  <!-- <div class="contextmenu">
        <ul>
            <li>跳转到首页</li>
            <li>进入到详情</li>
            <li>逗你玩</li>
        </ul>
    </div> -->

    <!-- IMPORT JS -->
    <script>

        let body = document.body;


        /*  window.onload = function (ev) {
             console.log(ev);
         }; */


        /* document.body.ontouchstart = function (ev) {
            let point = ev.changedTouches[0];
            console.log(point);
        }; */


        /* document.onkeydown = function (ev) {
            console.log(ev);
        }; */


        // // let n = null;
        // body.addEventListener('click', function (ev) {
        //     // n = ev;
        //     console.log(ev, 0);
        // });

        // body.addEventListener('click', function (ev) {
        //     // console.log(ev === n); // true  当前操作触发后,两个函数中获取的ev是同一个
        //     console.log(ev, 1);
        // });

    </script>


    <script>

        // 禁用右键菜单(后续可能要改为自己的菜单)
        window.oncontextmenu = function (ev) {

            // 阻止默认行为:禁用自带的右键菜单
            ev.preventDefault();


            // 没有右键菜单则创建一个
            let contextmenu = document.querySelector('.contextmenu');
            
            if (!contextmenu) {
                contextmenu = document.createElement('div');
                contextmenu.className = "contextmenu";
                contextmenu.innerHTML = `
                    <ul>
                        <li>跳转到首页</li>
                        <li>进入到详情</li>
                        <li>逗你玩</li>
                    </ul>`;

                document.body.appendChild(contextmenu);
            }


            // 控制右键菜单位置

            contextmenu.style.left = `${ev.clientX+10}px`;
            contextmenu.style.top = `${ev.clientY+10}px`;

        };


        // 点击其他内容(不包含contextmenu及里面的内容),我们让右键菜单消失

        window.onclick = function (ev) {
            let target = ev.target,
                targetTag = target.tagName;

            // 点击contextmenu不做任何处理

            if (targetTag === 'LI') {
                target = target.parentNode;
                targetTag = target.tagName;
            }

            if (targetTag === "UL" && target.parentNode.className === "contextmenu") {
                return;
            }


            // 否则让右侧菜单消失

            let contextmenu = document.querySelector('.contextmenu');
            if (contextmenu) {
                document.body.removeChild(contextmenu);
            }
        };

    </script>

    <!--

        A标签的默认行为:

           + 页面跳转

           + 锚点定位(定位到当前页面指定ID的盒子位置,URL地址中会加入HASH值)

     -->

    <!--  <a href="#box" id="link">哈哈</a>

    <div class="box" id="box"></div> -->



    <!-- 第一种  href="javascript:;" -->

    <!-- <a href="javascript:;" id="link">哈哈</a> -->



    <a href="http://www.zhufengpeixun.cn/" id="link">哈哈</a>

    <script>
        // 点击A标签,先触发其点击事件行为,然后才是默认的跳转
        link.onclick = function (ev) {
            // 阻止默认行为
            ev.preventDefault();
            // return false;
        };
    </script>
</body>

</html>

image.png

场景

image.png

  • 顶层增加监听捕获阶段
    window.addEventListener(
    'click',
    (e) => {
    if (banned === true) {
    e.stopPropagation()
    }
    },
    true
    )
    
  • 增加遮罩
// 事件绑定
const ul = document.querySelector('ul')
  ul?.addEventListener('click', function (e) {
    const target = e.target
    if (target.tagName.toLowerCase() === 'li') {
    // 这里返回的是一个NodeList
      const liList = this.querySelectorAll('li')
      // 非真正数组调用数组上的方法:call 改变 this,调用 Array 原型链上的方法
      const index = Array.prototype.indexOf.call(liList, target)
      alert(`内容为${target.innerHTML},索引为${i}`)
    }
  })

事件捕获&事件冒泡

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

    image.png

    • 目标阶段,没有捕获与冒泡之分,先添加先执行

    • 使用stopPropagation()取消事件传播时,事件不会被传播给下一个节点,但是,同一节点上的其他listener还是会被执行;如果想要同一层级的listener也不执行,可以使用stopImmediatePropagation()

    • preventDefault()只是阻止默认行为,跟JS的事件传播一点关系都没有

    • 一旦发起了preventDefault(),在之后传递下去的事件里面也会有效果