上一篇👉: 浏览器同源策略
目录
- 浏览器事件机制
- 1.事件概念及模型
- 事件模型
- 2.事件冒泡
- 3.事件委托
- (1)事件委托的概念
- (2)事件委托的特点
- (3)局限性
- (4)优化建议
- 使用场景示例
- 4.同步和异步的区别
- **同步(Synchronous)**
- **异步(Asynchronous)**
- 概括对比
- 5.对事件循环的理解
- 6. 事件触发过程详解
- 1. **捕获阶段(Capturing Phase)**
- 2. **目标阶段(Target Phase)**
- 3. **冒泡阶段(Bubbling Phase)**
- 特殊情况
- `addEventListener`方法参数
- 阻止事件传播
- 示例代码
- 7.宏任务和微任务
- 微任务(Microtasks)
- 宏任务(Macrotasks)
- 8. 什么是执行栈
- 执行栈的工作原理
- 1. 函数调用
- 2. 函数执行
- 3. 函数返回
- 实际应用中的体现
- 错误追踪
- 递归与栈溢出
- 理解执行栈的重要性
- 9. Node 中的 Event Loop 和浏览器中的有什么区别
- Node.js 中的 Event Loop 阶段
- 浏览器中的 Event Loop
浏览器事件机制
事件是浏览器中一种重要的交互机制,它允许JavaScript响应用户操作(如点击、移动鼠标)或页面自身的状态变化(如加载完成、滚动)。事件处理是前端开发中的基础,理解事件模型对于构建动态和交互式网页至关重要。
1.事件概念及模型
- 事件(Event):用户与网页交互(如点击按钮)或网页状态改变(如页面加载完毕)时触发的行为。
- 事件对象(Event Object):每当事件发生时,浏览器都会创建一个事件对象,封装了事件相关的详细信息,如事件类型(click、mousemove)、事件目标(触发事件的元素)、事件时间戳等,还提供了阻止默认行为(preventDefault())和阻止事件传播(stopPropagation())等方法。
事件模型
浏览器普遍支持以下三种事件模型:
- DOM0级事件模型
- 特点:最简单的事件处理方式,直接将事件处理函数赋值给元素的事件属性。如element.onclick = function() {…};
- 传播:不存在事件流(即捕获和冒泡阶段),事件直接在目标元素上触发。
- 兼容性:所有浏览器均支持,但功能有限,不能同时绑定多个相同类型的事件处理器。
- IE事件模型(也称为DOM1级事件模型)
- 特点:通过element.attachEvent(‘on[eventName]’, handler);来添加事件监听器。
- 事件流:分为两个阶段:事件处理阶段(先触发目标元素的事件处理函数),然后是事件冒泡阶段(从目标元素开始,向外层元素传播)。
- 兼容性:主要是旧版IE浏览器支持,现代浏览器已逐渐弃用。
- DOM2级事件模型
- 特点:引入了addEventListener和removeEventListener方法来添加和移除事件监听器,支持事件捕获、目标、冒泡三个阶段。
- 事件流:
- 捕获阶段:从根节点开始,逐层向下直到目标元素,检查是否有对应的捕获阶段事件处理函数。
- 目标阶段:到达目标元素,触发该元素的事件处理函数。
- 冒泡阶段:从目标元素开始,逐层向上,直至文档根节点,触发沿途元素的冒泡阶段事件处理函数。
- 参数:addEventListener接受三个参数,分别为事件类型、处理函数和一个布尔值(true表示在捕获阶段触发,false表示在冒泡阶段触发,默认为false)。
- 兼容性:所有现代浏览器均支持,是目前推荐使用的事件模型。
DOM2级事件模型提供了更为灵活和强大的事件处理能力,支持事件捕获、目标和冒泡三个阶段,是开发复杂交互应用的首选模型。
2.事件冒泡
在处理浏览器事件时,阻止事件冒泡是控制事件流向、避免不必要的父元素响应事件的一种常见需求。不同浏览器(主要是IE与其他标准浏览器)在实现上有所不同,下面是阻止事件冒泡的两种通用方法:
对于W3C标准浏览器(如Chrome、Firefox、Safari和Edge等)
使用event.stopPropagation()
方法。此方法属于DOM Level 2事件模型的一部分,可以在事件处理函数内部调用,以阻止当前事件向上冒泡到父元素。
element.addEventListener('click', function(event) {event.stopPropagation(); // 阻止事件向上冒泡// 你的事件处理逻辑
}, false);
对于Internet Explorer(特别是IE8及以下版本) IE已经再见了
👉: 微软IE浏览器于6月15日正式停止支持
由于早期版本的IE不完全支持W3C的事件模型,需要使用event.cancelBubble属性来阻止事件冒泡。
element.attachEvent('onclick', function(event) {event = event || window.event; // 兼容性处理,确保event存在event.cancelBubble = true; // 阻止事件冒泡// 你的事件处理逻辑
});
兼容性处理:在实际开发中,为了确保跨浏览器兼容性,通常会同时使用这两种方法:
function stopEventBubble(e) {e = e || window.event; // 兼容性处理if (e.stopPropagation) { // W3C标准方法e.stopPropagation();} else { // IE兼容性e.cancelBubble = true;}
}// 绑定事件
var element = document.getElementById('yourElement');
if (element.addEventListener) {element.addEventListener('click', function(e) {stopEventBubble(e);// 事件处理逻辑}, false);
} else if (element.attachEvent) {element.attachEvent('onclick', function(e) {stopEventBubble(e);// 事件处理逻辑});
}
3.事件委托
事件委托是一种JavaScript编程技巧,它充分利用了DOM事件流(特别是事件冒泡阶段)的特性,允许开发者在父元素上一次性为多个子元素绑定事件处理器,而不是在每个子元素上单独绑定。以下是您所提供内容的整理和补充,以便更好地理解事件委托的概念、特点、局限性及使用场景
(1)事件委托的概念
事件委托的核心思想在于,当事件在DOM树中从目标元素向上逐级传播(即事件冒泡)时,父元素可以捕捉到这些事件,并通过检查事件的实际目标(event.target)来决定是否执行特定的处理逻辑。这种方法可以减少事件监听器的数量,提高页面性能,尤其是处理大量动态生成的元素时非常有用。
(2)事件委托的特点
- 减少内存消耗:避免了为大量子元素单独绑定事件处理器,从而节省内存资源。
- 动态绑定事件:适用于动态添加或删除子元素的场景,无需为新添加的元素手动绑定事件处理器,也不需要在元素移除时清理监听器。
- 灵活性与可扩展性:通过在事件处理函数中判断目标元素,可以实现更加灵活的事件处理逻辑,易于维护和扩展。
(3)局限性
- 不适用于无事件冒泡的事件,例如focus和blur。
- 对于频繁触发且需要精确位置计算的事件(如mousemove),直接在目标元素上绑定事件可能更优,以避免每次事件触发时遍历DOM带来的性能开销。
- 使用不当可能会影响页面性能,尤其是在深度嵌套的DOM结构中频繁使用事件委托或绑定过多的事件处理器
(4)优化建议
- 限制使用范围:仅在确实需要的场景应用事件针对性委托,如频繁更新的动态内容区域。
- 减少层级深度:尽量将事件处理器绑定在靠近目标元素的父级上,避免在body或document级别绑定过多事件。
- 合并事件处理:将多个事件类型或逻辑相近的处理逻辑合并到同一个事件委托中,通过检查事件类型或目标元素等条件分发处理,减少事件监听器的数量。
使用场景示例
考虑一个需求,需要给页面中所有<a>
标签添加点击事件。直接在每个<a>
上绑定事件处理器不仅繁琐,而且对于动态添加的链接无效。采用事件委托的方式,可以在document或一个包含所有链接的共同父元素上设置一个监听器,检查触发事件的目标元素是否为<a>
标签,如果是,则执行相应的操作。这样既简化了管理,又支持了动态内容的事件处理。
document.addEventListener("click", function(e) {var node = e.target;while (node !== document) { // 更改为 document 以确保最终跳出循环if (node.nodeName === "A") {console.log("a clicked");break;}node = node.parentNode;}
}, false);
这段代码展示了如何在点击事件发生时,从触发事件的元素开始,逐级向上遍历其父元素直至document,检查途中是否遇到了<a>
元素,并作出相应处理。这正是事件委托机制的典型应用。
4.同步和异步的区别
同步(Synchronous)
在同步操作情境下,进程在执行某项请求时,若该请求需要一定时间来完成,进程会直接等待这个请求的结果返回,之后才继续执行接下来的任务。简而言之,同步执行保证了操作的顺序执行,但可能引起等待期间的效率问题。
异步(Asynchronous)
相比之下,异步处理方式则允许进程在发起一个可能耗时的操作后,无需等待其完成,即可继续执行后续代码。当该操作最终完成时,系统将以事件、回调、Promise或async/await等方式通知进程,进程随后在适当时间点处理完成事件的结果。异步机制提高了程序的并发能力和响应速度,尽管它要求更复杂的编程模型来管理任务的非顺序完成。
概括对比
- 同步确保操作按序执行,简单直观,但可能导致程序在I/O等操作上阻塞等待。
- 异步通过非阻塞执行优化了资源利用和响应时间,适合处理耗时任务,但增加了编程逻辑的复杂性。
5.对事件循环的理解
JavaScript的事件循环(Event Loop)机制是其异步编程模型的核心,它确保了单线程环境下能够高效处理异步操作而不阻塞主线程。
JavaScript引擎的执行环境基于以下主要组件运作:
- 执行栈(Call Stack):保存当前执行的函数调用信息,遵循后进先出(LIFO)原则。
- 任务队列(Task Queues):分为两大类,宏任务(Macrotask)队列和微任务(Microtask)队列。异步操作完成后,其回调会被推入相应的队列。
- 事件循环(Event Loop):负责监控执行栈和任务队列,当执行栈为空时,从队列中取出任务执行。
事件循环执行步骤
- 执行全局脚本:这是第一个宏任务,包括所有同步代码。
- 检查微任务队列:执行完当前宏任务后,事件循环会检查微任务队列。如果有微任务,全部执行完毕后再继续。微任务包括:Promise的回调、MutationObserver、process.nextTick(Node.js)等。
- 渲染页面(可选):在某些环境中,如浏览器,在一个宏任务周期结束且所有微任务执行完毕后,有机会进行UI渲染。
- 执行宏任务:从宏任务队列中取出下一个任务执行。常见的宏任务来源有:setTimeout、setInterval、I/O、UI渲染更新、script(整体代码)等。
- 重复循环:上述过程构成一个事件循环周期,不断重复,直到宏任务和微任务队列均为空,或浏览器/Node.js环境决定终止。
重要点强调
- 微任务优先级高于宏任务:在一个宏任务结束后,会立即执行完当前所有的微任务,然后再执行下一个宏任务。
- 避免微任务滥用:虽然微任务能实现快速响应,但过度使用或复杂的微任务链可能会导致页面冻结或性能问题。
- 渲染机会:在事件循环中,页面渲染通常发生在每个宏任务结束且微任务队列清空之后,但这取决于具体的JavaScript运行环境(如浏览器)。
6. 事件触发过程详解
事件在DOM中的传播遵循三个明确的阶段:
1. 捕获阶段(Capturing Phase)
- 事件从
Window
对象开始,沿着DOM树向下传播至目标元素。沿途注册了捕获阶段事件监听器的元素将按顺序触发这些监听器。
2. 目标阶段(Target Phase)
- 事件到达目标元素,触发该元素上所有注册的事件监听器,无论是捕获、目标还是冒泡阶段的监听器。
3. 冒泡阶段(Bubbling Phase)
- 事件从目标元素开始,反向传播回DOM树的顶层(
Window
对象)。这一路上,所有注册了冒泡阶段事件监听器的元素都将触发这些监听器。
特殊情况
- 若在同一个元素上同时注册了冒泡和捕获事件,它们的触发顺序遵循注册的顺序,而不是严格按照捕获->目标->冒泡的阶段划分。
addEventListener
方法参数
- 第三个参数决定事件处理的阶段:
- 布尔值:
true
表示捕获阶段,false
或省略默认为冒泡阶段。 - 对象:提供更多选项,如:
capture
: 类似布尔值参数,指定事件是否在捕获阶段触发。once
: 设为true
,监听器只执行一次后即被移除。passive
: 设为true
,表明监听器永远不会调用preventDefault
。
- 布尔值:
阻止事件传播
event.stopPropagation()
:阻止事件在当前阶段继续向上或向下传播,不影响同阶段内其他监听器。event.stopImmediatePropagation()
:不仅阻止事件传播,还阻止目标元素上其他相同类型的事件监听器执行。
示例代码
// 先打印"冒泡",再打印"捕获"
const node = document.querySelector('#someNode');
node.addEventListener('click', event => console.log('冒泡'), false);
node.addEventListener('click', event => console.log('捕获'), true);// 使用 `stopImmediatePropagation` 阻止其他相同事件触发
node.addEventListener('click', event => {event.stopImmediatePropagation();console.log('仅此一次');
}, false);// 即使点击,也不会执行下方的监听器,因为上方的已阻止
node.addEventListener('click', event => console.log('不会执行'), false);
7.宏任务和微任务
微任务(Microtasks)
微任务在当前执行栈结束后,且浏览器渲染前执行,具有较高优先级,确保快速处理与响应。主要包括:
-
Promise 回调
-
当
Promise
状态从pending
变为fulfilled
或rejected
时,触发的.then
、.catch
和.finally
回调函数。 -
Node.js 中的
process.nextTick
- 用于将回调函数推迟到当前执行栈的末尾,但仍在下一个事件循环迭代开始前执行。
-
DOM 变更观察(MutationObserver)
- 监听 DOM 树结构变化,并在变化发生后执行注册的回调函数。
宏任务(Macrotasks)
宏任务构成了 JavaScript 代码执行的主框架,按照顺序执行,并在每个宏任务执行后检查微任务队列。主要包括:
-
脚本执行(Script)
- 加载的 JavaScript 文件或
<script>
标签内的代码块执行。
- 加载的 JavaScript 文件或
-
setTimeout
与setInterval
- 分别用于在指定延迟后或每隔固定时间执行函数。
-
Node.js 中的
setImmediate
- 安排回调函数在当前 I/O 循环的下一次迭代开始时执行。
-
I/O 操作
- 包含文件读写、网络请求等,通常以异步方式进行,并通过回调通知完成情况。
-
UI 渲染
- 浏览器在执行完当前宏任务后,可能会进行界面重绘和样式计算的更新。
8. 什么是执行栈
执行栈,又称调用栈或调用堆栈,是编程语言(尤其是JavaScript)中用于追踪函数调用顺序的核心数据结构。它遵循后进先出(LIFO, Last In First Out)原则,如同一个顶部开口的盒子,最近放入的元素最先被取出。
执行栈的工作原理
1. 函数调用
- 每当一个函数被激活执行时,其执行上下文(包括局部变量、参数、函数体等信息)会被封装成一个栈帧(Stack Frame),并压入执行栈的顶部。
2. 函数执行
- 执行栈顶部的函数开始执行。如果此函数内部又调用了其他函数,被调用函数的栈帧会继续被压入栈顶。
3. 函数返回
- 当一个函数执行完毕,其对应的栈帧会从栈顶弹出,控制权返回到调用它的函数,继续执行剩余代码,直至栈为空。
实际应用中的体现
错误追踪
- 当JavaScript程序发生异常时,错误堆栈(Error Stack)提供了一条从异常发生点回溯至最初调用的完整路径,极大方便了调试过程。
递归与栈溢出
- 递归是执行栈的一个典型应用案例,但过度递归可能导致栈空间耗尽,引发栈溢出错误(Stack Overflow)。合理设置递归终止条件或采用迭代等策略是避免此类问题的关键。
理解执行栈的重要性
- 优化递归逻辑:避免因递归过深引起的栈溢出,通过设计合理的递归终止条件或转换算法逻辑为迭代形式。
- 高效调试:利用错误堆栈快速识别错误发生的上下文,缩短问题定位时间。
- 异步编程理解:虽然异步操作并不直接增加执行栈的负担,但其回调函数会在微任务或宏任务队列处理时进入执行栈,深入理解这一机制对于掌握JavaScript的异步编程模型至关重要。
9. Node 中的 Event Loop 和浏览器中的有什么区别
Node.js 中的 Event Loop 和浏览器环境下的 Event Loop 主要区别在于它们的设计目标不同,导致它们的实现细节有所差异,尤其是在事件循环的具体阶段划分上。以下是关键区别和process.nextTick的执行顺序说明:
Node.js 中的 Event Loop 阶段
Node.js 的 Event Loop 包含以下六个阶段,这些阶段按顺序循环执行:
- Timers - 处理setTimeout和setInterval的回调。
- Pending callbacks - 执行非I/O相关的待处理回调,如setImmediate(在某些旧版本中)。
- Idle, Prepare - 内部使用,通常不需要关注。
- Poll - 监听新的I/O事件;执行setImmediate(当前版本的主要处理位置)。
- Check - 调用setImmediate的回调。
- Close callbacks - 如Socket的’close’事件回调。
浏览器中的 Event Loop
相比之下,浏览器的Event Loop主要围绕UI渲染、用户交互和Web API(如setTimeout, fetch, addEventListener等)设计,其模型相对简单,主要关注于执行宏任务(macro tasks)和在每个宏任务结束后立即清空微任务(micro asks)队列。
tprocess.nextTick 特性
- 独立于Event Loop阶段:process.nextTick在Node.js中是一个特殊的机制,它不直接属于Event Loop的任何一个阶段。每当当前执行的阶段即将结束时,如果有process.nextTick队列,Node.js会清空这个队列中的所有回调,然后执行它们,之后再进入下一个阶段或者处理微任务。
- 高于微任务的优先级:尽管process.nextTick和Promise的.then回调(微任务)都属于非阻塞操作,但process.nextTick回调会在当前阶段的末尾、微任务之前被执行,这使得它具有更高的优先级 。
- 连续执行:如果在process.nextTick的回调中又调用了process.nextTick,这些新的回调会累积在同一阶段的末尾执行,而不是分散到不同的循环迭代中。
示例分析
process.nextTick的执行顺序体现了它的高优先级特性:
setTimeout(() => {console.log('timer1');Promise.resolve().then(function() {console.log('promise1');});
}, 0);
process.nextTick(() => {console.log('nextTick');// 连续调用nextTick形成递归,这些递归调用会依次执行
})
输出会是连续的’nextTick’,然后再是’timer1’和’promise1’。这是因为process.nextTick的回调在每个阶段结束时执行,且在微任务(如Promise回调)之前,故会先连续打印所有nextTick的回调内容,之后才是setTimeout和Promise的微任务。
理解这些细微差别对于编写高性能、响应迅速的Node.js应用程序至关重要,尤其是在处理高并发I/O操作和复杂的异步逻辑时。
总之,Node.js与浏览器环境下的Event Loop机制虽有共通之处,但各自的设计目的和应用场景导致它们在具体实现细节上展现出显著差异。Node.js的Event Loop以其独特的六阶段循环,精细化地处理各种异步操作,尤其强调了I/O密集型任务的高效管理。而浏览器的Event Loop则更加侧重于UI渲染和用户交互的即时响应,确保网页的流畅体验。
process.nextTick
作为Node.js中的一项特殊机制,凭借其超乎微任务的执行优先级和即时响应能力,为开发者提供了处理高优先级异步逻辑的强有力工具。它不拘泥于Event Loop的特定阶段,而是在每个阶段结束前清空自身的回调队列,确保了高度的灵活性和控制力,但同时也要求开发者审慎使用,以免因过度使用导致的栈溢出或性能瓶颈。
理解这些底层机制对于优化代码执行效率、避免潜在的异步陷阱以及编写更加健壮和可维护的JavaScript应用至关重要。不论是服务端的Node.js环境还是客户端的浏览器环境,深入掌握Event Loop及其相关异步处理模型,都是提升开发者技能树中不可或缺的一环。通过合理安排宏任务与微任务、明智地选用诸如setTimeout、setImmediate、process.nextTick及Promise等工具,可以有效地构建出既高性能又具备良好用户体验的现代Web应用。