JS15 - DOM 事件机制 - 实例 - 写字板、右键菜单、鼠标跟随、鼠标拖拽、跳转顶部、轮播图、选项卡切换

491 阅读7分钟

DOM 事件概述

何为事件

  • 事件:在编程时发生的系统动作或者发生的事情,同时,用户可以某种方式对事件做出回应
  • 事件监听器/事件处理器:事件触发时运行的代码块。当我们定义了一个用来回应事件被激发的代码块时,我们会说注册了一个事件(监听)处理器。监听器留意事件是否发生,然后处理器就是对事件发生做出的回应

事件组成

事件一般由事件源、事件类型、事件处理函数三大部分组成

  • 事件源:事件发起的源头,即由谁发起的系统动作
  • 事件类型:事件发起的类型,即发起了什么样的系统动作
  • 事件处理函数:事件发起后做什么,即对于该系统动作所做出的回应
//事件源:btn
let btn = document.querySelector("button");
//事件类型:click
btn.onclick = bgChange;
//事件处理函数:bgChange()
function bgChange(){
    btn.style.backgroundColor = `rgb(${Math.random()*255},${Math.random()*255},${Math.random()*255})`;
}

事件注册(绑定)

方式一:DOM0 注册 - 点语法

let btn = document.querySelector("button");
btn.onclick = bgChange;  //事件绑定:JavaScript 点语法
function bgChange(){
    btn.style.backgroundColor = `rgb(${Math.random()*255},${Math.random()*255},${Math.random()*255})`;
}
//注意:这种方式不饿能绑定多个相同事件,后面绑定的会覆盖前面的

方式二:DOM2 注册 - 方法 addEventListener()

<body>
    <button id="btn">button</button>
</body>
<script>
    function bgChange(){
        btn.style.backgroundColor = `rgb(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255})`;
    }
    // addEventListener() 方法 绑定事件
    btn.addEventListener("click",bgChange);
    // 该方法将事件执行全部放在一个匿名函数中也是可行的
    btn.addEventListener("click",function bgChange(){
        btn.style.backgroundColor = `rgb(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255})`});
</script>
//注意:这种方式可以绑定多个相同事件,按代码先后顺序执行

方式三:HTML 属性注册(不推荐)

<body>
    <!-- HTML 内联绑定 -->
    <button onclick="alert('clickbuton')">button</button>
    <!-- HTML 内联绑定 -->
    <button onclick="bgChange()">button</button>
</body>
<script>
    function bgChange(){
        document.getElementsByTagName("button")[0].style.backgroundColor = `rgb(${Math.random()*255},${Math.random()*255},${Math.random()*255})`;
    }
</script>

注意:一个监听器、多个注册器

  • 通过 DOM0 btn.onclick = bgChange 绑定时,后面绑定的相同事件类型会覆盖前面绑定的
  • 通过 DOM2 btn.addEventListener("click", bgChange) 绑定时,不会覆盖前面绑定的多个相同事件类型,会按照绑定的先后顺序依次执行,即同一个监听器能够注册多个处理器
<body>
    <button id="btn">button</button>
</body>
<script>
    function bgChange(){
        btn.style.backgroundColor = `rgb(${Math.random()*255},${Math.random()*255},${Math.random()*255})`;
        console.log("first");
    }
    function textChange(){
        btn.innerText = "new content";
        console.log("second");
    }
    function colorChange(){
        btn.style.color = `rgb(${Math.random()*255},${Math.random()*255},${Math.random()*255})`;
        console.log("third");
    }
    // addEventListener() 方法 绑定多个事件,依次执行,不会覆盖前面相同的事件类型
    btn.addEventListener("click",bgChange);
    btn.addEventListener("click",textChange);
    btn.addEventListener("click",colorChange);
    //控制台依次输出:first second third
</script>

注意:兼容 IE 6 7 8

//attachEvent() 方法在Edge、Chrome等浏览器中报错,不存在该方法
btn.attachEvent("onclick",function(){
    console.log(123)
});
btn.attachEvent("onclick",function(){
    console.log(123)
});

事件解绑

场景分析:绑定事件主要是为了能够有效响应某些系统动作,而如果对于只需要响应系统动作一次的场景,就需要在回应一次系统动作之后,就把事件处理函数清除,例如,在抽奖场景中,点击一次抽奖后,就不允许再次点击,当然这种情况下,也可以给元素标签设置禁用disabled,但容易在审查元素中被篡改。

方式一:赋值 null

  • 说明:这种方式不能解绑通过 addEventListener() 方法注册的事件
btn.onclick = function(){
    console.log("假装模拟抽奖动作...");
    btn.onclick = null; //把事件类型直接赋值为null,事件处理函数也就不存在了
}

方式二:方法 removeEventListener()

  • 功能removeEventListener()  方法可以删除使用 EventTarget.addEventListener() 方法添加的事件。可以使用事件类型,事件侦听器函数本身,以及可能影响匹配过程的各种可选择的选项的组合来标识要删除的事件侦听器。

  • 语法removeEventListener(type, listener, useCapture); type, listener两个必须,useCapture 可选(true 表示移除捕获阶段事件)

  • 指明注册事件:调用 removeEventListener() 时,若传入的参数不能用于确定当前注册过的任何一个事件监听器,该函数不会起任何作用。

  • 捕获冒泡相互独立:如果同一个事件监听器分别为“事件捕获(capture 为 true)”和“事件冒泡(capture 为 false)”注册了一次,这两个版本的监听器需要分别移除。

function price(){
    console.log("假装模拟抽奖动作...");
    btn.removeEventListener("click",price);
}
btn.addEventListener("click",price);

注意:兼容 IE 6 7 8

function price(){
    console.log("假装模拟抽奖动作...");
    btn.dettachEvent("click", price);
}
btn.attachEvent("click", price);

事件对象

表现形式

  • 形参:事件发生时,传入的第一个参数,这个参数一般命名为 event evt e 等。这个参数能够在触发事件时,自动地传递给事件处理函数,而该参数就是事件对象。事件对象中包含了该事件的相关描述信息,例如,触发点击事件的时候,点击的是哪个位置,坐标多少,触发键盘事件的额时候,按键的是哪个按钮

  • window.event === 事件处理函数形参

