余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

DOM 高级工程师不完全指南(建筑高级工程师职称评定条件)

xiyangw 2022-11-25 11:40 18 浏览 0 评论

“前端框架真的太香了,香到我都不敢徒手撕 DOM 了!”

虽然绝大多数前端er都有这样的困扰,但本着基础为大的原则,手撕 DOM 应当是一个前端攻城狮的必备技能,这正是本文诞生的初衷 —— DOM 并没有那么难搞,如果能去充分利用它,那么你离爱上它就不远了。

DOM 高级工程师不完全指南(建筑高级工程师职称评定条件)

三年前我初入前端坑的时候,发现了一个叫做 jQuery 的宝贝,她有一个神奇的 $ 函数,可以让我快速选中某一个或一组 DOM 元素,并提供链式调用以减少代码的冗余。虽然现在提到 jQuery 这个名词,你会觉得老土,“都 9102 年了你跟我说 Nokia?”。土归土,但也是真的香。尽管这几年风生水起的 Vue、React 加剧了 jQuery 的没落,但全世界仍有超过 6600 万个网站在使用 jQuery,占全球所有网站数量的 74%。

jQuery 也给业界留下了产生深远影响的“遗产”,W3C 就仿照其 $ 函数实现了 querySelector 和 querySelectorAll。而讽刺的是,也正是这两个原生方法的出现,大大加快了 jQuery 的没落,因为它们取代了前者最常用的功能 —— 快捷的选择 DOM 元素。

虽然这两个新方法写起来有点长(问题不大,封装一哈),但是它们是真的贼好用。

来,冲!

获取单个 DOM 元素

向 document.querySelector 中传入任何有效的 css 选择器,即可选中单个 DOM 元素:

document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')

如果页面上没有指定的元素时,返回 null

获取元素集合

使用 document.querySelectorAll 可以获取一个元素集合,它的传参和 document.querySelector 一毛一样。它会返回一个静态的 NodeList ,如果没有元素被查找到,则会返回一个空的 NodeList 。

NodeList 是一个可遍历的对象(aka:伪数组),虽然和数组很像,但它确实不是数组,虽然可以利用 forEach 遍历它,但它并不具备数组的一些方法,比如 map、reduce、find。

那么问题来了,如何将一个伪数组转化为数组呢?ES6 为开发者提供了两个便利的选择:

const arr = [...document.querySelectorAll('div')]
// or
const alsoArr = Array.from(document.querySelectorAll('div'))

远古时代,开发者们常用 getElementsByTagName 和 getElementsByClassName 去获取元素集合,但不同于 querySelectorAll,它们获取的是一个动态的 HTMLCollection,这就意味着,它的结果会一直随着 DOM 的改变而改变。

元素的局部搜索

当需要查找元素时,不一定每次都基于 document 去查找。开发者可以在任何 HTMLElement 上进行 DOM 元素的局部搜索:

const container = document.querySelector('#container')
container.querySelector('#target')

方法名太长了!

事实证明,每个优秀的开发者都是很懒的。为了减少对宝贝键盘的损耗,我一般会这么干:

const $ = document.querySelector.bind(document)

保护机械键盘,从我做起。

少年,爬上这棵 DOM 树

上述内容的主题是查找 DOM 元素,这是一个自上而下的过程:从父元素向其包含的子元素发起查询。

但没有一个 API 可以帮助开发者借由子元素向父元素发起查询。

迷惑之际,MDN 给我提供了一个宝藏方法:closest 。

Starting with the Element itself, the closest() method traverses parents (heading toward the document root) of the Element until it finds a node that matches the provided selectorString. Will return itself or the matching ancestor. If no such element exists, it returns null.

也就是说,closest 方法可以从特定的 HTMLElement 向上发起查询,找到第一个符合指定 css 表达式的父元素(也可以是元素自身),如果找到了文档根节点还没有找到目标时,就会返回 null 。

添加 DOM 元素

如果用原生 JavaScript 向 DOM 中添加一个或多个元素,一般开发者的内心都是抗拒的,为啥呢?假设向页面添加一个 a 标签:

<a href="/home" class="active">首页</a>

正常情况下,需要写出如下的代码:

const link = document.createElement('a')
link.setAttribute('href', '/home')
link.className = 'active'
link.textContent = '首页'

// finally
document.body.appendChild(link)

真的麻烦。

而老大哥 jQuery 可以简化为:

$('body').append('<a href="/home" class="active">首页</a>')

但,各位观众,如今原生 JavaScript 也可以实现这一操作了:

document.body.insertAdjacentHTML(
	'beforeend',
 '<a href="/home" class="active">首页</a>'
)

