技术思考【元素操作】

172 阅读8分钟

元素操作

DOM 树里每一个内容都称之为节点,元素操作实质上是元素节点操作。节点类型又分为以下三种:

  1. 元素节点
    • 所有的标签 比如 bodydiv
    • html 是根节点
  2. 属性节点
    • 所有的属性 比如 href
  3. 文本节点
    • 所有的文本

重点记住元素节点,可以更好的让我们理清标签元素之间的关系。

使用原生js实现内容操作

增加的本质就是在赋值

  • innerText
  • innerHTML

修改的本质是重新赋值

  • innerText
  • innerHTML

删除的本质是赋值空字符串

  • innerText
  • innerHTML

查询的本质是获取值

  • innerText
  • innerHTML

使用原生js实现属性操作

  • 添加内置属性:元素.属性 = 值
  • 添加自定义属性
    1. document.setAttrute()
    2. data-

没学过

  • 元素.属性 = 新值

  • 元素.属性

使用原生js实现元素操作

查找节点

基于文档查找

  • document.querySelector (选择器):在整个文档中查找满足条件的元素,只返回第一个,返回值是一个 dom 元素,就可以直接操作

  • document.querySelectorAll (选择器):在整个文档中查找所有满足条件的元素,返回一个伪数组

    • 不管有没有找到都是返回伪数组
    • 伪数组不能直接操作,需要遍历出每一个 dom 元素再操作

基于元素查找

除了用基于文档查找的方式获取元素外,还可以用元素节点之间的关系来获取、查找元素。

  1. 父节点

    • parentNode

      返回最近一节父元素节点,找不到返回 null

      <div>
          <p></p>
      </div>
      
      console.log(p.parentNode);  // div
      console.log(div.parentNode);  // null
      
  2. 子节点

    <div class="erweima">
        <!-- abc -->
        <span>aaa</span>
        <img src="images/code.png" alt="" />
        <span> X3 </span>
    </div>
    
    • childNodes

      let erweima = document.querySelector('.erweima')
      console.log(erweima.childNodes)  // NodeList(9) [text, comment, text, span, text, img, text, span, text]
      

      获得所有子节点、包括文本节点(空格、换行)、注释节点等。

    • children

      let erweima = document.querySelector('.erweima')
      console.log(erweima.children)  // HTMLCollection(3) [span, img, span]
      
      1. 仅获得所有元素节点
      2. 返回的还是一个伪数组
      3. 该伪数组不支持使用 forEach 进行遍历。
  3. 兄弟节点

    • 下一个兄弟元素节点

      nextElementSibling 属性

    • 上一个兄弟元素节点

      previousElementSibling 属性

    注意:

    还有方法可查询下一个或上一个兄弟节点,包含所有节点(注释,文本),不常使用,了解即可。

    • 下一个兄弟元素节点 nextSibling
    • 上一个兄弟元素节点 previousSibling

增加节点

很多情况下,我们需要在页面中增加元素。一般情况下,可采取新增节点的操作:

1.创建一个新的节点
2.把创建的新的节点放入到指定的元素内

  • 创建节点

    即创造出一个新的网页元素,再添加到网页内,一般先创建节点,然后插入节点。

    let 变量 = document.createElement('标签名')
    
  • 追加节点

    要想在界面看到,还得插入到某个父元素中

    1. 插入到父元素的最后一个子元素:

      父元素.appendChild(创建的新标签变量)
      

      会将子元素追加到父容器的最后

    2. 插入到父元素中某个子元素的前面:

      父元素.insertBefore(创建的新标签变量, 参考的子元素)
      

      会将指定的元素插入到某个子元素之前。如果第二个参数写 null ,则效果与 appendChild 一致,做追加。但是不能不写第二个参数,否则报错。

    3. 克隆节点

    无论用 appendChild 方法还是 insertBefore 都是把原来父元素的子节点挪到新的父元素下,导致原父元素的子节点被删掉。克隆则是把该子节点复制新一份,不会删除原来的子节点。

    元素.cloneNode(布尔值)
    
    1. 若为 true ,则代表克隆时会包含后代节点一起克隆
    2. 若为 false ,则代表克隆时不包含后代节点
    3. 默认为 false

删除节点

  1. 通过父元素删除

    父元素.removeChild(要删除的元素)
    
    • 如不存在父子关系则删除不成功,不能用爷爷删孙子,一定是要父子关系。
    • 删除节点和隐藏节点( display:none ) 有区别的: 隐藏节点还是存在的,但是删除,则从 html 中删除节点。
  2. 删除自身

    元素.remove()
    

重绘和重排