function formalParam(evt){
    console.log(window.event === evt)   //true
    console.log(window.event.x);        //X轴坐标信息
    console.log(evt.x);                 //X轴坐标信息
    console.log(evt);
    //输出(事件对象-->鼠标事件对象):MouseEvent {isTrusted: true, screenX: 1772, screenY: 250, clientX: 172, clientY: 150, …}altKey: falsebubbles: truebutton: 0buttons: 0cancelBubble: falsecancelable: trueclientX: 172clientY: 150composed: truectrlKey: falsecurrentTarget: nulldefaultPrevented: falsedetail: 1eventPhase: 0fromElement: nullisTrusted: truelayerX: 172layerY: 150metaKey: falsemovementX: 0movementY: 0offsetX: 163offsetY: 141pageX: 172pageY: 150path: (5) [button#btn, body, html, document, Window]relatedTarget: nullreturnValue: truescreenX: 1772screenY: 250shiftKey: falsesourceCapabilities: InputDeviceCapabilities {firesTouchEvents: false}srcElement: button#btntarget: button#btntimeStamp: 507.2700000018813toElement: button#btntype: "click"view: Window {window: Window, self: Window, document: document, name: "", location: Location, …}which: 1x: 172y: 150__proto__: MouseEvent
    evt = evt || window.event;          //兼容IE 6 7 8,低版本浏览器不支持形参
}
btn.addEventListener("click",formalParam);

属性 evt.target

  • 精准指向:如果要在多个元素上设置相同的事件处理函数时,evt.target 能够精准指向当前操作的元素,返回 元素对象

  • 例如,有 9 块方格,当谁被点击,谁就改变颜色,此时用 evt.target 总是能准确选择当前操作的方格,并执行操作让它变色。

<head>
    <style>
        div{
            width: 100px;
            height: 100px;
            float: left;
            text-align: center;
            display: table;
            background-color: black;
        }
        div::after{
            content: "";
            display: table-cell;
            vertical-align: middle;
        }
    </style>
</head>
<body>
    <div></div>    <div></div>    <div></div>    
    <div></div>    <div></div>    <div></div>
    <div></div>    <div></div>    <div></div>
</body>
<script>
    let squares = document.querySelectorAll("div");
    for (let i = 0; i < squares.length; i++) {
        squares[i].onclick = function(evt){
            //通过 evt.target 指向自身
            evt.target.style.backgroundColor = `rgb(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255})`;
            //在这种遍历条件下,也可以通过下标指向自身
            squares[i].style.backgroundColor = `rgb(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255})`;
        }        
    }
</script>

evt.targrt 属性 指向自身.gif

阻止事件默认行为

  • 事件默认行为问题:如果某个元素绑定了一个事件,那么触发这个事件类型也就会执行相应的事件处理函数,这个过程是默认直接执行的,但如果在特殊情况下,以web表单提交为例,当提交表单的信息有误时,希望代码能够中断提交,并返回一个错误提示,此时默认行为就需要被改变

  • 阻止默认行为不是阻止事件传播:Event 接口的 preventDefault() 方法,告诉user agent:如果此事件没有被显式处理,它默认的动作也不应该照常执行。此事件还是继续传播,除非碰到事件侦听器调用stopPropagation() 或stopImmediatePropagation(),才停止传播。

  • 阻止事件默认行为

    • HTML 标签可以通过表单自带验证功能来检验,但不同浏览器的支持程度不一,因此,最好过度不要依赖

    • Javascript 通过 DOM0 绑定的事件可以通过 return falseevt.preventDefault() 两种方式阻止默认行为

    • JavaScript 通过 DOM2 绑定的事件需要通过 evt.preventDefault() 函数,阻断默认行为,return false 无效

举例一:阻止web表单点击提交的默认行为

<body>
    <form action="http://www.baidu.com" method="get" target="_blank">
        <fieldset>
            <legend>form</legend>
            <!-- 可见标签属性没有设置格式检测 -->
            <input type="text" name="username" id="user_name">
            <input type="submit" value="submit" id="user_submit">
        </fieldset>
    </form>
</body>
<script>
    //dom0 阻止默认行为办法:return false 或 evt.preventDefault
    user_submit.onclick = function(evt){
        //例子:表单只有输入James,才能被提交,否则打印错误信息,并阻止DOM事件默认行为
        if (user_name.value === "James") {
            console.log(`Welcome ${user_name.value}.`)
        } else {
            console.log("You are not James.");
            return false; //或 evt.preventDefault();
        }
    }
</script>
<body>
    <form action="http://www.baidu.com" method="get" target="_blank">
        <fieldset>
            <legend>form</legend>
            <!-- 可见标签属性没有设置格式检测 -->
            <input type="text" name="username" id="user_name">
            <input type="submit" value="submit" id="user_submit">
        </fieldset>
    </form>
</body>
<script>
    //dom2 阻止默认行为方法:evt.preventDefault,也适用于 dom0 事件默认行为
    user_submit.addEventListener("click",function(evt){
        //例子:表单只有输入James,才能被提交,否则打印错误信息,并阻止DOM事件默认行为
        if (user_name.value === "James") {
            console.log(`Welcome ${user_name.value}.`)
        } else {
            console.log("You are not James.");
            // return false;   //无法阻止 DOM2 绑定事件的默认行为
            evt.preventDefault();
        }
    })
</script>

阻止DOM事件默认行为.gif

举例二:阻止点击复选框就被选中的默认行为

<body>
    <p>Please click on the checkbox control.</p> 
    <label for="check">Checkbox:</label>
    <input type="checkbox" id="check">
    <div></div>
</body>
<script>
    check.onclick = function(evt){
        document.getElementsByTagName("div")[0].innerHTML +="<p>return false OR preventDefault() 阻止了“点击复选框即选中”的默认行为</p>";
        evt.preventDefault();
    }
</script>

evt.preventDefault()阻止默认行为.gif

事件流(Event Flow)

事件传播及事件模型

  • DOM 事件流/事件传播:DOM 结构是一个树型结构,当事件产生时,该事件会在元素节点与根结点之间的路径传播,路径所经过的节点(包含innerText文本节点)都会收到该事件,这个传播过程可称为 DOM事件流
  • 事件模型:当一个事件发生在具有父元素的元素上时,现代浏览器运行两个不同的阶段 - 捕获阶段冒泡阶段,因此DOM同时支持两种事件流模型 —— 捕获型事件冒泡型事件,其中,捕获型事件先发生。
  • 特点 1:DOM事件流最独特的性质是,innerText 文本节点也触发事件,但在IE中不会
  • 特点 2:DOM 事件流是不会因为是否绑定了事件就捕获(冒泡)或不捕获(冒泡),事件流在任何情况下都存在,只是需不需要触发函数而已
  • 特点 3:在DOM时间流中的捕获和冒泡过程中,最高一级是window,因此在document节点之上还有一个window对象参与事件流