这个方法允许你将任何有效的 HTML 字符串插入到一个 DOM 元素的四个位置,这四个位置由方法的第一个参数指定,分别是:

  • 'beforebegin': 元素之前
  • 'afterbegin': 元素内,位于现存的第一个子元素之前
  • 'beforeend': 元素内,位于现存的最后一个子元素之后
  • 'afterend': 元素之后
  • <!-- beforebegin -->
    <div>
    	<!-- afterbegin -->
     <span></span>
     	<!-- beforeend -->
    </div>
    <!-- afterend -->

    舒服了呀。

    更舒服的是,它还有两个好兄弟,让开发者可以快速地插入 HTML 元素和字符串:

    // 插入 HTML 元素
    document.body.insertAdjacentElement(
    	'beforeend',
    	document.createElement('a')
    )
    
    // 插入文本
    document.body.insertAdjacentText('afterbegin', 'cool!')

    移动 DOM 元素

    上面提到的兄弟方法 insertAdjacentElement 也可以用来对已存在的元素进行移动,换句话说:当传入该方法的是已存在于文档中的元素时,该元素仅仅只会被移动(而不是复制并移动)。

    如果你有以下 HTML:

    <div class="first">
     <h1>Title</h1>
    </div>
    
    <div class="second">
     <h2>Subtitle</h2>
    </div>

    然后操作一下,把 <h2> 搞到 <h1> 的后面去:

    const h1 = document.querySelector('h1')
    const h2 = document.querySelector('h2')
    
    h1.insertAdjacentElement('afterend', h2)

    于是我们就得到了这样的结果:

    <div class="first">
     <h1>Title</h1>
     <h2>Subtitle</h2>
    </div>
    
    <div class="second">
    	
    </div>

    替换 DOM 元素

    replaceChild? 这是几年前的做法了,每当开发者需要替换两个 DOM 元素,除了需要拿到这必须的两个元素之外,还需要获取他们的直接父元素:

    parentNode.replaceChild(newNode, oldNode)

    而如今,开发者们可以使用 replaceWith 就可以完成两个元素之间的替换了:

    oldElement.replaceWith(newElement)

    从用法上来说,要比前者清爽一些。

    需要注意的是:

  • 如果传入的 newElement 已经存在于文档中,那么方法的执行结果将是 newElement 被移动并替换掉 oldElement
  • 如果传入的 newElement 是一个字符串,那么它将作为一个 TextNode 替换掉原有的元素
  • 移除 DOM 元素

    和替换元素的老方法相同,移除元素的老方法同样需要获取到目标元素的直接父元素:

    const target = document.querySelector('#target')
    target.parentNode.removeChild(target)

    现在只需要在目标元素上执行一次 remove 方法就 ok 了:

    const target = document.querySelector('#target')
    target.remove()

    用 HTML 字符串创建 DOM 元素

    细心的你一定发现了,上文提到的 insertAdjacent 方法允许开发者直接将一段 HTML 插入到文档当中,如果我们此刻只想生成一个 DOM 元素以备将来使用呢?

    DOMParser 对象的 parseFromString 方法即可满足这样的需求。该方法可以实现将一串 HTML 或 XML 字符串转化为一个完整的 DOM 文档,也就是说,当我们需要获得预期的 DOM 元素时,需要从方法返回的 DOM 文档中获取这个元素:

    const createSingleElement = (domString) => {
    	const parser = new DOMParser()
     return parser.parseFromString(domString, 'text/html').body.firstChild
    }
    
    // usage
    const element = createSingleElement('<a href="./home">Home</a>')

    做一个检查 DOM 的小能手

    标准的 DOM API 为开发者们提供了很多便利的方法去检查 DOM 。比如,matches 方法可以判断出一个元素是否匹配一个确定的选择器:

    // <div class="say-hi">Hello DOM!</div>
    
    const div = document.querySelector('div')
    
    div.matches('div') // true
    div.matches('.say-hi') // true
    div.matches('#hi') // false

    contains 方法可以检测出一个元素是否包含另一个元素(或者:一个元素是否是另一个元素的子元素):

    // <div><h1>Title</h1></div>
    // <h2>Subtitle</h2>
    
    const $ = document.querySelector.bind(document)
    const div = $('div')
    const h1 = $('h1')
    const h2 = $('h2')
    
    div.contains(h1) // true
    div.contains(h2) // false

    一招鲜:compareDocumentPosition

    compareDocumentPosition 是一个强大的 API ,它可以快速判断出两个 DOM 元素的位置关系,诸如:先于、跟随、是否包含。它返回一个整数,代表了两个元素之间的关系。

    // 还是用上面的例子哈
    container.compareDocumentPosition(h1) // 20
    h1.compareDocumentPosition(container) // 10
    h1.compareDocumentPosition(h2) // 4
    h2.compareDocumentPosition(h1) // 2

    标准语句:

    element.compareDocumentPosition(otherElement)

    返回值定义如下:

  • 1: 两个元素不再同一个文档内
  • 2: otherElement 在 element 之前
  • 4: otherElement 在 element 之后
  • 8: otherElement 包含 element
  • 16: otherElement 被 element 所包含
  • 那么问题来了,为什么上面例子中第一行的结果是20、第二行的结果是10呢?

    因为 h1 同时满足“被 container 所包含(16)” 和 “在 container 之后”,所以语句的执行结果是 16+4=20,同理可推出第二条语句的结果是 8+2=10。

    DOM 观察者:MutationObserver

    在处理用户交互的时候,当前页面的 DOM 元素通常会发生很多变化,而有些场景需要开发者们监听这些变化并在触发后执行相应的操作。MutationObserver 是浏览器提供的一个专门用来监听 DOM 变化的接口,它强大到几乎可以观测到一个元素的所有变化,可观测的对象包括:文本的改变、子节点的添加和移除和任何元素属性的变化。

    如同往常一样,如果想构造任何一个对象,那就 new 它的构造函数:

    const observer = new MutationObserver(callback)

    传入构造函数的是一个回调函数,它会在被监听的 DOM 元素发生改变时执行,它的两个参数分别是:包含本次所有变更的列表 MutationRecords 和 observer 本身。其中,MutationRecords 的每一条都是一个变更记录,它是一个普通的对象,包含如下常用属性:

  • type: 变更的类型,attributes / characterData / childList
  • target: 发生变更的 DOM 元素
  • addedNodes: 新增子元素组成的 NodeList
  • removedNodes: 已移除子元素组成的的 NodeList
  • attributeName: 值发生改变的属性名,如果不是属性变更,则返回 null
  • previousSibling: 被添加或移除的子元素之前的兄弟节点
  • nextSibling: 被添加或移除的子元素之后的兄弟节点
  • 根据目前的信息,可以写一个 callback 函数了:

    const callback = (mutationRecords, observer) => {
    	mutationRecords.forEach({
     		type,
     	 target,
     		attributeName,
     		oldValue,
    		addedNodes,
    		removedNodes,
     } => {
     	switch(type) {
     		case 'attributes':
     			console.log(`attribute ${attributeName} changed`)
     			console.log(`previous value: ${oldValue}`)
    		 	console.log(`current value: ${target.getAttribite(attributeName)}`)
    		 	break
     		case 'childList':
     			console.log('child nodes changed')
     			console.log('added: ${addedNodes}')
     			console.log('removed: ${removedNodes}')
     			break
     		// ...
     	}
     })
    }

    至此,我们有了一个 DOM 观察者 observer,也有了一个完整可用的 DOM 变化后的回调函数 callback,就差一个需要被观测的 DOM 元素了:

    const target = document.querySelector('#target')
    observer.observe(target, {
    	attributes: true,
     attributeFilter: ['class'],
     attributesOldValue: true,
     childList: true,
    })

    在上面的代码中,我们通过调用观察者对象的 observe 方法,对 id 为 target 的 DOM 元素进行了观测(第一个参数就是需要观测的目标元素),而第二个元素,我们传入了一个配置对象:开启对属性的观测 / 只观测 class 属性 / 属性变化时传递属性旧值 / 开启对子元素列表的观测。

    配置对象支持如下字段:

  • attributes: Boolean,是否监听元素属性的变化
  • attributeFilter: String[],需要监听的特定属性名称组成的数组
  • attributeOldValue: Boolean,当监听元素的属性发生变化时,是否记录并传递属性的上一个值
  • characterData: Boolean,是否监听目标元素或子元素树中节点所包含的字符数据的变化
  • characterDataOldValue: Boolean,字符数据发生变化时,是否记录并传递其上一个值
  • childList: Boolean,是否监听目标元素添加或删除子元素
  • subtree: Boolean,是否扩展监视范围到目标元素下的整个子树的所有元素
  • 当不再监听目标元素的变化时,调用 observer 的 disconnect 方法即可,如果需要的话,可以先调用 observer 的 takeRecords 方法从 observer 的通知队列中删除所有待处理的通知,并将它们返回到一个由 MutationRecord 对象组成的数组当中:

    const mutationRecords = observer.takeRecords()
    callback(mutationRecords)
    observer.disconnect()

    怕啥都不要怕 DOM

    尽管大部分 DOM API 的名字都很长(写起来很麻烦),但它们都是非常强大并且通用的。这些 API 往往旨在为开发者提供底层的构建单元,以便在此之上建立更为通用和简洁的抽象逻辑,因此从这个角度出发,它们必须提供一个完整的名称以变得足够明确和清晰。

    只要能发挥出这些 API 本应该发挥出的潜能,多敲几下键盘又何妨呢?

    DOM 是每个 JavsScript 开发者必不可少的知识,因为我们几乎每天都在使用它。莫怕,大胆激发自己操作 DOM 的洪荒之力吧,尽早成为一个 DOM 高级工程师。

    相关推荐

    Vue的框架(了解)

    前端MVC设计模式MVC设计模式,其实就是将前端实现某个业务的所有代码划分为三部分Model:模型,指数据模型,这个数据一般来自于服务器View:视图,指页面标签内容Controller:控制...

    Vue.js实战 第五章练习一

    练习要求:在原有表格基础上,新增一项是否选中该商品的功能,总价变为只计算选中商品的总价,同时提供一个全选的按钮。实现思路:按照vue数据和dom元素双向绑定的特性,定义allCheckStatus变量...

    Vue基础到进阶教程之class和style绑定

    关于class和style我们并不陌生,这个在学习css的时候就是家常便饭了,操作元素的class列表和内联样式是数据绑定的一个常见需求。因为它们都是属性,所以我们可以用v-bind处理它们,...

    深入Vue 必学高阶组件 HOC「进阶篇」

    作者:ssh转发连接:https://mp.weixin.qq.com/s/seKoLSIMtTd1sU4uDrgZCA前言高阶组件这个概念在React中一度非常流行,但是在Vue的社区里讨论...

    周末大礼包,23道高质量中级前端面试题。金九银十,建议收藏

    这套面试题考察的内容比较常见,涉及到JavaScript、ES6、CSS、Vue、简单算法,浏览器相关知识等。题目列表1.JavaScript的数据类型有哪些2.什么是同源策略3.跨域的方法...

    vue3.0-摒弃Object.defineProperty,基于 Proxy 的观察者机制

    写在前面:11月16日早上,Vue.js的作者尤大大在VueToronto的主题演讲中预演了Vue.js3.0的一些新特性,其中一个很重要的改变就是Vue3将使用ES6的Proxy作...

    程序员都必掌握的前端教程之VUE基础教程(七)

    阅读本文约需要10分钟,您可以先关注我们,避免下次无法找到。本篇文章成哥继续带大家来学习前端VUE教程,今天主要讲解VUE的表单处理等知识点。下面我们就一起来学习该块内容吧!01简介在日常开发中,我...

    web前端开之网站搭建框架之vue详解

    网站搭建框架之vueVue是web前端快速搭建网站的框架之一。它与jQuery有所不同,是以数据驱动web界面(以操作数据改变页面,而jQuery是以操作节点来改变页面),同时,vue还实现了数据的双...

    vue3.0尝鲜-基于 Proxy 的观察者机制探索

    Vue.js的作者尤大大在VueToronto的主题演讲中预演了Vue.js3.0的一些新特性,其中一个很重要的改变就是Vue3将使用ES6的Proxy作为其观察者机制,取代之前使用...

    TypeScript 设计模式之观察者模式

    一、模式介绍1.背景介绍在软件系统中经常碰到这类需求:当一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。这是建立一种「对象与对象之间的依赖关系」,一个对象发生改变时将「自动通知其他...

    vue面试3

    1.单页面应用与多页面应用的去别2.简述一下Sass、Less,且说明区别?他们是动态的样式语言,是CSS预处理器,CSS上的一种抽象层。他们是一种特殊的语法/语言而编译成CSS。变量符不一样,les...

    VUE v-bind 数据绑定

    动态的绑定一个或多个attribute,也可以是组件的prop。缩写::或者.(当使用.prop修饰符)期望:any(带参数)|Object(不带参数)参数:attrOrP...

    vue初学习之自定义选择框实现

    v-model简单介绍在使用vue的过程中会经常用到input和textarea这类表单元素,vue对于这些元素的数据绑定和我们以前经常用的jQuery有些区别。vue使用v-model实现这些标签...

    Vue实现拖拽穿梭框功能四种方式

    一、使用原生js实现拖拽打开视频讲解更加详细Vue实现拖拽穿梭框功能的四种方式_哔哩哔哩_bilibili<html><head><meta...

    Vue3.x setup 语法糖实现props双向绑定

    1.背景为了封装一下Element-Plus的分页插件,需要实现父子组件之间的传值。2.父组件<scriptsetuplang="ts">letqueryPa...

    取消回复欢迎 发表评论: