DOM编程

109 阅读5分钟

一、网页其实是一棵树

  • JS是如何操作这棵树?
  1. 浏览器往window上加一个document即可(Document Object Model,文档对象模型DOM)
  2. JS用document操作网页(把网页抽象成一个document对象,并对其进行操作)

二、DOM API

1、获取元素(element),也叫标签(tag)

  1. window.idxxx或者直接idxxx(最方便)
  2. document.getElementById('idxxx')(如果id的名字和全局属性名字一样,就用这个,但大部分时候都用1.)
  3. document.getElementsByTagName('div')[0](获取第0个div标签,如果不写[0],就会获取到所有div,且不能操作)
  4. document.getElementsByClassName('red')[0](获取第0个类名为'red'的标签)
  • 以上2.3.4.永远不要用,除非是要兼容IE
  1. document.querySelector('#idxxx')(和1.的区别在于,括号里的内容可以写得十分复杂,如document.querySelector('div>span:nth-child(2)'),获取div里的span,且这个span还是第2个元素(只要找到满足条件的第1个span,如用6. All可找到所有满足条件的span))
  2. document.querySelectorAll('.red')[0]/document.querySelectorAll('#idxxx')[0](不写[0]就不能指定获取第几个,且不能操作)
  • 工作中用5.6.,如果是demo可以用1.,但还是推荐用正规5.6.

