DOM编程与事件

918 阅读6分钟

1.DOM(Document Object Model)

文档对象模型

2.获取元素的API

  • windows.idXXX或者直接idXXX,举例:
  • document.getElementById('idxxx'),一般用上面的方式获取元素,对于id名称为关键字比如:parent的时候,用这种方法。
  • document.getElementsByTagName('div')[0]
  • document.getElemetsByClassName('red')[0]
  • document.querySelector('#idXXX'),这种写法与CSS选择器的写法相同。
  • document.querySelectorAll('.red')[0] 推荐使用最下面两种,需要兼容IE的时候,才用getElement(s)ByXXX
    对于 获取特定元素:
  • 获取HTML元素:document.documentElement
  • 获取head元素:document.head
  • 获取body元素:document.body
  • 获取窗口(窗口不是元素):window
  • 获取所有元素:document.all,需要注意:由于这种方式最开始只能在IE浏览器中使用,后来其他浏览器才沿用过去的。但是为了区分,document.all的值,只对ie为true,所以可以用于检测当前浏览器是不是IE,举例: 在IE中: 在Chorme中:
2.1 获取到的元素的原型链

随意获取页面上的一个元素,用console.dir()可以看到该元素的元素本身->HTMLDivElement.prototype->HTMLElement->Element->Node->EventTarget->Object完整原型链。图示:

原图地址: github.com/Scott-YuYan…

  • 第一层:HTMLDivElement.prototype --所有div共有的属性
  • 第二层:HTMLElement.prototype --所有HTML标签共有的属性
  • 第三层:Element.prototype -- XML/HTML所共有的属性
  • 第四层:Node.prototype -- (Node是接口)所有节点的共有属性
  • 第五层:EventTarget.prototype -- 最重要的函数属性:addEventListener
  • 最后一层:Object.prototype

3.节点的增删改查

3.1 增加节点

创建标签节点

  • let div1 = document.createElement('div')
  • document.createElement('style')
  • document.createElement('script')
  • document.createElement('li')

创建一个文本节点

  • text1=document.createTextNode('你好')

标签中插入文本

  • div1.appendChild(text1)
  • div1.innerText = 'ni hao' 或者 div1.textContent='ni hao'
  • 但是不能用div1.appendChild('ni hao')

插入到页面中

  • 创建的标签默认处于JS线程中,需要将标签插入到body或者head中,才会生效
  • document.body.appendChild(div)或者 已存在的元素.appendChild(div)

注意, 例:页面中有div#test1 和div#test2

let div = document.createElement('div');
test1.appendChild(div);
test2.appendChild(div);
问:最终的div出现在哪里?(test1? test2? 还是两个都有?)

答案是只会在test2中,同一个元素不能出现在两个地方,除非复制一份,可使用div.cloneNode(true)--true表示深拷贝,即将所有后代一起拷贝。

3.2 删节点

两种形式:

  • 旧方法:child.parentNode.removeChild(child) --找到自己的父亲节点,然后用父亲节点删掉自己
  • 新方法,但是不兼容IE:child.remove()
  • 节点删除后,只是被放在内存中,还是可以被恢复的。如果节点被置为空:child = null那么该节点就会被垃圾回收所彻底删除。
3.3 改节点

写标准属性:

  • 改class:div.className='red blue'(会将原属性直接覆盖)
  • 改class:div.classList.add('red')
  • 改style:div.style='width:100px;color:blue;'
  • 改style的一部分:div.style.width='200px'
  • 改大小写:div.style.backgroundColor='white'(-c被简化为C)
  • 改attribute属性:div.setAttribute('','')
  • 改data-* 属性:div.dataset.x='frank'

读标准属性:

  • div.classList / a.href
  • div.getAttribute('class')/a.getAttribute('href') 两种方式都可,有些地方稍微有些不同:
<a href="/xxx" id="aTarget">链接</a>

改事件处理函数

  • div.onclick()函数默认为null,点击不会有任何事情产生。如果修改div.onclick()为调用函数fn(),那么点击该div的时候,就会通过fn.call(div,event)调用这个函数

改文本内容:

  • div.innerText = 'xxx'
  • div.textContent = 'xxx'

改HTML内容:

  • div.innerHTML = '<h2>二级标题</h2>' //大小写不能错,且比较消耗内存
3.4 查节点
  • 查父亲:node.parentNode | node.parentElement
  • 查爷爷:node.parentNode.parentNode
  • 查子代:node.childNodes | node.children 注意:
        <ul id="liTest">
            <li>儿子</li>
            <li>儿子</li>
            <li>儿子</li>
        </ul>

竟然有7个childNodes,原因在于需要注意前后的回车空格。将li写成一行,或者使用children可避免这种情况。

  • 查兄弟姐妹:node.parentNode.childNodes | children还要排除自己
  • 查第一个 | 最后一个孩子:node.firstChild | node.lastChild
  • 查上一个 | 下一个兄弟节点:node.previousSibling | node.nextSibling --这种方式会获取到文本内容
  • 查上一个 | 下一个兄弟元素节点:node.previousElementSibling | node.nextElementSibling --这种方式不会获取到文本内容

4.为什么DOM操作会比较慢

