前端最基础的就是 HTML+CSS+Javascript。掌握了这三门技术就算入门,但也仅仅是入门,现在前端开发的定义已经远远不止这些。前端小课堂(HTML/CSS/JS),本着提升技术水平,打牢基础知识的中心思想,我们开课啦(每周四)。

前面我们已经基本掌握常规的语法语义,以及基本的使用方法。接下来我们讲深入进去了解其中内在的原理。

今天我们要讲什么?

  1. 事件机制
  2. 事件对象(Event)
  3. event loop

DOM (与事件的关系,看不看无所谓)

既然 DOM 有版本,那么在他的环境上事件的支持也是有版本的。文档

DOM 事件(0 级)

body.onclick 这种定义方式的。

  1. 不可以多次监听事件,因为是赋值的方式,下次赋值会覆盖。
  2. 只可以在冒泡阶段触发

DOM 事件(2 级)

addEventListener 方式定义的。

  1. 可以多次监听,切按监听顺序执行回调(有序)
  2. 取消监听需要同一引用的函数。举个栗子

        // 错误案例,两个方法不是同一引用,导致清除不掉
        document.addEventListener('click', function(){})
        document.removeEventListener('click', function(){})
    
        // 正确案例,同一引用,可以清除。
        function documentClick(){}
        document.addEventListener('click', documentClick)
        document.removeEventListener('click', documentClick)
    
  3. 可以选择触发阶段(冒泡&捕获) capture

事件机制

标准事件:EMCAScript 标准规定事件流包含三个阶段,分别为事件捕获阶段目标阶段事件冒泡阶段
先存个代码,之后的例子我们用这个例子。测试看我这里的 DEMO

<html onclick="alert('html')">
    <body onclick="alert('body')">
        <a onclick="alert('a')">click</a>
    </body>
</html>

事件捕获阶段

捕获阶段:由外到内,触发规律为 html > body > a
如果想在捕获阶段就触发,需要传入参数 {capture: true}

事件冒泡阶段

冒泡阶段:由内到外,触发规律为 a > body > html
这个阶段执行是 W3C 默认的,等价于 {capture: false}

事件执行顺序


图片来源-https://www.w3.org/TR/DOM-Lev...
事件的捕获阶段 > 处于目标阶段 > 事件的冒泡阶段 > 事件的默认行为
这里为什么要强调这个顺序呢?

  1. 因为默认行为是在最后面,所以我们都可以用 e.preventDefault() 来阻止。
  2. 基于上条的阻止默认事件。在移动端滑动时,阻止默认事件需要手动设置 passivefalse
    passive: Boolean,设置为 true 时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
  3. 我们真正单击的元素事件触发不在冒泡和捕获阶段,而在目标阶段触发DEMO-冒泡&捕获阶段触发事件,可以看到,他是通过定义时的先后顺序来触发的。

事件对象(Event)

Event 对象--mdn

事件对象(属性&方法)

bubblesboolean是否冒泡
cancelableboolean是否可以取消的默认动作。
currentTargetElement返回其事件监听器触发该事件的元素。(this 的真实指向)
eventPhaseIntenger返回事件传播的当前阶段
targetElement返回触发此事件的元素。(事件的目标节点)
timeStampDate触发的时间戳
typeString事件名称。
isTrustedboolean该事件是否是浏览器生成(true 代表是,false 代表是开发人员创建
preventDefaultFunction取消事件的默认行为在 cancelable=true 时有效
stopPropagationFunction取消事件的捕获或者冒泡行为在 bubbles=true 时有效
  1. IE: event.cancelBubble=true; //阻止事件冒泡
  2. IE: event.returnValue=false; //阻止事件的默认行为
  3. 获取事件 window.event

事件类型(分类、Event对象之类)

DOM event 子类,根据不同的事件类型,返回的对象会有些许不同,比如 Mouse 类型的,就会有单击坐标之类的。 KeyboardEvent 之类的就会有按键之类的。

new 一个事件对象

CustomEvent() --mdn

document.body.onclick=function(e){console.log(e)}
var btn=document.body;
var event= new CustomEvent("click");
btn.dispatchEvent(event);

其实这里我们可以自定义事件的名称,然后我们就可以实现一个发布订阅的功能

document.addEventListener("bus", function(e) { console.log(e, e.detail) });
var event = new CustomEvent("bus", {detail: {LN_type: 'lilnong.top'}});
document.dispatchEvent(event);

event loop (事件循环)

首先,我们要牢记一件事情 js 是单线程
Event Loop 中文叫事件循环。是浏览器内部的一种机制,javaScript 单线程运行时如何不阻塞 UI
Javascript 有一个 main thread 主线程call-stack 调用栈(执行栈),所有的任务都会被放到调用栈(栈采用的是后进先出的规则)等待主线程执行。

任务类别&任务队列(Task Queue)

JavaScript 中,任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)

