JS事件深度解析一、二 事件的综述 事件的完整生命周期 进阶 JS事件全链条 事件循环 事件传播 循环和异步 成为事件达人

141 阅读47分钟

前言

这是一篇详细讲述事件跌宕起伏一生的进阶好文, 文笔优美,情节曲折,悬念迭起,引人入胜,量大管饱,恳请大家一键三连,多多支持。谢谢谢谢。

本文分五部分:

事件的综述

事件的完整生命周期

事件的传播和处理

事件的循环和异步

各种事件类型详解

**码字不易,但知识的传播更重要,欢迎全文转载,请勿摘录片段。**

一、 事件的综述

首先需要了解几个术语:

  • 宿主环境:将js引擎作为一个组件包含在内,并且为它提供运行所需的资源的外部系统

就是说,宿主环境提供了所有的资源,比如网络 文件 渲染 各种功能接口等等,没有了宿主环境, js引擎就是光杆司令,它就只能空转,做不了任何事情。

  • 宿主对象:所有不是由 JS 语言本身定义的、而是由环境提供的对象/功能/api,都叫宿主对象。

比如 Fetch document XMLHttpRequest 等等 由宿主环境提供的都叫宿主对象。

而js语言定义的对象,比如 Array, Object, Promise, Math, Map 等等,是js的原生内置对象。

我们常说的 事件循环 任务队列 都是由宿主环境提供并且管理的。