传播顺序(事件生命周期三阶段)

  • 传播顺序(三阶段):以 click 事件为例,当鼠标单击 innerText 文本时会触发click事件,该事件的捕获阶段最先开始,从document节点开始逐渐向下传播,直到 innerText 文本节点,事件进入目标阶段,在目标阶段结束之后,事件由 innerText 文本节点开始事件的冒泡阶段,直到document节点为止,总结即为 捕获型事件阶段 --> 目标事件阶段 --> 冒泡型事件阶段

  • 阶段1 - 捕获型事件流 - 事件自上而下传播(document节点-->目标节点)的阶段:最开始由 最外层祖先<html> 捕获事件,同时,浏览器会检测元素是否有事件被注册(每个元素的注册事件可能不同)事件监听器 addEventListener(type, listener, useCapture); 的 useCapture 属性是否为 true (默认false),如果注册事件且useCapture为true,则就在捕获阶段执行事件处理函数;否则,事件会被接着传递给派生元素路径的下一个元素,直至目标元素。由此,可见只有按照 DOM2 方式注册事件并进行配置,才能看到捕获的回调函数被触发。

  • 阶段2 - 目标事件执行:真正的目标节点处理事件的阶段

  • 阶段3 - 冒泡型事件流 - 事件自下而上传播(目标节点-->document节点)的阶段:当事件在某一元素被触发时,事件将跟随该节点的各个父节点冒泡穿过整个的DOM节点层次,直到DOM元素遇到依附有该事件类型处理器的节点。在冒泡过程中的任何时候都可以终止事件的冒泡,在遵从的浏览器里可以通过调用事件对象上的 stopPropagation() 方法,在IE低版本(只支持冒泡)里可以通过设置事件对象的 cancelBubble 属性为true。如果不停止事件的传播,事件将一直通过DOM冒泡直至到达文档根节点。

事件流的参数传递

  • 参数传递:在一个HTML文档中,DOM 事件流的目标事件对象,会作为整个事件流中,每个元素所注册的事件处理函数的事件对象(参数),因此,evt.target 无论在本次事件流中的哪一个元素所注册的事件类型,都会指向同一个元素对象,就是与目标事件所绑定的元素对象
<!DOCTYPE html>
<html lang="en">
<head>
    <style>
    div{border: 1px solid black;margin: 5px;text-align: center;}
    .grand-farther{width: 220px;height: 220px;}
    .farther{width: 180px;height: 170px;}
    .child{width: 120px;height: 60px;display: inline-block;}
    </style>
</head>
<body>
    <div class="grand-farther" id="grand_farther">grand-farther
        <div class="farther" id="farther">farther
            <div class="child" id="child_fir">child</div>
            <div class="child" id="child_sec">brother</div>
        </div>
    </div>
</body>
<script>
    //Remind:由于为了检查捕获与冒泡阶段,故代码写了两遍,那么目标执行阶段也就会执行两次
    
    //不设置捕获阶段执行事件 --> 冒泡阶段
    farther.onclick = function(evt){
        //如果事件流中传递的参数是本元素注册的事件对象,就说明是点击的本元素,也即本元素就是目标事件对象
        if (evt.target.id === "farther") {
            console.log("farther-目标事件阶段");  
        } else {
            console.log("farther-冒泡阶段");  //由于事件冒泡,没有点击父元素,也在子元素执行之后执行了
        }
    }
    child_fir.addEventListener("click",function(evt){
        if (evt.target.id === "child_fir") {
            console.log("child_fir-目标事件阶段");  
        } else {
            console.log("child-冒泡阶段");   //点击子元素,会在冒泡阶段先执行
        }
    })
    child_sec.onclick = function(evt){
        if (evt.target.id === "child_sec") {
            console.log("child_sec-目标事件阶段");  
        } else {
            console.log("child-brother-冒泡阶段"); //兄弟元素,不会受到冒泡阶段影响
        }
    }
    grand_farther.addEventListener("contextmenu",function(evt){
        if (evt.target.id === "grand_farther") {
            console.log("grand_farther-目标事件阶段");  
        } else {
            console.log("grand-farther-冒泡阶段");
        }
    })
    // document.body.addEventListener("click")
    
    // 设置捕获阶段执行事件 --> 捕获阶段
    farther.addEventListener("click",function(evt){
        if (evt.target.id === "farther") {
            console.log("farther-目标事件阶段");  
        } else {
            console.log("farther-捕获阶段");  //由于事件捕获,没有点击父元素,也在子元素执行之前执行了
        }
    },true)
    child_fir.addEventListener("click",function(evt){
        if (evt.target.id === "child_fir") {
            console.log("child_fir-目标事件阶段");  
        } else {
            console.log("child-捕获阶段");   //点击子元素,会在冒泡阶段先执行
        }
    },true)
    child_sec.addEventListener("click", function(evt){
        if (evt.target.id === "child_sec") {
            console.log("child_sec-目标事件阶段");  
        } else {
            console.log("child-brother-捕获阶段"); //兄弟元素,不会受到冒泡阶段影响
        }
    },true)
    grand_farther.addEventListener("contextmenu",function(evt){
        if (evt.target.id === "grand_farther") {
            console.log("grand_farther-目标事件阶段");  
        } else {
            console.log("grand-farther-捕获阶段");
        }
    },true) //设置useCapture参数,true表示捕获时执行函数,不写则默认false,表示只在冒泡阶段执行函数
</script>
</html>

DOM事件流-捕获+目标+冒泡.gif

阻止 DOM 事件流

  • 问题:由于DOM事件流的存在,导致在很多时候点击页面,会出现意想不到的额外动作发生,如果代码数量增多,这将会是一种令人讨厌的行为,因此为了解决这个问题,我们需要通过一种方法来阻止事件流的继续传播

  • 历史:为什么我们要弄清楚捕捉和冒泡呢?那是因为,在过去糟糕的日子里,浏览器的兼容性比现在要小得多,Netscape(网景)只使用事件捕获,而 Internet Explorer 只使用事件冒泡。当 W3C 决定尝试规范这些行为并达成共识时,他们最终得到了包括这两种情况(捕捉和冒泡)的系统,最终被应用在现在浏览器里。

  • 补充:默认情况下,所有事件处理程序都是在冒泡阶段注册的,这在大多数情况下更有意义。如果想在捕获阶段注册一个事件,可以将 addEventListener() 第三个属性设置为 true。

child_fir.addEventListener("click",function(evt){
    if (evt.target.id === "child_fir") {
        evt.stopPropagation();  //阻止具相同事件类型的父元素在冒泡阶段执行,不涉及捕获阶段
        evt.cancelBubble = true;    //兼容 IE 6 7 8
        console.log(`  child_fir-目标事件阶段`);  
    } else {
        console.log("  child-冒泡阶段");   //点击子元素,会在冒泡阶段先执行
    }
})
child_sec.oncontextmenu = function(evt){
    if (evt.target.id === "child_sec") {
        evt.stopPropagation(); //阻止具相同事件类型的父元素在冒泡阶段执行,不涉及捕获阶段
        console.log(`  child_sec-目标事件阶段`);  
    } else {
        console.log(`  child-brother-冒泡阶段`); //兄弟元素,不会受到冒泡阶段影响
    }
}