浏览器渲染页面的步骤如下所示:

  1. 解析( ParserHTML,生成 DOM 树( DOM Tree )
  2. 同时解析( ParserCSS,生成样式规则 ( Style Rules )
  3. 根据 DOM 树和样式规则,生成渲染树( Render Tree )
  4. 进行布局 Layout (回流/重排):根据生成的渲染树,得到节点的几何信息(位置,大小)
  5. 进行绘制 Painting (重绘): 根据计算和获取的信息进行整个页面的绘制
  6. Display : 展示在页面上

重绘

由于节点(元素)的样式的改变并不影响它在文档流中的位置和文档布局时(比如: colorbackground-coloroutline 等), 称为重绘。

即当元素节点发生颜色、背景颜色、字体大小这种不影响整体布局排列以及宽高、仅仅是视觉效果上的改变,则会引起重绘。

重排

Render Tree 中部分或者全部元素的尺寸、结构、布局等发生改变时,浏览器就会重新渲染部分或全部文档的过程称为 回流。

即当元素节点发生边框、内外边距、宽高等这种改变自身大小以及整体布局的属性时,会引发回流,也称为重排。

总结

重绘不一定引起回流,而回流一定会引起重绘

案例

元素操作

整体效果如下图所示。

案例.png

<h1>城市选择:</h1>
<!-- multiple:选择多项 -->
<select id="src-city" name="src-city" multiple>
    <option value="1">北京</option>
    <option value="2">上海</option>
    <option value="3">深圳</option>
    <option value="4">广州</option>
    <option value="5">西红柿</option>
</select>
<div class="btn-box">
    <!--实体字符-->
    <button id="btn1">&gt;&gt;</button>
    <button id="btn2">&lt;&lt;</button>
    <button id="btn3">&gt;</button>
    <button id="btn4">&lt;</button>
</div>
<!-- multiple 支持多选 -->
<select id="tar-city" name="tar-city" multiple></select>

点击第一个按钮时,左侧选择表单内的所有元素移动到右边;点击第二个按钮时,右边所有的元素移动到左边;点击第三个按钮时,左边被选中的元素移动到右边;点击第四个按钮时,右边被选中的元素移动到左边。

解题思路:

  1. 先获取事件源,左右两侧的选择表单与四个按钮。为四个按钮绑定点击事件。

    let srcCity = document.querySelector('#src-city')
    let tarCity = document.querySelector('#tar-city')
    let btn1 = document.querySelector('#btn1')
    let btn2 = document.querySelector('#btn2')
    let btn3 = document.querySelector('#btn3')
    let btn4 = document.querySelector('#btn4')
    
  2. 当第一个按钮触发点击事件后(第二个按钮操作类似,故不赘述):

    1. 获取左侧表单的所有孩子元素节点 children ,打印出来可以看到获取到所有的 option 子元素。

    2. 对所有的 option 子元素用 for 进行循环遍历,再 appendChild 追加到右侧表单中。

    3. 不用 forEach 遍历是因为用 appendChild 获取到的子元素伪数组没有 forEach 方法,如果使用 forEach 则会报以下错误。

      案例报错.png

    注意:

    遍历完一次,要把变量 i 自减一,让索引恢复。如果没有自减一,会以一下流程进行运作:初始值 i 为0,索引值为0的元素 “北京” 被移动到右侧表单中,此时左侧表单还剩下4个元素,索引 i ++,值为1,进行下一次的遍历,让索引值 i 为1的元素移动到右侧。

    由于“北京”已经被移动到右侧,因此“上海”变为索引第0项。而索引号此时查找的是1,因此会跳过“上海”把“深圳”移动到右侧。最后会造成有两个元素遗留在左侧。

    这也是为什么在增删改查操作数据时是依据 id 来操作而不是依据索引号,因为索引号会有变更,不是唯一对应值。

    1. 最后再注重细节,判断如果左侧没有元素(即左侧孩子长度为0)则直接跳出循环,函数结束使用 return ,后面的语句不再执行。
    btn1.addEventListener('click', function() {
        let options = srcCity.children
        for (let i = 0; i < options.length;) {
            if (options.length == 0) {
                return
            }
            tarCity.appendChild(options[i])
        }
    })
    
    btn2.addEventListener('click', function() {
        let sta = tarCity.children
        for (let i = 0; i < sta.length;) {
            if (sta.length == 0) {
                return
            }
            srcCity.appendChild(sta[i])
        }
    })
    
  3. 点击第三个按钮触发回调函数,执行以下操作(第四个按钮的操作类似,故不过多赘述):

    1. 获取当前表单内所有的子元素,用变量 option 接收。

    2. option 进行循环遍历。

    3. if 判断,如果孩子元素处于被选择状态,则移动到右侧去。让变量 i 自减一,从0开始继续遍历。

    注意:

    这里之所以不会造成死循环,是因为我们把变量自减一写在了 if 判断里,只有符合被选中状态才让变量自减一,如果不符合则继续执行下去。

    btn3.addEventListener('click', function() {
        let options = srcCity.children
        for (let i = 0; i < options.length; i++) {
            if (options[i].selected) {
                tarCity.appendChild(srcCity.children[i])
                i--
            }
        }
    })
    
    btn4.addEventListener('click', function() {
        let sta = tarCity.children
        for (let i = 0; i < sta.length; i++) {
            if (sta[i].selected) {
                srcCity.appendChild(tarCity.children[i])
                i--
            }
        }
    })
    

实现一个简单的日期插件

自制一个时间插件,让页面可显示当前时间获取显示倒计时。

显示当前时间

function timer(document) {
    setInterval(function() {
        let date = new Date()
        let year = date.getFullYear()
        let month = date.getMonth() + 1
        let day = date.getDate()
        let hour = date.getHours()
        let minute = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
        let second = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()

        return document.innerHTML =
            `今天是${year}${month}${day}日,现在是${hour<10?'0'+hour:hour}:${minute}:${second}`
    }, 1000)
}

timer(document.querySelector('h1'))

时间插件.png

本案例没有复杂的点,先 new 一个事件对象,再一一调用即可。

倒计时

先获取现在的时间戳,再获取将来的时间戳,进行相减再换算即可获取相差时间,在定时器内自减一即可。减到0就停止定时器。

由于时间戳是以毫秒为单位,因此换算公式如下:

  • 小时:毫秒 / 1000 (秒) / 60 (分钟) / 60 (小时),取整数即可。
  • 分钟:毫秒 / 1000 (秒) % 3600 (小时的余数即分钟) / 60 ,取整数即可。
  • 秒: 毫秒 / 1000 % 60 。
// 获取将来的时间
let date = new Date('2022-1-27 18:30:00')
let oldTime = date.getTime();

// 开启定时器,让倒计时自动开启
function times(oldTime, hourEle, minuteEle, secondEle, document) {
    let timeId = setInterval(() => {
        // 获取现在的时间
        let nowTime = Date.now();
        // 相减除以1000,把毫秒转换为秒
        let res = (oldTime - nowTime) / 1000

        // 判断,如果时间走完则停止定时器
        if (res === 0) {
            clearInterval(timeId)
        }

        // 计算出时、分、秒
        let hour = parseInt(res / 3600)
        let minute = parseInt(res % 3600 / 60)
        let second = parseInt(res % 60);
        // 把算出来的时分秒输出到相应的模块中,不够2位数在前面补0
        hourEle.innerHTML = hour < 10 ? '0' + hour : hour
        minuteEle.innerHTML = minute < 10 ? '0' + minute : minute
        secondEle.innerHTML = second < 10 ? '0' + second : second

        timer = new Date()
        let nowHour = timer.getHours()
        let nowMinute = timer.getMinutes()
        let nowSecond = timer.getSeconds()
        document.innerHTML =
            `现在是${nowHour<10?'0'+nowHour:nowHour}:${nowMinute<10?'0'+nowMinute:nowMinute}:${nowSecond<10?'0'+nowSecond:nowSecond}`
    }, 1000)
}
times(oldTime, hourEle, minuteEle, secondEle, document.querySelector('.tips'))

倒计时.png

错误汇总

  1. 使用 forEach 遍历所有子节点

    let sta = tarCity.children
    sta.forEach(element => {
        console.log(1);
    });
    

    会报以下错误。

    this.children.forEach is not a function parentNode.children
    

    获取所有元素节点,不包括文本和注释,返回的是一个伪数组,但不能使用 forEach 进行遍历。

    foreach方法.png 可以看到,该伪数组没有 forEach 方法,因此会报错。

  2. 删除失败

    删除子节点时出现报错。

    Failed to execute 'removeChild' on 'Node': The node to be  removed is not a child of this node.
    

    执行 removeChild 失败了,你要删除的节点不是当前 node 的子节点。

    如果使用爷爷删孙子之类的就会出现这种错误,一定是要父子关系才行。

  3. 追加失败

    追加元素时出现报错。

    Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.
    
    Failed to execute 'insertBefore' on 'Node': parameter 1 is not of type 'Node'.
    

    参数1不是类型 node 。 错误为添加的不是节点类型。常见的犯错行为是创建元素时没加引号;

    let li = document.createElement(li)
    

    以及追加元素时为参数添加引号,此时该参数会被解析为字符串,而不是变量。

    tarCity.appendChild('li')