MacroTask(宏任务)

<script>setTimeoutsetIntervalsetImmediateI/OUI Rendering
异步任务会在有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

MicroTask(微任务)

Process.nextTick(Node独有)、PromiseMutationObserver
每个宏任务执行完毕后,会检查 microTask 队列是否有回调,会按照先入先出的规则执行,都执行完再执行宏任务,如此循环。

调用栈

栈采用的是后进先出的规则,这里我们调用 a()a() 内部会调用 aa(), aa() 内部又调用 aa()

function a(){return aa()}
function aa(){return aaa()}
function aaa(){return 1}
  1. a 进栈
  2. aa 进栈
  3. aaa 进栈
  4. aaa 出栈
  5. ...

事件循环的进程模型

  1. 选择任务队列中最先进入的任务,如果任务队列为空,则执行跳转到微任务(MicroTask)的执行步骤
  2. 任务设置为已选择任务
  3. 执行任务
  4. 任务设置为空
  5. 运行完成的任务从任务队列中删除
  6. MicroTasks 步骤:

    1. 进入 MicroTask 检查点
    2. 设置 MicroTask 检查点标志为 true
    3. 当事件循环 MicroTask 不为空时:

      1. 选择最先进入队列的任务,
      2. 设置为已选择的任务
      3. 运行
      4. 将已经执行完成的 MicroTask 改变状态
      5. 移出 MicroTask
    4. 清理IndexDB事务
    5. 设置 MicroTask 检查点的标志为false。
  7. 更新界面渲染。
  8. 返回第一步。

举个栗子(常问无聊题)

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

new Promise(function(reslove){
    console.log('Promise-start')
    reslove();
}).then(function() {
  console.log('Promise-end');
})
console.log('script end');

结构应该没错

  1. 任务入栈(代码块)
  2. console.log('script start'); 栈中,同步代码,直接输出
  3. function() {console.log('setTimeout');}MacroTask
  4. new Promise 同步代码,执行
  5. 入栈 function(reslove){console.log('Promise-start');reslove();}
  6. 执行 console.log('Promise-start');
  7. 出栈
  8. .then(function() {console.log('Promise-end');})MicroTask
  9. console.log('script end'); 同步代码,输出
  10. 当前执行完出栈,判断 MicroTasks
  11. 执行 console.log('Promise-end');
  12. 完成所有 MicroTasks
  13. 渲染 UI
  14. MacroTasks是否有数据?
  15. 执行 MacroTasks 中第一个。
  16. console.log('setTimeout'); 输出。

异步事件(消息)

  1. DOM 事件
  2. setTimeout
  3. XHR
  4. Promise

总结

  1. 事件机制

    1. 当前执行块
    2. 当前执行块的微任务队列
    3. 宏任务队列
  2. Event 事件级别
  3. addEventListener 要主要保存 function 的引用,用于解绑
  4. 队列,先进先出(想起了梗,吃多了拉)
  5. 堆栈,先进后出(想起了梗,吃多了吐)
  6. 触发阶段 捕获>目标>冒泡
  7. Event 对象,针对不同的类型,有自己独特的属性。

微信公众号:前端linong

初级阶段文章目录

  1. 前端培训-初级阶段(17) - 数据存储(cookie、session、stroage)
  2. 前端培训-初级阶段(13) - 正则表达式
  3. 前端培训-初级阶段(13) - 类、模块、继承
  4. 前端培训-初级阶段(13) - ECMAScript (内置对象、函数)
  5. 前端培训-初级阶段(13) - ECMAScript (语法、变量、值、类型、运算符、语句)
  6. 前端培训-初级阶段(13、18)
  7. 前端培训-初级阶段(9 -12)
  8. 前端培训-初级阶段(5 - 8)
  9. 前端培训-初级阶段(1 - 4)

资料

  1. 前端培训目录、前端培训规划、前端培训计划
  2. JavaScript系列----事件机制
  3. 事件参考--mdn
  4. tasks-microtasks-queues-and-schedules
  5. 一次弄懂Event Loop(彻底解决此类面试问题) --光光同学-juejin
03-05 21:58