CSS 阻止事件/事件穿透

  • 关键代码pointer-events:none;

  • 主要功能:指定某元素成为(或 不成为)鼠标事件的 target

  • 取值 none 说明:该取值表示元素永远不会成为鼠标事件的 target,但是,当其后代元素的 pointer-events 属性指定其他值时,鼠标事件可以指向后代元素,在这种情况下,鼠标事件将在捕获或冒泡阶段触发父元素的事件侦听器。

<style>
    a[href="http://www.baidu.com"]{
        pointer-events: none;
    }
</style>
<body>
    <!-- 因为css设置pointer-events为none,,所以该链接的鼠标事件失效,无法点击 -->
    <a href="http://www.baidu.com">gsd</a>
    <!-- 可以正常点击 -->
    <a href="https://developer.mozilla.org/">asd</a>
</body>

事件委托

  • 需求场景:在重复的元素上绑定一个相同的事件,这需要把所有目标元素都考虑到绑定的操作,这样一来如果元素数量巨大,代码便会特别冗余,例如,一系列列表项,希望让每个列表被点击的时候弹出一条信息,如果每个都单独注册时间会特别麻烦

  • 事件流为前提:以click事件类型为例,点击子元素的时候,不管子元素有没有click事件,只要父元素有click事件,那么就可以触发父元素的点击事件

  • 委托操作:如果想在大量子元素中单击任何一个都可以运行一段代码,可以将事件监听器设置在其父节点上,并让子节点上发生的事件冒泡到父节点上,而不是每个子节点单独设置事件监听器,例如,如果想让每个列表项被点击时弹出一条信息,可以将click单击事件监听器设置在父元素<ul>上,这样事件就会从列表项冒泡到其父元素<ul>上。

<body>
    <ul id="ul_obj">
        <li>list content sequence 1</li>
        <li>list content sequence 2</li>
        <li>list content sequence 3</li>
        <li>list contemporary 4</li>
        <li>list conttent contemp 5</li>
    </ul>    
</body>
<script>
    ul_obj.addEventListener("click",function(evt){
        //事件对象形参evt是实际点击的那个元素
        console.log(evt.target.innerText);  
        /**当点击上一行代码的时候,
         * 可以发现点击ul能够输出所有li,说明能够触发ul的事件,
         * 如果点击任意一个li元素,能够输出当前点击的li元素innerText值,
         * 而li并没有注册事件,说明是在冒泡阶段,由于父元素ul注册了事件,
         * 起到了事件委托的作用
         */
    })
</script>

事件委托.gif

DOM 各类事件及对象属性

鼠标事件 MouseEvent

  • MouseEvent 接口:指用户与指针设备(如鼠标)交互时发生的事件。
  • 接口派生:MouseEvent 派生自 UIEvent,UIEvent 派生自 Event。
  • 常见事件click dblclick mouseup mousedown mousemove mouseover mouseout mouseenter mouseleave contextmenu

事件类型

  • click 单击:(1)定点设备的按钮(通常是鼠标的主键)在一个元素上被按下和放开时,将触发 click 事件;(2)该事件会在 mousedown 和 mouseup 事件依次触发后触发;(3)如果在一个元素上按下按钮,而将指针移到元素外再释放按钮,则在包含这两个元素的最具体的父级元素上触发事件

  • dblclick 双击:在单个元素上单击两次鼠标的指针设备按钮 (通常是小鼠的主按钮) 时,将触发 dblclick 事件

  • mousedown 鼠标按下:当指针在元素中时,mouseup 事件在指针设备(如鼠标或触摸板)按钮放开时触发

  • mouseup 鼠标抬起:当指针在元素中时,mousedown 事件在指针设备(如鼠标或触摸板)按钮按下时触发,与 mouseup 事件相反

  • mousemove 鼠标移动:当指针设备 ( 通常指鼠标 ) 在元素上移动时,mousemove 事件被触发

  • contextmenu 鼠标右键/菜单键:该事件通常在鼠标点击右键或者按下键盘上的菜单键时被触发。任何没有被禁用的鼠标右击事件将会使得 contextmenu 事件在目标元素上被触发,即鼠标右键事件是默认注册的,一般需要操作的主要是禁用鼠标右键,禁用方式主要就是通过事件对象调用方法 evt.preventDefault()

  • mouseenter(不冒泡) / mouseover 鼠标移入:当一个定点设备(通常指鼠标)第一次移动到触发事件元素中的激活区域时触发,从基本操作上两者非常相似,区别在于:(1)鼠标指针从 mouseenter 后代移动到它自己的物理空间时,不会在冒泡或捕获阶段执行;(2)单个 mouseover 事件会按层次结构冒泡,直到它被处理程序取消或者到达根,如果层次结构很深,发送事件可能相当多,这会导致严重的性能问题。在这种情况下,最好是监听 mouseenter 事件

  • mouseleave(不冒泡) / mouseout 鼠标移出:当移动指针设备(通常是鼠标),不再包含在这个元素或其子元素中时,鼠标移出事件被触发。区别在于mouseleave 不会冒泡而 mouseout 会冒泡,因此 mouseout 移入遮盖了父元素可视区域的子元素也会被触发

事件对象属性

  • target 触发元素:是对触发事件的元素的引用,因此返回的是一个元素对象,该属性可以用来实现事件委托(event delegation)

  • pageX pageY 文档全页:只读属性,单位像素,距离整个文档的左上角的坐标

  • clientX clientY 可视窗口:只读属性,距离客户端区域(浏览器可视窗口)左上角坐标。例如,不论页面是否有水平滚动,点击客户端区域左上角,clientX clientY 值都将为 0

  • offsetX offsetY 触发元素:只读属性,距离触发元素左上角的坐标值

实例 - canvas 写字板(鼠标)