说 js能怎样怎样,实际上是 js语言的能力+宿主环境赋予的能力 。


  1. 事件的核心定义

    JavaScript 中的事件是宿主环境提供的一套标准化的异步消息分发机制,是系统内发生的、可被代码侦测到的“发生”或“信号” 。

    事件是一种能力,那么是不是所有的对象 所有的元素 所有的节点,都具备事件的能力呢?

    并不是所有, 这里就要提到一切的源头 事件目标 EventTarget

    EventTarget是一个接口 是一个对象 ,你要具备事件的能力,必须要实现这个接口。

    记得红皮书里讲那个迭代器部分,说要想具备迭代功能,必须实现迭代协议。事件也类似,

    EventTarget是宿主环境提供的一种能力 一种功能 一个对象,拥有了它 就拥有了事件的能力。

    这里要注意的是,事件 是宿主环境 也就是浏览器提供的,并不是js语言本身所有,这点很重要。

    那么,如何获得这种能力呢?

    • 继承源头

      创建一个类 , extends EventTarget ,直接获得原生的正宗事件能力。

    • 纯手工写

    • 引入其他事件库

    • 框架内置

    以上所说,是如何获得事件的能力, 而在平常的开发中,绝大部分,都是通过原型链,直接继承了EventTarget ,并不需要特地去获得。

    所以,在很多文章中,并没有提到EventTarget,因为单纯从js的角度来说,它处理的元素 节点 对象 等等 都已经通过原型链拥有了 或者通过一些框架内的自定义实现了或者封装了事件的能力。

  2. 事件的来源

    事件的来源,分两个层面,一个层面 是规范中定义的来源,灵一个层面 是浏览器具体实现的队列。

    • 规范定义

      • dom源
      • ui用户接口源
      • 网络源
      • 导航和历史源
      • 渲染源

      这是几个主要的事件来源。

    • 浏览器的具体实现

      浏览器将不同的来源的事件,映射为自己的多个任务队列,并不是完全按照规范中定义的来源来划分宏任务队列的。至于优先级,浏览器有自己的优化和调度策略 比如用户交互高优先 防鸡鹅调度打捞低优先等等。

      这些队列,一般来说是依据优先级的大小来划分。

      • 输入事件队列 通常是最高优先级

        处理用户的交互,保证用户打字 滚动 点击 没有延迟

      • 计时器事件队列 普通优先级

        settimeout 等, 有限的优先级,定时器中的回调函数都放在这里等待执行。(settimeout实际是一个浏览器提供的api函数,它是一个同步执行函数,但是做的是异步调度的工作)

      • 普通事件队列 一般默认优先级

        最常用的队列 处理逻辑的主战场 网络 文件 数据 等等

      • 空闲队列 最低优先级

        requestIdleCallback

        事件循环完全空了 没事做的时候 来这里瞄一眼。

  3. 事件和观察者模式

    观察者模式是一种软件设计模式。它定义了一种一对多的依赖关系。

    • “一” : 指的是被观察者。当它的状态发生改变时,它会对外发送通知。
    • “多” : 指的是观察者。它们一直盯着“被观察者”,一旦收到通知,就会自动执行相应的操作。

    而事件机制,是对观察者模式的一个实现。

    我们写代码时

    • DOM 节点(如 button 就是 被观察者

    • 我们写的回调函数(function() { ... } 就是 **观察者 **。

    • addEventListener 就是整个观察者模式的核心api,它安排了一个或多个观察者去盯着被观察者。

    • 被观察者状态发生改变,触发通知

  4. 事件和DOM事件

    可能还是有不少朋友对事件这个概念有疑惑。

    事件 是归属于宿主环境的,请记住js语言中 并没有事件的概念。

    事件是一个信号,是系统内发生的任何值得注意的事情。比如:键盘按下了、图片加载完了、网络断了、数据到了。。。。。。

    DOM 事件只是这个庞大信号系统中的一部分,专门负责**网页内容(文档)**层面的交互。

    之所以把DOM事件单独拿出来说,是因为它是我们在编写代码时,接触最多的一类事件。

    DOM 事件 是指发生在 HTML 文档元素(节点) 上的特定的交互瞬间。
    
    核心特征: 它们必须依附于某个 DOM 节点(如 <div>, , document)。
    
    典型场景: 用户和网页 UI 的交互。
    
    常见例子:
    
    click (鼠标点击)
    
    keydown (键盘按下)
    
    submit (表单提交)
    
    touchstart (手指触摸)
    
    
    

    那么作为对比,除了DOM事件以外,还有什么非DOM事件呢?

    A. BOM (Browser Object Model) 事件 / Window 事件
    这些事件发生在浏览器窗口层级,而不是具体的 HTML 标签上。
    
    resize: 浏览器窗口大小被改变。
    
    scroll: 页面滚动(虽然常绑定在 document,但本质是视图窗口的行为)。
    
    hashchange: URL 的锚点(#后面部分)发生变化(单页应用路由的基础)。
    
    storage: localStorage 或 sessionStorage 被修改时触发(用于跨标签页通信)。
    
    online/offline: 网络连接状态断开或恢复。
    
    B. 网络请求事件 (Network Events)
    当 JS 发起异步请求时,请求的状态变化也是事件。
    
    XMLHttpRequest (AJAX):
    
    readystatechange: 请求状态改变。
    
    progress: 下载进度。
    
    load/error/timeout: 请求成功、失败或超时。
    
    WebSocket:
    
    open, message, close, error。
    
    C. 媒体事件 (Media Events)
    专门针对 <video> 和 <audio> 对象的播放状态。
    
    play / pause: 播放/暂停。
    
    ended: 播放结束。
    
    volumechange: 音量改变。
    
    waiting: 缓冲中。
    
    D. 跨线程/跨窗口通信事件
    Web Worker: message 事件(主线程和 Worker 线程互相发消息)。
    
    iframe: message 事件(父页面和子页面通信,即 postMessage)。
    
    E. 开发者自定义事件 (Custom Events)
    这是最高级的用法。不由浏览器触发,而是由代码手动触发。
    
    使用 new CustomEvent() 创建,使用 dispatchEvent() 发送。
    
    用途: 用于组件间通信。可以手动派发一个事件,而不是依赖点击。
    

    那么,DOM事件和非DOM事件,有什么区别吗?

    DOM 事件:

    因为 DOM 结构本身是一棵树(Tree)。 当你点击一个按钮时,你不仅仅是点击了这个按钮,你同时点击了包裹它的 div,点击了 body,点击了 html,甚至点击了整个浏览器窗口。

    • 特征: 事件会在 DOM 树上“旅行”。

    • 路径: 捕获阶段(从外向内) -> 目标阶段(到达节点) -> 冒泡阶段(从内向外)。

    • 结果: 你可以在父节点(比如 div)上监听到子节点(button)的事件。这就是事件委托的基础。

    非 DOM 事件:

    比如 XMLHttpRequest(网络请求)或 Worker(线程通信)。它们的对象没有“父节点”的概念,它们是内存中独立的 JS 对象。

    • 特征: 只有目标阶段

    • 路径: 事件直接发送给该对象,触发完就结束了。它不会传给它的“上级”(因为它没有上级)。

    • 结果: 你不可能在 window 上通过冒泡监听到某个具体 ajax 请求的 load 事件(除非你自己手动去转发)。

    除了上面所说的传播机制不同,还有一个极其重要的区别:与浏览器原生行为的绑定。

    • DOM 事件: 通常带有浏览器的默认行为

      • <a rel="noopener noreferrer noopener noreferrer"> 标签的 click 会导致跳转。
      • `` 的 submit 会导致刷新页面。
      • 键盘的 keydown 会导致输入文字。
      • 因此: DOM 事件提供了 e.preventDefault() 来阻止这些行为。
    • 非 DOM 事件: 通常纯粹是信息通知

      • XHRload 只是告诉你加载完了。

      • 因此: 非DOM事件通常没有(但有例外)所谓的“默认行为”可供阻止。你调用 e.preventDefault() 没有任何意义。

  5. 事件和事件对象event

    通常来说,一个事件,之所以能成为事件, 要具有三个特质:

    • 遵循观察者模式
    • 携带事件的现场数据
    • 可观测的发生或状态的改变

    那么 携带事件的现场数据 ,这个就是要讲的event了。

    很多文章说,事件发生 比如鼠标被点击 马上就有事件对象被创建, 这个说法其实并不准确。

    严谨的描述 event 的创建时机:在事件被包装成任务 放入红任务队列排队 然后被取出开始执行,执行的第一步 是进行命中测试,确定事件发生的目标, 第二步,才是创建事件对象 。 第三步 是路径计算,确定传播路径。

    关于具体的流程,下面会详细讲。这部分作为综述,只是讲事件对象本身。

    在js层面的事件对象被创建之前,所有的相关信息,只是作为一个内存中的 c++ 结构体存在。

    那么 这里可以再给事件对象一个较为明确的定义:

    事件对象是浏览器将底层存有事件信息的 C++ 结构包装成 JS 对象,并在路径计算前完成创建,目的是为了让路径计算算法能读取其配置,并再气候的传播过程中充当一个携带现场数据及动态上下文的载体

    我们知道,以前的很长一段时间,前端的情况是 先有规范 再有实现 或者 先有实现 才有规范 或者虽有规范 但是实现不完全符合规范 ,总之是比较混乱,但是现在的情况已经好了很多,我们已经可以逐渐的信赖规范了。学习的时候 尽管实现上有些许的差别,但是可以用规范去加强理解。

    规范含义:在 ECMAScript 相关的规范中,[[ ]] 形式的名字表示一种抽象的内部插槽,它们定义了对象在语义上的内部状态或行为。它们是规范用来描述对象如何工作的术语,不是 JS 层能直接访问的普通属性。

    实现层面:js引擎和浏览器会用各种方式来实现这些规范中定义的抽象的内部插槽。

    JS 提供的可访问接口:很多内部插槽会通过公开的属性或方法提供出来(例如 event.typeevent.targetevent.bubbles 等,不止事件对象,js的其他对象也是如此。),这些公开接口并不是“直接读写了内部插槽”,而是这些内部状态的一种通过api暴露出来的方式。

    因为事件对象可以说是事件中最重要的部分,所以,很有必要重点来学习,下面 我们用比较大的篇幅来详细学习事件对象。

    事件对象,从js的角度来讲,它确实是一个真正意义上的对象,我们平常从红皮书 或者权威指南上看到的js对象的定义,略有简化,请记住这个终极理解:

    js对象的本质 = 非原始值 + 属性记录集合 + 原型链继承 + 由内部槽/内部方法决定行为

    从这个角度来说, 事件对象完全符合js对象的本质定义。

    读过js红皮书的朋友也许记得,在不少章节中 都有 [[...]] 这样的内部属性的写法,也就是上面所说的内部插槽。

    我们首先介绍js事件对象的内部插槽:

    核心状态插槽

    定义在 Event 接口中,所有事件对象共用。

    内部槽位类型描述
    [[type]]String事件类型(如 &#34;click&#34;, &#34;load&#34;)。初始化时设定。
    [[target]]EventTarget?初始派发目标。在 dispatchEvent 调用时被设定。
    [[relatedTarget]]EventTarget?与事件相关的次要目标(主要用于 MouseEventFocusEvent)。注意:它也参与重定位。
    [[currentTarget]]EventTarget?当前正在执行监听器的对象。在传播过程中实时更新,派发结束后重置为 null。
    [[eventPhase]]Integer当前阶段:0 (NONE), 1 (CAPTURING), 2 (AT_TARGET), 3 (BUBBLING)。
    [[timeStamp]]DOMHighResTimeStamp事件创建时间(相对于 Time Origin 的高精度时间戳)。
    [[isTrusted]]Booleantrue 表示由 UA(浏览器)生成;false 表示由脚本创建。
    [[path]]List传播路径。由一系列结构体组成,每个结构体包含 item (invocation target) 等信息。
    [[touch target list]]List(仅用于触摸逻辑)用于处理“隐式捕获”,即手指移出元素后仍将事件发送给初始目标。

    [[path]] 是传播路径,关于它的结构和填充,我们后面会详细的学习。

    标志位插槽

    通常在实现中会被压缩为一个 Bit Field 以节省内存。

    内部槽位 (Flag)描述
    [[stop propagation flag]]设置后停止向后续节点传播(stopPropagation)。
    [[stop immediate propagation flag]]设置后停止传播停止当前节点剩余监听器的执行。
    [[canceled flag]]设置后表示默认行为被阻止(preventDefault)。
    [[in passive listener flag]]标识当前是否处于 passive 监听器中(此时忽略 preventDefault)。
    [[composed flag]]标识事件是否可以穿越 Shadow DOM 边界传播。
    [[initialized flag]]标识事件对象是否已完成初始化(防止重复调用 initEvent)。
    [[dispatch flag]]标识事件是否正在派发中(防止重入/多次 dispatch)。
    [[bubbles]]标识事件是否支持冒泡。
    [[cancelable]]标识事件的默认行为是否可取消。
    子类专用槽位

    根据事件类型(C++ 类)的不同,按需存在的槽位。以下列举最核心的几类。

    a. CustomEvent 接口
    内部槽位描述
    [[detail]]存储开发者传入的自定义数据(payload)。
    b. UIEvent 接口 (鼠标、键盘事件的基类)
    内部槽位描述
    [[view]]通常指向 WindowProxy(即 window 对象)。
    [[detail]]对于 UI 事件通常是数字(如点击次数),不同于 CustomEvent 的 detail。
    c. MouseEvent 接口
    内部槽位描述
    [[screenX]], [[screenY]]屏幕绝对坐标。
    [[clientX]], [[clientY]]视口(viewport)相对坐标。
    [[ctrlKey]], [[shiftKey]], [[altKey]], [[metaKey]]修饰键状态(按下为 true)。
    [[button]]触发事件的按键(0:左,1:中,2:右)。
    [[buttons]]当前按下的按键(位掩码,例如 1=Left、2=Right、4=Middle、8=Back、16=Forward)。
    d. KeyboardEvent 接口
    内部槽位描述
    [[key]]键值字符串(如 &#34;Enter&#34;)。
    [[code]]物理按键代码(如 &#34;KeyA&#34;)。
    [[location]]按键位置(如 DOM_KEY_LOCATION_STANDARD)。
    [[repeat]]是否为长按自动重复。
    [[isComposing]]是否在输入法(IME)组合过程中。
    结构化/底层实现槽位
    内部槽位描述
    [[Prototype]]指向 Event.prototype 或子类原型。
    [[Extensible]]对象是否可扩展。
    [[NativePointer]][[EmbedderField]]这是js包装对象中存储的指针,指向底层C++ 的 原始对象

    最后还有内部槽位通过对象属性对外提供的可访问的部分,即公开接口,在后面的部分会详细学习。

    上面是出于对知识的完整性考虑,列出的表格, 在实际学习中, 前端开发者,了解到事件对象的插槽/槽位的深度,就已经是极限了,再继续深入学习,就是对应的c++结构,毫无必要。

    而没有列出的path路径字段和内部槽位对外提供的可访问接口,后面会专门学习。

    我们继续回到事件对象的创建,有两种创建方式:

    • 原生事件的创建

      比如鼠标点击 网络事件 等等,这类事件,是在宏任务被取出,执行第一步命中测试,取得具体目标,第二步创建事件对象时创建的, 一旦确定了目标元素,浏览器引擎(C++ 层,而不是 JS 引擎)就会实例化一个 Event 对象(例如 MouseEventPointerEvent)。这个对象是宿主对象,它被填充了所有相关的上下文信息:target(刚刚找到的元素)、currentTarget(最初为 null)、坐标、时间戳、bubbles 属性等。(这些信息,原本是存在于值钱的c++结构中。) 这个时候,浏览器会在 JS 环境上创建一个 JS wrapper。这个 wrapper 和底层的 C++ 对象互相关联(wrapper 内含对宿主对象的指针/引用,就是上面表格中的**[[NativePointer]]** 或 [[EmbedderField]],而宿主对象则通常保存一个对该js包装对象的弱引用或记录,以便于重复利用该js对象)。

      至此,js已经有了事件对象,虽然是‘包装对象’,但是依旧是真正意义上的js对象。

    • js创建的事件对象

      是在js代码中自己创建的,通常使用 new ,在最新的红宝书第5版里,依旧在使用createevent的方式,已经不建议使用了。在自己new事件对象时,需要知道自己使用哪种具体事件的构造函数,因为每种具体的构造函数所拥有的内部槽位不同,无法混用或通用。

      另外,js创建的事件对象,是同步创建的,执行到new代码,对象事件就立即生成, 这和原生的事件对象的创建不同。 自己new的事件对象 是纯正的js对象, 原生事件对象 是包装对象, 但是 他们都是真正的js对象。

    事件对象的创建详细过程将在后面的事件的生命周期部分介绍。

二、 事件的完整生命周期

在第一部分中, 介绍了事件中的一些重要的知识点。

重要的是eventtarget和event。

请注意,不要把这两个概念搞混淆了。

EventTarget 是一切的源头,它让某个东西,具备了事件处理能力。任何能处理事件的东西,都必须是已经实现了(继承也好 自己写也好 使用第三方库也好 )这个接口。

Event 是 一次事件的全部内容与状态的载体 它包含一次事件中的所有状态 (所有状态 所有关联到的对象 所有动态行为等等)

在这第二部分里,我们介绍事件的完整生命周期。

以一个物理点击事件为例,他的整个生命流程如下:

  1. 物理信号: 用户在硬件(例如鼠标或触摸屏)上按下。设备向操作系统 (OS) 发送一个硬件中断信号,并附带位置数据。

  2. OS 路由: 操作系统(例如 Windows、macOS、Android)接收该信号,确定哪个应用程序处于活动状态(即浏览器),并将此低级输入(例如“鼠标按下,坐标 X:Y”)传递给浏览器的浏览器进程 (Browser Process)

  3. IPC 到渲染器: 浏览器进程负责浏览器的“外壳”(地址栏、选项卡),但它不知道选项卡内的内容。它通过进程间通信 (IPC) 将事件(例如 mousedown)和坐标发送到负责该选项卡的渲染器进程 (Renderer Process)

  4. 合成器线程接收: 在渲染器进程中,事件首先由合成器线程 (Compositor Thread) 接收。该线程独立于主线程(js运行的地方)运行,负责平滑地合成页面的各个层(例如,用于平滑滚动)。

  5. 合成器命中测试: 合成器线程执行一次“快速”命中测试。它检查事件坐标是否落在它标记为“非快速滚动区域”(Non-Fast Scrollable Region) 的地方。该区域是页面上附加了事件处理程序(如 touchstartclick 监听器)的区域 。

  6. 事件路由决策:

    • 如果事件不在非快速滚动区域(例如,在可滚动的空白区域),合成器线程可以立即处理它(例如,开始滚动页面),而无需等待主线程 。

    • 如果事件非快速滚动区域,合成器线程必须将该事件转发到主线程 (Main Thread),因为只有主线程才能运行 JavaScript 。

    • 在合成器线程的决策逻辑中,存在一个关键的性能瓶颈:当合成器线程发现触点位于“非快速滚动区域”(即绑定了 touchstart/wheel 等事件)时,默认情况下,它必须挂起页面的滚动渲染,先向主线程发送事件信号,并同步等待 JS 回调函数的执行结果。

      为什么要等?因为浏览器无法预知你的代码中是否会调用 e.preventDefault() 来阻止默认的滚动行为。这种“跨线程的同步等待”一旦遇上主线程繁忙,就是造成移动端页面滑动卡顿(Scroll Jank)的根本原因。

      { passive: true } 的本质,是开发者向浏览器签署的一份**“异步执行承诺书”**。

      通过这个标记,你告诉合成器线程:“请直接开始滚动渲染,不要等我。我承诺在回调函数中绝不调用 preventDefault()。”

      一旦建立了这个协定,合成器线程就会立即处理滚动帧(保证丝滑流畅),同时将事件以非阻塞的方式发送给主线程去执行逻辑。此时,即便你违约在回调中强行调用了 preventDefault(),浏览器也会直接忽略该指令并在控制台抛出警告。

  7. 排队成为宏任务: 当事件(现在是 C++ 层面上的一个结构)到达主线程时,它不会立即执行。它被封装并放入红任务队列(也称为“任务队列”或“回调队列”)中,等待执行。此时,它已成为 JavaScript 事件循环模型的一部分。

  8. 事件循环出队与任务启动: JavaScript 事件循环机制持续监控着状态。当主线程的调用栈为空,且微任务队列也被清空(确保前一个循环彻底结束)时,事件循环才会从宏任务队列中取出那个排队已久的 mousedown 任务。 注意: 取出这个任务,标志着浏览器开始执行该任务内部包含的一系列逻辑

  9. 主线程命中测试(深度): 任务执行的第一步是在主线程上进行“深度”命中测试。与合成器线程(只知道图层)不同,主线程拥有完整的 DOM 树、CSS 样式和布局信息。它使用这些数据(特别是“绘制记录”)来精确确定事件坐标下最顶层的 DOM 元素。这个元素将成为 event.target

  10. 创建事件对象: 一旦确定了目标元素,浏览器引擎(C++ 层,而不是 JS 引擎)就会实例化一个 Event 对象(例如 MouseEventPointerEvent)。这个对象(一个“宿主对象”)被填充了所有相关的上下文信息:target(刚刚找到的元素)、currentTarget(最初为 null)、坐标、时间戳、bubbles 属性等。

    注意 :现在 浏览器引擎会让js引擎创建js层面的事件对象,就是把c++层的宿主对象包装成js层的事件对象。但是,浏览器出于优化的考虑,也许会采用懒加载的方式 在第11步完成后,按需让js引擎创建js事件对象。 不过从整个流程的合理性来说,可以认为此时 js事件对象也被创建。

  11. 确定传播路径: 浏览器根据 DOM 树结构计算事件的完整传播路径。这是一个包含从 window 开始,一直向下到 event.target 的所有祖先元素,然后再回到 window 的有序数组。

  12. 开始调度(捕获阶段): 任务现在开始沿着计算出的路径“调度”事件对象。它从 window 开始,向下传播到目标,在每个节点上触发已注册为在捕获阶段运行({capture: true})的 JavaScript 监听器 。

  13. 目标阶段: 这是一个特殊的阶段。规范在实现上并没有一个独立的“目标阶段循环”,而是将其拆解到了另外两个阶段中。

    1. 捕获遍历到达目标时,浏览器会将目标标记为 AT_TARGET,并执行目标上所有 capture: true 的监听器。
    2. 冒泡遍历开始时,浏览器再次访问目标,将其标记为 AT_TARGET,并执行目标上所有 capture: false(非捕获)的监听器。

    所以,实质上是捕获类监听器先执行,非捕获类监听器后执行同类监听器内部,才按添加顺序执行。

  14. 冒泡阶段: 事件随后从 event.target 向上传播回 window,在路径上的每个祖先元素上触发标准的(冒泡阶段)JavaScript 监听器 。

  15. 任务完成: 一旦事件到达 window 并且所有处理程序都已运行(前提是没有调用 stopPropagation()),这个宏任务就完成了。

  16. 微任务检查点: 在事件循环查找下一个宏任务之前,它会立即执行并清空微任务队列中的所有任务(例如,在事件处理程序中调度的 Promise.then() 回调)。

  17. 渲染: 在微任务队列清空后,浏览器现在有机会执行渲染更新(重绘页面)。

  18. 循环: 事件循环现在返回宏任务队列,以查找下一个任务。

下面,我们将以这整个流程为线索,介绍几个重要的步骤

从结构的角度来讲 物理点击 浏览器c++层创建初始结构 包装成宏任务入队列 被取出执行 命中测试 确定目标元素 浏览器c++层将初始结构和目标组合一起 创建了一个新的c++结构 填充槽位 浏览器调用js引擎 让js创建js层的事件对象 (包装了c++层的结构)填充关键槽位(可能有懒加载) 建立和c++结构的关联。

在具体的浏览器实现中, 有时会将第11步计算传播路径提前, 先计算传播路径 再开始创建js的事件对象。主要目的是可以通过先计算传播路径,确定是否有针对具体目标的监听, 假如没有, 那就根本没必要创建js的事件对象了。

介绍一下内部插槽的填充:

在命中测试完成 确定了具体的目标元素, 这个时候浏览器会创建一个c++事件实例结构,包括了最初的那个结构 又包括了目标元素,还有和事件类型相对应的内部槽位, 这是因为浏览器会根据事件的不同 调用不同的构造函数 创建不同的c++事件实例。每种事件实例都有专属于自己的槽位, 同时还有通用槽位。

浏览器创建事件对应的c++事件实例,填充内部槽位,我们先介绍静态数据, 这些数据一旦填充,在整个生命周期就不会改变。以下以一个点击事件为例

  • [[type]] 根据事件类型(如 "click")硬编码 静态只读

  • [[isTrusted]] 值被设为true(因为是浏览器原生触发,如果是脚本模拟自定义 则为假)静态只读

  • [[timeStamp]] 读取当前的高精度时间 静态 (只读)

  • [[target]] 指向命中测试找到的最深层的那个 DOM 节点。 半静态 (Shadow DOM 中表现不同)

  • [[NativePointer]] 指向底层的 C++ 结构体地址。 静态 (内部引用)

  • [[screenX/Y]] 读取操作系统传入的硬件光标坐标数据。 静态

  • [[bubbles]] 根据事件类型查表确定(例如 "click" 默认为 true,而 "focus" 或 "scroll" 默认为 false)。 状态: 静态 (只读)

  • [[cancelable]] 根据事件类型查表确定(指示该事件是否允许通过脚本取消默认行为)。 状态: 静态 (只读)

  • [[defaultPrevented]] 初始化为 false。 仅当脚本调用 event.preventDefault()[[cancelable]] 为真时,该值才会被修改为 true状态: 动态 (可变)

  • [[propagationStopped]] 初始化为 false。 这是一个内部控制标志,当脚本调用 event.stopPropagation() 时被设为 true,用于通知事件分发器停止遍历后续路径。 状态: 动态 (内部标记/不可见)

  • [[underlying_platform_event]] 保存对原始底层硬件输入结构的 C++ 指针引用 就是最开始的那个初始c++结构。这实现了“零拷贝”机制,仅在 JS 访问特定属性(如 pressure, tiltX)时才通过此指针去读取底层数据。 状态: 静态 (内部引用)

注意:此时,[[currentTarget]] 还是 null[[eventPhase]]NONE (0)

这里插一段,写这篇文章,一是自己需要对知识的总结归纳 二是希望写出来 是种分享,大数据时代 我们除了获取,不能忘记提供,关于木有图片。。。是因为我没有图床。。。其实就是懒。关于木有代码实例。。。还是因为懒。我喜欢用文字来描述来表达,虽然可能很多地方表达能力跟不上自己的想法。。。我是尽力了。其实我是有整篇的写作意图和明确的串联线索,只是写的多了,有时候就忘记或者是跑偏了,反正就是能力跟不上,反正就一个特点:字多。 大家当成小说看吧,其实我以前是写网文的。

我们学习到现在,已经能青春地意识到:

  1. “真身”在下层: C++ 层面的事件实例 才是真正意义上完整、权威的事件状态载体。它用有物理原始数据、DOM 传播的实时状态指针以及所有标准定义的内部槽位。
  2. “外壳”在上层: 我们在代码中操作的 JS 事件对象,本质上只是js 引擎创建的一个 代理壳 (Proxy/Wrapper)
  3. 核心连接: 这个壳内部并不直接存储大量数据,它最核心的东西是一个指向 C++ 结构的 内部指针 ([[NativePointer]])
  4. 数据获取的方式: 当我们在 JS 中访问属性时,并不是简单的读取内存,而是根据属性的特性,触发了不同的底层机制:
    • 实时透传 : 对于动态变化的数据(如 currentTarget, eventPhase),JS 对象通过 Getter 访问器 直接穿透到 C++ 结构中读取最新值。
    • 懒加载: 对于昂贵的计算属性(如 composedPath() 或标准化的 path),只有当 JS 第一次请求时,C++ 才会计算并将其转换为 JS 数组,然后挂载到 JS 对象上。
    • 缓存和优化: 对于静态不可变数据(如 type, timeStamp, isTrusted),js引擎可能会在第一次读取后将结果缓存在 JS 壳的“快照”中,以避免频繁跨越 C++/JS 边界带来的性能损耗。
  5. 可扩展性: 这个 JS 壳虽然是代理,但它也是一个标准的 JS 对象。因此,我们手动添加的自定义属性(如 e.myTag = &#34;test&#34;)是保存在 JS 壳 自己的堆内存中的,C++ 层对这些数据一无所知。

下面开始计算传播路径

  • 浏览器使用内部算法, 从 [[target]] 开始,沿着父节点一直向上找,直到 window。这个过程会填充非常重要的 [[path]] 插槽。

  • 现在我们开始详细的介绍path内部插槽的构成和用途

    假如不包括 Shadow DOM,那么路径的计算和确定,将是非常简单的 沿着target一直向上找到window就可以了。但是正因为Shadow DOM的存在,让传播路径的计算成了一个微有难度的工作。

    简单的描述一下概念,不算严谨,但可以当作了解。

    一个dom树中, 一个元素被挂载了一个影子dom,那么 该元素被叫为 host, 然后,逻辑上看 以host为根, 有了两颗树, 一棵是刚挂载的影dom树 另一棵是host原本的子节点元素树。 而挂载的影dom树,并不是直接挂载 而是用一个root 挂在host上,root下面 才是影dom。 host原本的子元素树 叫光dom 。

    看这部分内容的朋友,应该是对shadow dom已经有了解的, 以上简单介绍,只是为了后面方便使用影dom 光dom host root 等名词。

    在第一部分,曾为了知识的完整性,列出了内部插槽的其他部分的列表。 这里这部分作为可跳过的选看部分,将详细的介绍path内容,这部分内容我个人认为在可跳过的内容中,算是重要的,所以打算用略大的篇幅来讲,不感兴趣的朋友依旧可以跳过这部分 。

    path中 是一个结构列表,每一项都是一个结构,对应着事件传播路径中的一个元素 严谨的说 对应着一个具备事件能力 即实现了targetevent接口 的对象。通过此列表,就可以观察到 本次事件的完整传播路径。 而且 事件的传播路径 是一次性创建, 创建好以后, 不会再更改,存在于事件的整个生命周期。传播路径是固定的,但是监听的调用等等还是会动态变化,这里只是讲路径的确定, 监听和传播过程 后面部分会详细讲。

    在最新的权威文档中,path中的每一项,都有7个字段,下面逐一介绍

    1. invocation target(调用目标)

      • 类型:一个 EventTarget 对象(通常是 Node / Element / Document / Window,也可以是其它实现了 EventTarget 的对象)。
      • 描述: 这是该路径项对应的实际 DOM 目标。通常来说,就是当前的节点。
    2. invocation-target-in-shadow-tree (调用目标是否在 Shadow Tree 中)

      • 类型: Boolean
      • 描述: 一个布尔值,标记 invocation target 是否位于 Shadow DOM 树内部。
      • 作用: 用于处理 Shadow DOM 边界的事件封装(Encapsulation)。此标志影响分派算法在决定重定位(retargeting)和阶段(capturing/at-target/bubbling)时的行为,以及是否需要将目标“影子化/重定向”给 shadow host 等逻辑。规范在处理路径和设置 eventPhasecurrentTarget 时会检查此值。
    3. shadow-adjusted target (Shadow 修正目标)

      • 类型 要么是 null,要么是一个 潜在的事件目标(potential event target)

      • 描述(最关键)

        • 这是“对监听器可见的那个目标(retargeted target)” 具体说,当事件从一个 shadow tree 向外传播(或在 shadow 边界处被观察)时,浏览器会把实际原始目标根据监听器位置做重定位,重定位后的对象就称为 shadow-adjusted target

        • 在事件分派过程中:如果path中的某个项的 shadow-adjusted target 非空,规范把该 struct 视为“AT_TARGET”类型的位置(用于设置 eventPhase = AT_TARGET),并用它来决定在该位置要以什么 target 值去调用监听器。

      • 举例:若真实事件发生在 shadow 内部的某个 div,当在 shadow host(外部)上触发监听器时,shadow-adjusted target 可能是 host(或 host 的某个可见代理),而不是内部真实 div,从而实现了 Shadow DOM 的封装(retargeting)

      • 再举例:当事件从影DOM 冒泡到光DOM 时,为了保持封装性,外部不应看到内部的真实节点。这个字段决定了在当前项所处的位置上,开发者调用 event.target 时应该返回哪个节点(通常是 host,即影子的宿主,而不是影dom内部真实的节点)。

      • 再再举例 算了,不举了

    4. relatedTarget (相关目标)

      • 类型 null 或 一个 潜在事件目标

      • 描述 用于那些有“related target”语义的事件(例如 mouseover / mouseout、焦点事件中的 relatedTarget 等)来记录在该路径层次上与当前 invocation target 相关联的另一个目标(经过 retargeting 后可能是不同的对象)。

        简单来说 就是类似于 shadow-adjusted target,但是专门用于修正 event.relatedTarget

        比如在 mouseover/mouseout 事件中,如果相关元素在 Shadow DOM 内部,这个字段确保外部只能看到 Shadow Host,而不是内部细节。

      • 注意relatedTarget 的值也会受到 shadow tree 封装/重定位规则影响

    5. touch target list (触摸目标列表)

      • 类型 / 含义:一个“潜在事件目标”列表(sequence/list)List of Touch objects。主要用于触摸/多点触控相关的事件以记录与路径中该 struct 相关的所有触摸目标(比如 touchstart 的多个触点)。
      • 语义 / 用途:在分派触摸/Pointer 类型的事件时,规范需要知道该路径层上哪些具体触摸点是相关的,以便在给监听器报告事件时能确定哪些触点属于当前 currentTarget 的上下文。
      • 专门用于触摸事件(Touch Events)。当触摸点在 Shadow DOM 内部移动时,需要修正触摸点的 target 属性,以符合 Shadow DOM 的重定标(Retargeting)规则。
    6. root-of-closed-tree (是否为封闭树的根)

      • 类型 / 含义:布尔值。表示该 struct 表示的 invocation target(或其相关信息)是否处在一个 closed shadow tree 的根(即该 struct 表示的那一层涉及到一个 closed shadow root)。
      • 描述: 标记该路径项是否是一个模式为 closed 的 Shadow Root。
      • 如果为 true,则在使用 composedPath() 获取路径时,路径会在这个节点被截断,外部无法通过 API 获取到封闭 Shadow DOM 内部的节点。
      • 这个标志用于实现 closed shadow tree 的封装保护:当 root-of-closed-tree 为真时,规范在构建对外暴露的 invocation target 列表或在清理路径时会采取特殊处理(例如阻止 closed tree 内部节点出现在 composedPath() 的对外结果中,或在路径清理时决定是否插入清理 struct 等)。通俗说:它帮助浏览器决定“哪些内部节点必须对外屏蔽”。
    7. slot-in-closed-tree (是否为封闭树中的 Slot)

      • 类型 / 含义:布尔值。表示在路径构建时当前节点是不是“一个处在 closed shadow tree 中的 slot(slot-in-closed-tree)”的上下文标记。

      • 语义 / 作用:与插槽(``)与被插入的 light DOM 元素相关的路径构建有关。规范在把路径 append 到 event.path 时把这个标志一并记录,以便 later 在决定 clearTargets、retargeting、以及是否把某些 struct 暴露到对外路径(或触发 activation behavior)时使用。简单说,它用于正确处理插槽 + closed shadow tree 的组合场景

      • 同样用于控制 composedPath() 的暴露范围,确保封闭树的内部结构不泄露。

    以上7个字段,是规范规定的path中的字段,属于内部使用的数据,在我们的js层,并不能直接使用。

    新的一天,有点忘记进度了,上面讲了path的7个规范定义的字段。目的是为了下面讲传播路径的计算。前面好像也讲过, 如果是没有影dom,那么从命中具体目标以后,直接网上挨个找爸爸,挨个填path中的项。很简单。 但是因为有了影dom,路径的计算有点繁琐。

    那么被插槽进影dom的光dom元素,发生的事件它的路径如何呢?

    如果composed为真,此被slotted的元素上发生事件,路径为 此光dom--影slot--影root--光host--Document

    如果composed为假,路径依旧是 此光dom--影slot--影root--光host--Document

    这是因为规范规定:

    composed:false 只是 一个必要条件,但不是充分条件;还要满足 “该 shadow root 是事件目标所在根” 这个前提,才会被拦截返回 null(从而阻止继续向上到 host)。

    如果事件目标的根不是该 shadow root(例如目标属于 light tree,其根是 document),那么该 shadow root 不会返回 null,而是返回 host —— 事件继续传播。

    也就是说,被插槽进影dom的光dom元素, 依旧归属于光dom树,在它身上发生的可冒泡事件,在影dom它的slot位置开始,经历 此元素---slot---root 达到影dom边界,此时 规范定义了判断算法,必须要满足两个条件,才会被拦截, 一 是 composed为假 二是 该发生事件的元素的根是影root, 这样才会被拦截。

    被插槽进影dom的光dom元素,归属于光dom树, 它的根为document, 而不是影root,所以不满足拦截条件。

    很多资料或者文章把composed为假的情况 绝对化,从规范上说 是不对的。

    对于影dom内部的元素发生的事件,composed为假会拦截,因为他们同时符合根为影root的条件。

    但是对于归属于外部光dom的被插槽元素来说,它的根为document,不符合条件, 所以不会拦截。

    上面第4个 relatedTarget 比较有意思,可以稍微了解一下

    • 含义:对于 mouseover/mouseoutfocusin/focusout 等“有关联目标”的事件,记录相关目标。

      这个字段,并不是所有事件都具有,一般是有节点间转移的动作的事件才有。

      假如有元素a和b,鼠标此时在a上,现在,把鼠标从a移到b,那么,对于a来说,在它身上发生了mouseout事件,鼠标离开, 创建这个事件对象的时候,path路径中,target当然是a,而relatedtarget表示关联目标,就是和target对应的一个目标,因为鼠标是移动到了b身上,所以relatedtarget就是b。

      如果我们从b的角度来看,在b的身上发生了mouseover事件,鼠标到了b身上,那么这个事件对象创建,它的path路径的target是b, 和它关联的目标 relatedtarget则是a,因为鼠标从a过来的。

      请注意,relatedtarget 也遵守 影子DOM 的重定位规则。 从影dom外面看,如果 relatedtarget 指向的是 影DOM 内部的元素,它也会被替换为 Host

    • 用处:

      1. 在调用监听器时提供上下文(比如判断鼠标是从哪里移进来的)。

      2. 作为事件触发的裁决依据:浏览器会对比重定位后的 targetrelatedtarget。如果在某一层级,两个变成了同一个对象(例如都变成了 host),浏览器会认为没有发生实质性的交互,从而阻止该事件在这一层的触发。

    上面第3个 shadow-adjusted target (Shadow 修正目标)

    shadow-adjusted target

    • 含义:对于当前项 来说的目标,请注意,这个target是path中的一个字段,要和在event事件对象的内部插槽中,还有一个静态的原始目标对象相区别 不要搞混。通俗的解释:在影dom中 path中的target始终是发起事件的那个真正目标。 而跨出影dom后 该target变为host, 以host代替影dom内真正的事件发起目标,以实现不让外人偷窥到影dom内部情况的效果。也就是说,如果不存在影dom,则该target 始终都是事件的实际发起元素。 而在影dom中的项目上,也是事件的实际发起元素,但是越过root,在host这一项上,该target变为host,并且一直保持到window项。

      请注意

      对于被插槽进影dom的光dom元素,因为它依旧归属光dom,所以在影内 影外 host上,它的该target值都为真正的光dom本身。

    • 用处:在该项上的监听器中读取 event.target时要显示的对象。

      当该项表示的事件对象为被插槽进影dom中的时候,无论在何处 其值为真正本体。

      当事件源是影dom内部元素,该项位于影dom外时,显示host,位于影dom内时 显示真正事件发起目标, 位于host时,显示host。

    经过前面的大段铺垫,现在开始学习传播路径构建 这次是真的真的了。

    之所以执着于大段的讲path的字段和事件对象的内部插槽,主要是我个人认为,js层面的公开的api,只是对内部数据的整合和包装,只学习他们,无法真正准确的了解事件传播路径的算法和之后的事件传播及处理,以及这些内部数据和标志位之间的配合所带来的对于外部来说 比较不好理解的现象。 当然 限于能力,写的比较散乱,从开始到现在 一大半都算超纲注水,所以要赶快回归。 观看时跳过上面的这一大部分就好了。

    事件传播路径的构建,是一个算法,它是以当前DOM结构为基础的,不需要JS参与的行为。完全依靠dom结构,加上影dom的边际规则,逐步构建出来一条物理路径。所以 直到最后路径构建完成,我们都看不到监听等js的一根毛。 就类似于 传说中的低耦合,甚至是解耦,没耦。 路径的构建,基本上是浏览器引擎的活, 至于以后监听什么的,和路径没关系, 不管你听不听 路就在那里。不管你走不走,路也就在那里。 以后,js的监听和动态的设置活动,属于逻辑上的, 而现在构建出来的路径,属于物理上的。只需要注意一点 当前节点的状态,有可能是由之前节点创建时的js层面参与决定的, 但是也仅此而已,路径的创建,js一直是靠边站的。反正,就是这么个意思吧。

    其实吧 写到这里 有点沮丧。。。 也就是一个遍历算法,做了那么多的铺垫,早点直接说不就得了吗。然后我又想到前几天刷到的小片段: 入职以后前三个月每月工资2000,第四个月开始4000, 大智慧的朋友说 那你等到第四个月再去入职。

    继续码字

    这里就不得不引出两个比较重要的概念:

    • 合成/组成树 扁平树( Composed Tree flat tree )

      虽然名字是两个 但都是指的同一个东西,组成树的意思主要突出它的来源并不单一,比如光dom 影dom slot 等,然后根据规则组成的一棵树。

      扁平树的意思是从组成以后是一个整体的角度来讲的, flat表示消除了原本的影dom和光dom的隔阂,把正确的slot的内容投影拍扁进影dom槽中,表示是一棵单一的连续的树。

      注意 归属并没有改变。

      扁平树并不是一棵完整的或者部分的真实存在的树,它是一种规范中抽象定义,在实现中,逻辑存在,在需要时,动态计算出来的一种逻辑树。

      从名字 从存在形式 都已经说了,那么 它的内容是什么呢

      扁平树是以整棵 DOM 树为基础,但在遇到宿主host时,会使用其 影dom结构来替代原本的内容,同时将光 DOM 中被选中的节点“投影”进 影DOM 的插槽中,最终形成的一棵树。

      那么这里要特别注意,在物理上,并没有什么变化,host下依旧是一棵光dom 一棵影dom, 扁平树是一种抽象的逻辑树,按照规则 把它需要的东西 提取出来。物理上 原来怎么样 现在还是怎么样。

      ---------为了说清这扁平树 我可费了老大劲,改了好多遍。

    • 渲染树

      渲染树是基于扁平树,使用css规则,生成的用于布局和绘制的树。

      好像暂时用不到渲染树,先不详细说了,后面讲到渲染再说。

    dom树 物理存储结构,只有原始的层级。

    扁平树 抽象出来的 , 打通了光dom和影dom的隔阂 有逻辑层次结构 。

    渲染树 扁平树加css规则 有视觉呈现结构。

    事件传播路径的构建算法,大约有百分之七八十的内容,都以零散的方式在前面介绍过了,还剩一个系统性的算法描述作为总结,但是有点犹豫,因为虽然算法很简单,但是牵扯到的字段和标志位比较繁琐,需要比较大的篇幅来讲述,而这部分内容 在前端开发中,百分之八九十的可能性是用不到的。但是在写组件写库写shadowdom以及排除一些bug的时候,却是神兵利器。所以打算放在第三部分事件的传播和处理部分再详细介绍。

    现在我们来思考一下,就是真的只用脑子思考,在写这整篇文章的时候 有些知识点 ,甚至在有些地方多次反复的强调它所处的阶段 所在的位置等等,就是不断的试图在读者的脑中 构建出一个完整的事件模型,说到流程,你可以想到主要步骤, 说到光dom影dom 你可以想到一棵树。

    一棵dom树,某个节点被挂上了新的树,于是 这个节点成了host,下面有了两棵树 一棵光dom 一棵影dom。 这里有个问题,就是以哪棵树为着眼点,不少朋友认为,光dom是正宗嫡系,当然要以光dom为着眼点,关注上面有没有被slot等等, 其实是不恰当的。 要以影dom为主,以影dom的角度来看, 影dom被挂上来,就是接管 代替了光dom,我就是老大 看我的。 你光dom想干点啥,必须投影到我这里,你现在就是我的备件仓库/展销厅, 我给你机会,你才能出现。

    这是一种思考模型,从物理dom树 到扁平树的转变,虽然光dom始终是host的子树,是物理存在的,但是在思考时 使用扁平树的角度, 因为扁平树和渲染树对光dom是默认忽视的。

    那么 我们再稍微延申一下, 假如光dom树 没有被slot, 然后 我在js层面手动派发事件,会出现什么情况? 我在以前刚开始学习时,曾误以为,传播路径有两条 一条物理的 一条扁平的,后来才纠正过来,始终就是一条路,依靠算法来决定怎么走。

    依旧是路径构建算法,算法规定,A node’s get the parent algorithm, given an event, returns the node’s assigned slot, if node is assigned; otherwise node’s parent. 就是没有被slot的节点找到的父亲是物理链路上的父亲。

    也就是说 事件会传播到host 然后继续传播到document。 js可以对它像对其他元素一样 进行操作。包括监听 派发 允许slot等等等。 唯一的问题 就是因为它没有进扁平树 也就进不了渲染树,在视觉上是不在的。 看不到 所以除了设置slot加入扁平树的操作以外, 其他的操作 要慎重,避免各种bug的产生。

    影子dom也挺有趣的,给事件传播路径上增添了很多色彩,影子哥和事件传播路径的构建有关的内容,好像也差不多了。如果后续其他部分里还牵扯到Shadow DOM的内容,到时候想起来再讲吧。

    这第二部分马上写完了,最后总结升华一下

    记得在第一部分,提到了观察者模式,我们再再再的把它具体到事件上来。

    事件的发生,不管是因为什么原因,它的本质,就是 期待关注 。

    我有事,我有事啊,我说我有事了,这是事件的发生,我不需要知道谁会处理它,也不知道什么时候会被处理。

    三个阶段,捕获 目标 冒泡 提供了时间上的选择权 和处理的策略

    捕获是拦截和预处理

    目标是现场自己的处理

    冒泡是兜底和总结。

    传播路径,提供了空间和层次上的哨位 路径上不同的哨位,有着自己的不同的职责和环境,他们看待经过自己的事件,是用自己所在的哨位职责来观察的,同一个点击事件,在buttnn上看,是用户点按钮了, 在form上看, 是用户可能在提交表单, 在document上看,是用户还活着 没噶呢。 就是说 你可以选择 在具体哪个抽象层级上来处理这个业务洛基。

    而三个阶段和传播路径的结合,就是对于事件处理的从时间到空间到层次上的结合,择优选择,比如事件委托该放在哪里? 时间上,当然是选择到最后的冒泡了,空间和层次上 当然是选择越高越好了 大内总管那岗位厉害。而每个哨位 也有自己的小权力 比如这事归我管,不往上送了,比如这事只归我管 不给周围同事了。

    最后 我们进入玄幻模式: 事件流为我们提供了时间空间层次三维立体的选择权。

后续 :

写到这里 强行打住,讲完了事件的传播路径的构建,后面就是事件的传播和处理。篇幅已经有点大了,本来想全部完本以后一次性发出来,但是已经一万五千字了,先发出来。

后面是 第三部分 事件的传播和处理

还有第四部分 事件的循环和异步

还有第五部分 具体的各种事件 ,但是现在有点犹豫,第5部分打算给砍掉算了,具体的事件也没什么好说的。加上第5部分 肯定超5万字,打字太多了 键盘吃不消。

估计有不少朋友点进来一看,嚯 这写的什么东西 乱七八糟的。。。哎呀 看官大老爷们 网文也不是那么好写的啦。 到后来 我连 ctrl+b 加都有点懒得弄了。说是技术文章吧 我也不太好意思 排版烂 没图没码 全都是字,说是科普性文章吧 我又有些不甘心,因为在准确性上 我还是有追求的,风格呢 形散神不散吧。感谢观看。我们下篇再见。

本文首发于: 掘金社区

同步发表于: csdn

博客园

码字虽不易 知识脉络的梳理更是不易 ,但是知识的传播更重要,

欢迎转载,请保持全文完整。

谢绝片段摘录。

参考列表:

  • developer.mozilla.org

  • dom.spec.whatwg.org

  • html.spec.whatwg.org

  • tc39.es

  • developer.chrome.com

  • w3.org