原因在于:DOM操作是跨线程的。浏览器分为渲染引擎和JS引擎,JS引擎不能操作页面,只能操作JS,而渲染引擎不能操作JS只能渲染页面。当使用document.body.appendChild(div1)为body增加元素时,浏览器发现JS在body里面加了div1对象,浏览器会通知渲染引擎也新增一个div对象。新增加的div元素的所有属性,全部照抄div1对象。
推荐阅读: 1.为什么说DOM操作很慢

5.捕获与冒泡

由外向里叫事件捕获-网景发明,由内向外叫事件冒泡-IE发明 示意图:

5.1 添加事件绑定的API
  • ie5*: dom.attachEvent('onlick',fn) //冒泡
  • 网景: dom.addEventListener('click'.fn)//捕获
  • W3C;dom.addEventListener('click',fn,bool) //bool为空或者为falsy就让fn走冒泡,为true的话fn就走捕获

问,如下代码:

<div id="ye">
    <div id="ba">
        <div id="er">
            测试
        </div>
    </div>
</div>

\quad 当我们点击测试的时候,算不算ye ba er被点击了,如果算,那么是先执行ye还是先执行er?
问题的答案是:不确定,因为在不同的浏览器中,结果是不一样的。
\quad 继续问,那么如果我对同一个对象即增加了监听事件,又增加了捕获事件,那么触发的顺序是什么?
问题的答案是:谁先添加先执行谁。

5.2 事件event.target与event.currentTarget的区别?
  • event.target-用户操作的元素
  • event.currentTarget-程序员监听的元素
  • this是event.currentTarget 举例:
<div>
	<span>文字</span>
</div>

当我们给div添加事件监听,用户点击文字的时候,e.target指的就是span,而event.currentTarget指的就是div。

5.3 取消冒泡

对于需要取消冒泡的情况,可以使用e.stopPropagation,那么浏览器就不会继续执行外层的绑定函数,一般适用于封装某些独立的组件。但是注意,有些事件可以取消冒泡,有些事件是不能取消冒泡的,举例:

  • 点击事件:

  • 鼠标滚动事件:

那么如何阻止鼠标滚动事件,举例: 代码-css:

::-webkit-scrollbar {
    display: none;
}

代码-js:

x.addEventListener('wheel', (e)=>{
  e.preventDefault();
});

x.addEventListener('touchstart',(e)=>{
  e.preventDefault();
});

6.自定义事件

前面我们提到了基本的冒泡和捕获事件,那么浏览器是自带100多种事件的,可查看MDN-事件看到浏览器所包括的100多种事件。
除此之外,我们还可以自定义事件,举例(还是以5.1中的html为例子):

<script>
    let ye = document.querySelector("#ye");
    ye.addEventListener('click',()=>{
        const myEvent = new CustomEvent('myEvent',{
            detail:{'name':'zhangsan',},
            bubbles:true,//设置自定义事件的冒泡
            cancelable:false,//设置冒泡是否可被取消

        })
        ye.dispatchEvent(myEvent)
    })
    ye.addEventListener('myEvent',(event)=>{
        console.log(event.detail);
    })
</script>

7.事件委托

事件委托,顾名思义,就是将本该由事件监听对象去完成的事件,委托给其他对象完成。比如以下场景:

  • 场景1:需要给100个按钮添加点击事件,需要怎么做?
<div id="outer">
    button#id$*100
</div>

那么可以将监听事件委托给button的外面一层:

<script>
    let outer = document.querySelector("#outer");
    outer.addEventListener('click',(event)=>{
        let target = event.target;
        if (target.tagName.toLowerCase() === 'button'){
            console.log(target.id)
        }
    })
</script>
  • 2.场景2 需要监听目前不存在的事件 举例:
<div id="outer">
</div>
<script>
    let outer = document.querySelector("#outer");
    const button = document.createElement("button");
    button.textContent = 'click';
    setTimeout(()=>{
        outer.appendChild(button);
    },1000)
</script>

click不存在页面上,那么同样可以把事件监听委托到外部元素上:

    outer.addEventListener('click',(event)=>{
        const tag = event.target;
        if (tag.tagName.toLowerCase() === 'button'){
            console.log(tag.innerText)
        }
    })

这么做的优点:1 节约监听数量 2 可以监听动态生成的元素。

8.属性同步

  • 标准属性:对div标准属性的修改,会被浏览器同步到页面中,比如id、className、title
  • data-*属性:同上
  • 对非标准属性的修改,只会停留在JS线程中,不会同步到页面中。举例:
<div id="test" x="test" data-x="test"></div>
        
function changeAttr() {
    let div1 = document.querySelector('#test');
    div1.id = 'newTest';
    div1.dataset.x = 'newTest'; // 同步过去了
    div1.x = 'newTest'; // 没有同步过去
}

运行后: 可以看到非标准属性x的属性值没有改变。

8.1 property vs attribute
  • property属性:JS线程中div1的所有属性,叫做div1的property
  • attribute属性:渲染引擎中div1对应标签的属性,叫做attribute 二者区别:
  • 二者大部分情况相同,但是如果不是标准属性,只在一开始的时候相同
  • attribute只支持字符串,而property支持字符串,布尔等类型