2、获取特定元素

  1. document.documentElement,获取html元素(document.documentElement.tagName//相对应获取到的标签名是HTML)
  2. document.head,获取head元素
  3. document.body,获取body元素
  • 以上,对应获取到的标签名都是大写,如HTML
  1. window,获取窗口(窗口不是元素),有什么用呢?比如可以通过window添加一些监听事件,如window.onclick=()=>{console.log('hi')}
  2. document.all,获取所有元素,且这是第6个falsy值,对此,展开介绍如下:
  • document.all是由IE发明的,所以早期程序员会用document.all来检测当前页面是不是IE
if(document.all){
  console.log('IE浏览器');...在IE运行的代码
}else{
  console.log('非IE浏览器');...在非IE运行的代码
}
  • 现在所有浏览器都有document.all功能,但原在IE运行的代码现也只能在IE运行,在非IE不能运行,故“赐”document.all为假,执行else
  • if(document.all){console.log('3')}else{console.log('4')},打出4

三、获取到的元素是?——一个对象

  • 这个对象有六层原型 微信图片_20230408092518.png(截图来自饥人谷课件) 2019-10-17-20-36-26.png(截图来自饥人谷课件)

四、节点

1、节点node包括(x.nodeType得到一个数字,如div.nodeType//1)

  1. 1表示元素Element,也叫标签Tag
  2. 3表示文本Text
  3. 8表示注释Comment
  4. 9表示文档Document
  5. 11表示文档片段DocumentFragment
  • 记住1.2.即可

2、节点的增删改查

2.1、增
  1. 创建一个标签节点,let div1=document.creatElement('div'),style、script、li等
  2. 创建一个文本节点,let text1=document.creatTextNode('你好')
  3. 标签里面插入文本
  • div1.appendChild(text1),Node提供的函数
  • div1.innerText='你好'/div1.textContent='你好',Element提供的函数
  • 但是不能用div1.appendChild('你好')
  1. 插入页面中
  • 创建的标签默认处于JS线程中,须插入head或body里才生效
  • document.body.appendChild(div)/已在页面中的元素.appendChild(div)
  • 一个标签做两次插入至不同元素中,最终这个标签会出现在哪? 微信图片_20230408095042.jpg
2.2、删
  1. div1.parentNode.removeChild(div1),旧方法
  2. div2.remove(),新方法,但是不兼容IE
  • 以上只是把一个node移出页面,但还在内存中
2.3、改
2.3.1、写标准属性
  1. 改class
  • div.className='red blue'(全覆盖),若想再加一个black,div.className+='black'//'red blue black'
  • 也可用div.classList.add('black')//'red blue black'
  1. 改style
  • div.style='width:100px;color:blue;',全改了,比如之前的position也没有了
  • 所以用div.style.width='200px',只改width,其余还在
  1. 注意大小写
  • div.style['background-color'],这是页面原来的,改写时需写成div.style.backgroundColor='black'
  1. 改data-*属性 微信图片_20230408101055.jpg
2.3.2、读标准属性

微信图片_20230408101409.jpg

2.3.3、改事件处理函数,如何理解?
  1. 先给一个<div id="test">test</div>
  2. div.onclick默认为null,点击div不会有任何事件发生,即console.log(test.onclick)//null
  3. 但是如果把div.onclick改为一个函数fn
test.onclick=function(x){
  console.log(this)
  console.log(x)
}
  1. 那么点击div时,浏览器就会调用这个函数,并且是这样调用的
fn.call(div,event)
test.onclick.call(test,event)
  1. div会被当做this,event则包含了点击事件的所有信息
  2. div.addEventListenerdiv.onclick的升级版
2.3.4、改内容(改“儿子”)
  1. 改文本内容
  • div.innerText='xxx'/div.textContent='xxx'
  1. 改HTML内容
  • div.innerHTML='<span>hi</span>',这里面的内容可以写得很复杂,但不能超过约两万字
  1. 改标签
  • div.innerHTML=''//先清空,div.appendChild(div2)//再加内容
2.3.5、改“爸爸”
  • newParent.appendChild(div),直接从原来地方消失,到新这里
2.4、查
2.4.1、查“爸爸”
  • node.parentNode/node.parentElement
2.4.2、查“爷爷”
  • node.parentNode.parentNode
2.4.3、查“子代”
  • node.childNodes/node.children
  • 探讨长度问题
  1. 微信图片_20230408104215.jpgchildNodes包括文本节点的
  2. 但如果<ul id="test"><li>1</li><li>2</li><li>3</li></ul>console.log(test.childNodes.length)//3
  3. 微信图片_20230408105029.jpg
  4. 微信图片_20230408105735.jpg
2.4.4、查“兄弟姐妹”
  • node.parentNode.childNodes/node.parentNode.children
  • 这是找到爸爸的小孩,还要排除掉自己,并且childNodes还要排除掉文本节点,更麻烦
  • 所以使用children,遍历爸爸的小孩,不是自己就装进一个空数组里
let Siblings=[]
let c2=div2.parentNode.children
for(let i=0;i<c2.length;i++){
  if(c2[i]!==div2){
    Siblings.push(c2[i])
  }
}
2.4.5、查看“老大”
  • node.firstChild
2.4.6、查看“老幺”
  • node.lastChild
2.4.7、查看上一个“孩子”、下一个“孩子”
  • node.previousSibling
  • node.nextSibling
  • 以上,有可能会查到文本节点,可以用node.previousElementSiblingnode.nextElementSibling
2.4.8、遍历一个div里的所有元素
travel=(node,fn)=>{
  fn(node)
  if(node.children){
    for(let i=0;i<node.children.length;i++){
      travel(node.children[i],fn)
    }
  }
}
travel(div1,(node)=>console.log(node))

五、DOM操作是跨线程的

浏览器功能划分为渲染引擎(渲染HTML和CSS)和JS引擎(执行JS),这是两个线程。

1、各线程各司其职

  • JS引擎不能操作页面,只能操作JS
  • 渲染引擎不能操作JS,只能操作页面
  • 那么document.body.appendChild(div1),这句JS是如何操作页面的?

2、跨线程通信

  • 当浏览器发现JS在body里加了个div1对象
  • 浏览器就会通知渲染引擎在页面里也新增一个div元素
  • 新增的div元素所有属性都照抄div1对象 微信图片_20230408134047.png(截图来自饥人谷课件)
  • 插入新标签的完整过程 微信图片_20230408134318.png(截图来自饥人谷课件) 微信图片_20230408134454.png(截图来自饥人谷课件)
  • 以上例子,若无test.clientWidth,浏览器只会执行test.classList.add('end')操作,就无宽度从100-200的动画了,而test.clientWidth是获取这个div的客户端宽度,test.classList.add('start')必须立即渲染,不然无法得知

3、两个线程的属性同步问题

  1. 标准属性的修改,会被浏览器同步到页面中,如id、className、title等
  2. data-*属性也一样
  3. 非标准属性的修改,只停留在JS线程中,如自定义的x属性
  4. 所以,如果有自定义属性,又想同步到页面中,使用data-作为前缀,data-x

4、property属性和attribute属性

  • JS线程中的属性叫做property(对象的属性)
  • 渲染线程中的属性叫做attribute(标签的属性)
  • 大部分,同名的property和attribute值相等
  • 如果不是标准属性,那么只在一开始时相等
  • attribute只支持字符串
  • 而property支持字符串、布尔等类型