<!DOCTYPE html>
<html lang="en">
<head>
    <style>
        canvas{
            width: 500px;
            height: 500px;
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <h1>Drawing with mouse events</h1>
    <!--注意:canvas 元素对象设定的width与height属性与坐标密切相关,
        因此,CSS属性的width和height不能设置的与行内属性有差别,
        否则将会是的绘图出现距离偏差 -->
    <canvas width="500" height="500px"></canvas>
</body>
<script>
    let draw_sec = document.querySelector("canvas");
    /**补充:如何通过canvas对象创建2D画布
     * 方法:基于Canvas元素对象 --> <canvas> 元素可被用来通过 JavaScript(Canvas API 或 WebGL API)绘制图形及图形动画
     *      HTMLCanvasElement.getContext(contextType)
     *      HTMLCanvasElement.getContext(contextType,contextAttributes) --> 更多查阅MDN文档
     * 参数:contextType
     *      "2d" --> 建立一个 CanvasRenderingContext2D 二维渲染上下文
     * 应用:创建2D画布 --> HTMLCanvasElement.getContext("2d")
     */ 
    //创建一个2D画布
    let canvasContext = draw_sec.getContext("2d"); //需要标签名是 canvas,否则报错:Uncaught TypeError: draw_sec.getContext is not a function
    /**补充:如何设置2D画布的绘画显示
     * 对象:基于建立的2D画布对象,CanvasRenderingContext2D
     * 方法+属性:(开始绘制-结束绘制-绘制呈现)从上到下
     *      CanvasRenderingContext2D.beginPath(); --> 创建一个构图新路径,同时清空子路径列表
     *      CanvasRenderingContext2D.strokeStyle = color/画布渐变对象/画布图片 --> 画笔(绘制图形)颜色或者样式的属性
     *      CanvasRenderingContext2D.lineWidth = value --> 线宽
     *      CanvasRenderingContext2D.moveTo(x, y); --> 路径起点,移到 (x,y) 坐标
     *      CanvasRenderingContext2D.lineTo(x, y); --> 路径终点,移到 (x,y) 坐标
     *      CanvasRenderingContext2D.closePath(); --> 封闭绘图,将笔点返回到当前子路径起始点
     *      CanvasRenderingContext2D.stroke(); --> 将前面代码的绘制动作呈现在画布上
     */
    //设置2D画布绘画显示,组成绘图动作方法
    function drawLine(canvasContext,x_start,y_start,x_end,y_end){
        canvasContext.beginPath();
        canvasContext.strokeStyle = "gray";
        canvasContext.lineWidth = 2;
        canvasContext.moveTo(x_start,y_start);
        canvasContext.lineTo(x_end,y_end);
        canvasContext.stroke();
    }
    //设定变量,判断是否需要绘图
    let isDraw = false; //默认还没有开始绘图
    let x, y;
    //鼠标点下,不松开,开启绘图功能
    draw_sec.addEventListener("mousedown",function(evt){
        //获取绘图起点
        x = evt.offsetX;
        y = evt.offsetY;
        isDraw = true;
    })
    //鼠标移动,跟着鼠标自由绘图
    draw_sec.addEventListener("mousemove",function(evt){
        if (isDraw === true) {
            drawLine(canvasContext,x,y,evt.offsetX,evt.offsetY);
            x = evt.offsetX;
            y = evt.offsetY;
        } //else none
    })
    //鼠标松开,结束绘图
    draw_sec.addEventListener("mouseup",function(evt){
        if (isDraw === true) {
            drawLine(canvasContext,x,y,evt.offsetX,evt.offsetY);
            x = 0;
            y = 0;
            isDraw = false;
        } //else none
    })
</script>
</html>

mousedown+mouseup+mousemove实现canvas画布写字板.gif

实例 - 自定义鼠标右键菜单

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Document</title>
  <style>
    body{
      position: relative;
    }
    .main-page{
      width: 200px;
      height: 200px;
      background-color: lightgray;
      text-align: center;
      line-height: 50px;
    }
    ul{
      text-align: center;
      list-style-type: none;
      padding: 0;
      margin: 0;
    }
    li:hover{
      background-color: black;
      color: wheat;
      height: 50px;
      line-height: 50px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div id="main_page" class="main-page">右键点击唤醒菜单</div>
</body>
<script>
  let newDiv = document.createElement("div");
  //设置右键菜单元素及样式
  function createNewDiv(x,y,content){
    main_page.appendChild(newDiv);
    newDiv.setAttribute("id","new_div");
    newDiv.style.width = "100px";
    newDiv.style.backgroundColor = "white";
    newDiv.style.position = "absolute";
    newDiv.style.left = x + "px";
    newDiv.style.top = y + "px";
    document.getElementById("new_div").innerHTML = `
      <div style="text-align:center;background:black;color:white;">${content}</div>
      <ul><li>list item</li></ul>
      <ul><li>list item</li></ul>
      <ul><li>list item</li></ul>
      <ul><li>list item</li></ul>
      <ul><li>list item</li></ul>
      `;
  }
  //鼠标右键打开右键菜单
  main_page.addEventListener("contextmenu",function(evt){
    evt.preventDefault();
    createNewDiv(evt.clientX,evt.clientY,`${evt.clientX} ${evt.clientY}`);
  })
  //左键点击任意住关闭右键菜单
  window.addEventListener("click",function(){
    !!document.getElementById("new_div") ? main_page.removeChild(newDiv) : null;
  })
</script>
</html>

contextmenu 鼠标右键事件-自定义右键菜单.gif

实例 - 鼠标跟随(鼠标悬停就显示更大的元素框)

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <style>
        .device-ultimal{
            width: 200px;
            height: 100px;
            background-color: lightblue;
            padding: 50px;
            margin: 20px;
            position: relative;
        }
        .new-flow-ele{
            width: 150px;
            height: 100px;
            background-color: lightcoral;
            position: absolute;
            z-index: 10;    /* 不被相邻元素遮挡 */
            /**补充:pointer-events
             * 功能:指定某元素作为(或 不作为)鼠标事件的 target
             * 取值 none 说明:
             *      元素永远不会成为鼠标事件的target,
             *      但是,当其后代元素的 pointer-events 属性指定其他值时,
             *      鼠标事件可以指向后代元素,在这种情况下,
             *      鼠标事件将在捕获或冒泡阶段触发父元素的事件侦听器
             */
            pointer-events: none; /* 穿透当前指向的元素 --> 在本例中主要是为了避免鼠标放在移动块上后出现子元素捕获父元素的事件而执行 */
        }
    </style>
</head>
<body>
    <div id="device" class="device-ultimal"></div>
    <div id="back" class="device-ultimal"></div>
</body>
<script>
    //创建鼠标跟随的新元素
    let newFlowEle = document.createElement("div");
    //事件监听:实时更改坐标位置,实现鼠标跟随
    device.addEventListener("mousemove",function(e){
        device.appendChild(newFlowEle);
        newFlowEle.id = "new_flow_ele";
        newFlowEle.style.left = e.offsetX + "px";
        newFlowEle.style.top = e.offsetY + "px";
        newFlowEle.className = "new-flow-ele";
        newFlowEle.innerHTML = "<b>totam vitae expedita eius ipsa dignissimos?</b>"
    });
    //鼠标移出后,跟随元素消失
    device.addEventListener("mouseout",function(e){
        !!document.getElementById("new_flow_ele") ? device.removeChild(newFlowEle) : null;
    })
</script>
</html>

鼠标跟随.gif

实例 - 鼠标拖拽

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
    <style>
        .square{
            width: 500px;
            height: 500px;
            margin: auto;
            background-color: lightgray;
            position: relative;
        }
        .box{
            width: 150px;
            height: 150px;
            background-color: violet;
            position: absolute;
            pointer-events: none;  /* 点击事件穿透拖拽 */
        }
        .box2{
            width: 10px;
            height: 100px;
        }
    </style>
</head>
<body>
    <div id="square" class="square">
        <div id="box" class="box"></div>
    </div>    
</body>
<script>
    let boxSquare = document.getElementById("square");
    let boxDrag = document.getElementById("box");
    /**补充:Element.getClientRects() 和 Element.getBoundingClientRect
     * 功能:
     * Element.getClientRects() 方法返回一个指向客户端中每一个盒子的边界矩形的矩形集合
     * Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置
     */ 
    let boxSqaureRect = boxSquare.getBoundingClientRect();
    let boxSquareHeight = boxSqaureRect.bottom - boxSqaureRect.top;
    let boxSquareWidth = boxSqaureRect.right - boxSqaureRect.left;
    
    //在拖拽目标元素区域内,鼠标按下,中心自动对准目标
    boxSquare.addEventListener("mousedown",function(evt){
        boxDrag.style.left = (evt.offsetX - Number.parseInt(getComputedStyle(boxDrag).width)/2) + "px";
        boxDrag.style.top = (evt.offsetY - Number.parseInt(getComputedStyle(boxDrag).height)/2) + "px";
        //鼠标按下不松开,执行拖拽,不超过父元素边框
        boxSquare.addEventListener("mousemove",moveDrag)
    })

    /**通过方法addEventListener() 注册的事件,无法通过赋值null解绑,
     * 因此,需要使用removeEventListener() 方式,移除事件,
     * 从而需要将事件处理函数,独立分装出来或者使用声明式函数
     */ 
    
    function moveDrag(evt){
        //拖动
        boxDrag.style.left = evt.offsetX - Number.parseInt(getComputedStyle(boxDrag).width)/2 + "px";
        boxDrag.style.top = evt.offsetY - Number.parseInt(getComputedStyle(boxDrag).height)/2 + "px";
        //限制左右
        if (boxSquareWidth <= Number.parseInt(getComputedStyle(boxDrag).width)/2 + evt.offsetX) {
            boxDrag.style.left = boxSquareWidth - Number.parseInt(getComputedStyle(boxDrag).width) + "px";
        } else if(evt.offsetX <= Number.parseInt(getComputedStyle(boxDrag).width)/2) {
            boxDrag.style.left = 0;
        } //else none
        //限制上下
        if (boxSquareHeight <= Number.parseInt(getComputedStyle(boxDrag).height)/2 + evt.offsetY) {
            boxDrag.style.top = boxSquareHeight - Number.parseInt(getComputedStyle(boxDrag).height) + "px";
        } else if(evt.offsetY <= Number.parseInt(getComputedStyle(boxDrag).height)/2) {
            boxDrag.style.top = 0;
        } //else none
    }

    //鼠标松开,结束拖拽,目标元素原地不动
    boxSquare.addEventListener("mouseup",function(evt){
        boxSquare.removeEventListener("mousemove", moveDrag);
    })
</script>
</html>

鼠标拖拽.gif

实例 - 点击跳至顶部

<style>
    html,body{height: 120%;margin: 0;padding: 0;text-align: center;}
    .header{
        height: 80px;width: 100%;background-color: aqua;display: table;
        position: fixed;top:-80px;
    }
    .header span{display: table-cell;vertical-align: middle;}
    .display-header{animation: headerAnimation .5s linear;animation-fill-mode: forwards;}
    .icon{
        width: 50px;height: 50px;background-color: black;color: white;line-height: 50px;cursor: pointer;
        position: fixed;bottom: 30px;right: 30px;display: none;
    }
    .display-icon{display: block;}
    @keyframes headerAnimation{
        from{
            top: -80px;
        }to{
            top: 0;
        }
    }
</style>
<body>
    <div id="header" class="header"><span>HEADER</span></div>
    <div id="icon" class="icon"></div>
</body>
<script>
    //获取元素
    let headerNav = document.getElementById("header");
    let goTop = document.getElementById("icon");
    //BOM 窗口滚动事件
    onscroll = function(){
        //滚动的时候,实时获取窗口卷入距离
        let distance = document.documentElement.scrollTop || document.body.scrollTop;
        //卷入距离大于100,就显示
        if (distance > 100) {
            headerNav.classList.add("display-header");
            goTop.classList.add("display-icon");
        } else {
            headerNav.classList.remove("display-header");
            goTop.classList.remove("display-icon");
        }
    }
    //点击跳转顶部
    goTop.addEventListener("click",function(){
        scrollTo(0,0);
    });
</script>

跳转顶部.gif

实例 - 轮播图

<style>
    *{
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
    }
    .banner{
        height: 400px;
        position: relative;
    }
    .banner-pics{
        height: 100%;
    }
    .banner-pics li{
        height: 100%;
        position: absolute;
        top: 0;
        bottom: 0;
        background-size: contain;
    }
    .pic-display{
        display: block;
    }
    .pic-none{
        display: none;
    }

    .banner-points{
        width: auto;
        height: auto;
        position: absolute;
        bottom: 10px;
        left: 50%;
        margin: 0;
        padding: 0;
        list-style-type: none;
    }
    .banner-points li{
        width: 10px;
        height: 10px;
        margin: 5px 5px;
        background-color: rgba(0,0,0,0.5);
        border-radius: 50%;
        float: left;
        cursor: pointer;
    }
    .banner-left,.banner-right{
        width: 40px;
        height: 100%;
        display: table;
        font-size: x-large;
        color: white;
        cursor: pointer;
        position: absolute;
        top: 0;
    }
    .banner-left span,.banner-right span{
        display: table-cell;
        text-align: center;
        vertical-align: middle;
    }
    .banner-left{
        left: 0;
    }
    .banner-right{
        right: 0;
    }
</style>
<body>
    <section class="banner">
        <!-- 图片区域 -->
        <ul class="banner-pics">
            <li data-pic="0"></li>
        </ul>
        <!-- 焦点区域 -->
        <ul class="banner-points">
            <li data-point="0"></li>
        </ul>
        <!-- 左右切换按钮 -->
        <div class="banner-left" data-check="left"><span></span></div>
        <div class="banner-right" data-check="right"><span></span></div>
    </section>
</body>
<script>
    //功能:DOM实现轮播图
    let data = [
        {url:"./materials/material-to_do_hires.jpg"},
        {url:"./materials/material-sundae.jpg"},
        {url:"./materials/material-rose-outdoors.jpg"},
        {url:"./materials/material-ice-cream.jpg"},
    ]

    //获取元素
    let bannerPics = document.querySelector(".banner-pics");
    let bannerPoints = document.querySelector(".banner-points");
    let bannerLeft = document.querySelector(".banner-left");
    let bannerRight = document.querySelector(".banner-right");
    //删除CSS调试用元素
    bannerPics.removeChild(bannerPics.firstElementChild);
    bannerPoints.removeChild(bannerPoints.firstElementChild);
    //创建元素
    let picELe = document.createElement("li");
    let pointEle = document.createElement("li");
    //添加到页面(加上后台数据)
    for (let i = 0; i < data.length; i++) {
        //添加图片
        let pic = bannerPics.appendChild(picELe.cloneNode());
        pic.setAttribute("data-pic",i);
        pic.style.backgroundImage = `url(${data[i].url})`;
        pic.style.transition = "opacity .2s linear"; //设置图片切换的时候呈现渐变效果
        Number(pic.dataset.pic) === 0 ? pic.style.opacity = "1": pic.style.opacity = "0";
        //添加焦点
        let point = bannerPoints.appendChild(pointEle.cloneNode());
        point.setAttribute("data-point",i);
        Number(point.dataset.point) === 0 ? point.style.backgroundColor = "rgba(255, 255, 255, 0.8)" : null;
    }
    //鼠标点击左右切换轮播图,约定:字符串“left”切换上一张,字符串“right”切换下一张,数字对应第几张
    let checkterm = "right";
    let index = 0;
    let pics = document.querySelectorAll(".banner-pics li");
    let points = document.querySelectorAll(".banner-points li");
    function change(checkterm){
        pics[index].style.opacity = "0"; //先把当前显示的图片隐藏
        points[index].style.backgroundColor = "rgba(0,0,0,0.5)"; //当前焦点切换成默认
        //判断是哪一种切换
        if (checkterm === "right") {
            index++;
        } else if(checkterm === "left") {
            index--;
        } else{
            index = checkterm;
        }
        //判断是否超出了数组限制
        index < 0 ? index = data.length - 1 : null;
        index >= data.length ? index = 0 : null;
        //显示切换后的图片和焦点
        pics[index].style.opacity = "1";
        points[index].style.backgroundColor = "rgba(255, 255, 255, 0.8)";
    } 
    bannerLeft.addEventListener("click",change.bind(bannerLeft,bannerLeft.dataset.check));
bannerRight.addEventListener("click",change.bind(bannerRight,bannerRight.dataset.check));
Array.from(points).forEach(item => {
    item.addEventListener("click",change.bind(item,item.dataset.point));
});
</script>

DOM实现轮播图.gif

键盘事件 KeyboardEvent

  • KeyboardEvent 接口:用户与键盘的单个交互 keydown, keypress 与 keyup 用于识别不同的键盘活动类型
  • 常见事件keydown keypress keyup
  • 注意事项: KeyboardEvent 只在低级别提示用户与一个键盘按键的交互是什么,不涉及这个交互的上下文含义。当需要处理文本输入的时候,使用 input 事件代替。
  • 事件区别:keydown 和 keyup 事件提供指出哪个键被按下的代码,而 keypress 指出哪些字符被输入。例如,小写字母“a”在 keydown 和 keyup 时会被报告为 65,但在 keypress 时为 97。所有事件均将大写字母“A”报告为 65。

事件类型

  • keydown 按下:所有按键均会触发keydown事件,无论这些按键是否会产生字符值

  • keyup 松开:在按键被松开时触发

  • keypress 键入:(不推荐)fired when a key that produces a character value is pressed down

事件对象属性

  • target 触发元素:是对触发事件的元素的引用,因此返回的是一个元素对象,该属性可以用来实现事件委托(event delegation)

  • keyCode 键盘编码

    image.png

触摸事件 -- 移动端

事件概述:为了给基于触摸的用户界面提供高质量的支持,触摸事件提供了在触摸屏或触控板上解释手指(或触控笔)活动的能力。整个交互期间,程序接收开始、移动、结束三个阶段的触摸事件。触摸事件与鼠标事件类似,不同的是触摸事件还提供同一表面不同位置的同步触摸

事件类型

  • touchstart:一个或多个触点与触控设备表面接触时被触发

  • touchmove:触点在触控平面上移动时触发

  • touchend:触点离开触控平面时触发

  • touchcancel:在触点被中断时触发,中断方式基于特定实现而有所不同(例如,创建了太多的触点)

事件对象属性

  • changedTouches:返回一个 TouchList 对象,表示在前一个 touch 事件和当前的事件之间,状态发生变化的独立触点,对不同事件意义不同,touchstart 事件而言,包含新增加的触点;touchmove 事件而言,包含变化的触点;touchend 事件而言,包含离开的触点

  • targetTouches:返回一个 TouchList 对象,仍与触摸面接触的所有触摸点,不适用touchend事件

  • touches:所有当前在与触摸表面接触的 Touch 对象,不管触摸点是否已经改变或其目标元素是在处于 touchstart 阶段,不适用touchend事件

  • 属性调用注意:由于上述属性在TouchList对象的第一个键值对,因此在调用的时候主要加上索引值 evt.touches[0].pageX

TouchList {0: Touch, length: 1}
0: Touch
    clientX: 181.07693481445312  
    clientY: 176.8461456298828  
        -->相对 - 可见视区(visual viewport)左上角 的 XY 坐标,不包括滚动偏移
    force: 1                     
        -->用户对触摸平面的压力大小,是一个从 0.0(没有压力) 到 1.0(最大压力) 的浮点数
    identifier: 0                
        -->此 Touch 对象的唯一标识符,可以根据它来判断在触摸过程中,是否同一次触摸过程。
    pageX: 181.07693481445312    
    pageY: 176.8461456298828     
        -->触点相对于HTML文档左上角的 XY 坐标。当存在水平滚动的偏移时,这个值包含了水平滚动的偏移
    radiusX: 11.5                
    radiusY: 11.5                
        -->包围接触区域的最小椭圆的轴 (X 轴 Y 轴) 半径
    rotationAngle: 0             
        -->角度值,表示上述由radiusX 和 radiusY 描述的椭圆为了尽可能精确地覆盖用户与平面之间的接触区域而需要顺时针旋转的角度
    screenX: 1840.076904296875   
    screenY: 343.8461608886719   -->触点相对于屏幕左上角的 XY 坐标
    target: canvas.touch-canvas
    __proto__: Touch
length: 1
__proto__: TouchList

实例 - 选项卡切换

<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        html,body{
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            font-size: 0.2rem;
            overflow: auto;
        }
        .container{
            height: 90%;
            margin: 0.2rem;
            border: 1px solid black;
        }
        .navigator{
            width: 100%;
            height: 10%;
            color: white;
            display: flex;
            justify-content: space-evenly;
            align-items: center;
            flex: auto;
        }
        .navigator > div{
            width: 100%;
            height: 100%;
            background-color: black;
            display: flex;
            justify-content: space-evenly;
            align-items: center;
        }
        .navigator > div:nth-child(1){
            background-color: white;
            color: black;
        }
        .content{
            width: 100%;
            height: 80%;
            margin: 0;
            padding: 0;
            list-style-type: none;
            position: relative;
        }
        .content li{
            height: 100%;
            position:absolute;
            padding: 0.2rem;
            border-top: 0;
            display: none;
            text-align: center;
        }
        .content > li:nth-child(1){
            display: block;
        }
    </style>
</head>
<body>
    <section id="container" class="container">
        <nav id="navigator" class="navigator">
            <div>1</div>
            <div>2</div>
            <div>3</div>
        </nav>
        <ul id="content" class="content">
            <li>1<br>Lorem ipsum dolor sit amet consectetur adipisicing elit. Corrupti vitae reprehenderit atque blanditiis officia reiciendis inventore omnis ad! Reiciendis numquam rem nulla asperiores ut recusandae magnam dolores est officia nemo.</li>
            <li>2<br>Lorem ipsum dolor sit amet consectetur adipisicing elit. Corrupti vitae reprehenderit atque blanditiis officia reiciendis inventore omnis ad! Reiciendis numquam rem nulla asperiores ut recusandae magnam dolores est officia nemo.</li>
            <li>3<br>Lorem ipsum dolor sit amet consectetur adipisicing elit. Corrupti vitae reprehenderit atque blanditiis officia reiciendis inventore omnis ad! Reiciendis numquam rem nulla asperiores ut recusandae magnam dolores est officia nemo.</li>
        </ul>
    </section>
</body>
<script>
    //计算fontSize适配移动段 - 以375px为基准,同时设置fontSize为100px基础
    document.documentElement.style.fontSize = document.documentElement.clientWidth / 375 * 100 + "px";
    let btns = document.querySelectorAll("#navigator > div");
    let tabs = document.querySelectorAll("#content > li");
    //按键切换
    Array.from(btns).forEach(function(item,index){
        item.addEventListener("touchstart",function(){
            Array.from(btns).forEach((ele,i) =>  {
                ele.style.backgroundColor = "black"
                ele.style.color = "white";
                ele.removeAttribute("id");
                tabs[i].style.display = "none";
            });    
            this.style.backgroundColor = "white";
            this.style.color = "black";
            //内容页与按键同步切换
            tabs[index].style.display = "block";
        });
    })
</script>
</html>

移动短选项卡切换.gif

实例 - canvas 写字板(触控/手写笔)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    *{
        margin: 0;
        padding: 0;
    }
    html,body{
        width: 100%;
        height: 100%;
    }
    .touch-canvas{
        display: block;
        /* margin: auto; */
        /* margin-top: 5vh; */
        background-color: lightcoral;
    }
</style>
<body>
    <!-- 画布的尺寸、位置(包括margin等)只能由自身设置决定,不能再style中设置,否则会造成落笔偏差 -->
    <canvas class="touch-canvas" ></canvas>
</body>
<script>
    //获取元素
    let touchCanvas = document.querySelector(".touch-canvas");

    //移动段样式适配
    document.documentElement.fontSize = document.documentElement.clientWidth / 375 * 100 + "px";  //计算fontSize以适配移动端
    touchCanvas.width = document.documentElement.clientWidth;
    touchCanvas.height = (document.documentElement.clientHeight * 0.5); //占据屏幕宽的50%
    console.log(touchCanvas.width)
    //创建2D画布画笔
    let drawPlain = touchCanvas.getContext("2d");
    function draw(drawPlain,xStart,yStart,xEnd,yEnd){
        drawPlain.beginPath();
        drawPlain.strokeStyle = "purple";
        drawPlain.lineWidth = 3;
        drawPlain.moveTo(xStart,yStart);
        drawPlain.lineTo(xEnd,yEnd);
        drawPlain.stroke();
    }
    //判断是否需要作画,x,y为作画点
    let isDrawing = false;
    let x,y;
    //触屏作画准备,明确起点
    touchCanvas.addEventListener("touchstart",function(evt){
        x = evt.touches[0].pageX;
        y = evt.touches[0].pageY;
        isDrawing = true;
    });
    //触屏移动开始作画,实时更新触屏点
    touchCanvas.addEventListener("touchmove",function(evt){
        if (isDrawing === true) {
            draw(drawPlain,x,y,evt.touches[0].pageX,evt.touches[0].pageY);
            x = evt.touches[0].pageX;
            y = evt.touches[0].pageY;
        } //else none
        evt.preventDefault();
    });
    //拿起或中断,结束作画
    touchCanvas.addEventListener("touchend",function(evt) {
        if (isDrawing === true) {
            x = evt.changedTouches[0].pageX;
            y = evt.changedTouches[0].pageY;
            isDrawing = false;
        } //else none
        console.log("Last Stroke Location:"+ x + y);
    });
    touchCanvas.addEventListener("touchcancel",function(evt){
        if (isDrawing === true) {
            x = evt.changedTouches[0].pageX;
            y = evt.changedTouches[0].pageY;
            isDrawing = false;
        } //else none
        console.log("Last Stroke Location:"+ x + y);
    });
</script>
</html>

Canvas写字板 触控事件.gif

表单类事件

focus 聚焦 blur 失焦

  • 触发:元素获取/失去焦点时触发,这两个事件不可取消,也不会冒泡。

  • 区别 focusin:focus 和 focusin 区别仅在于后者会事件冒泡。因此,focus 事件有两个可以实现事件委托的方法:通过在支持的浏览器上使用 focusin (en-US) 事件,或者通过设置 addEventListener() 的参数useCapture 值为 true。

  • 区别 focusout:blur 和 focusout 区别仅在于后者会事件冒泡。

input 输入 change 改变

  • 触发:适用于启用 contenteditable 的元素,以及开启了 designMode 的任意元素,尤其是 <input> <select> <textarea> 元素的 value 被修改时,会触发 input 事件,change 事件则是在输入完成并提交之后才会触发,因此并不是每次元素的 value 改变时都会触发。

  • 属性 target:在contenteditable 和 designMode 的情况下,事件的 target 为当前正在编辑的宿主。如果这些属性应用于多个元素上,当前正在编辑的宿主为最近的父节点不可编辑的祖先元素。

  • 区别 change:对于 type=checkbox 或 type=radio 的 input 元素,每当用户切换控件时,input 事件因为兼容性可能不会触发,建议 change 事件代替这些类型的元素。每当元素的 value 改变,input 事件都会被触发。这与 change事件不同。change 事件仅当 value 被提交时触发,如按回车键,从一个 options 列表中选择一个值等。

submit 提交

  • 触发条件(1)点击提交:点击提交按钮 <button><input type="submit">(2)回车提交:表单输入后,按下 Enter 键。如果直接调用 form.submit() 方法时,事件不会发送到表单。

  • timeStamp 属性:记录事件发生时的时间戳,常常记录表单的提交时间

reset 重置

  • 触发条件:表单 form 被重置时触发reset事件

  • timeStamp 属性:记录事件发生时的时间戳,常常记录表单的提交时间

浏览器事件(见BOM对象)

  • window.onload 事件,让页面资源加载完毕之后,再执行事件内部代码,当JavaScript加载早于页面的的时候,就能够保证页面不会出现js已经加载完,页面还没有显示完的情况
  • window.onresize 窗口大小改变的时候执行,媒体查询常用
  • window.onscroll 窗口滚动的时候执行对应函数